diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs new file mode 100644 index 00000000000..5c712776bd6 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Helpers for parsing JNI method signatures. +/// +static class JniSignatureHelper +{ + /// + /// Parses the JNI parameter type descriptors from a JNI method signature + /// and returns them as records. + /// + public static List ParseParameterTypes (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + int start = i; + SkipSingleType (jniSignature, ref i); + result.Add (new JniParameterInfo { JniType = jniSignature.Substring (start, i - start) }); + } + return result; + } + + /// + /// Extracts the return type descriptor from a JNI method signature. + /// + public static string ParseReturnTypeString (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return jniSignature.Substring (i); + } + + static void SkipSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': case 'Z': case 'B': case 'C': case 'S': + case 'I': case 'J': case 'F': case 'D': + i++; + break; + case 'L': + int end = sig.IndexOf (';', i); + if (end < 0) { + throw new ArgumentException ($"Malformed JNI signature: missing ';' after 'L' at index {i} in '{sig}'"); + } + i = end + 1; + break; + case '[': + i++; + SkipSingleType (sig, ref i); + break; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index ca5063ff019..e08676da6b4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -280,6 +280,15 @@ sealed record RegisterInfo public bool DoNotGenerateAcw { get; init; } } +/// +/// Parsed [Export] attribute data for a method. +/// +sealed record ExportInfo +{ + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } +} + class TypeAttributeInfo (string attributeName) { public string AttributeName { get; } = attributeName; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index f37c0dbb85a..97ab91393f6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -15,6 +16,13 @@ sealed record JavaPeerInfo /// public required string JavaName { get; init; } + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public required string CompatJniName { get; init; } + /// /// Full managed type name, e.g., "Android.App.Activity". /// @@ -50,6 +58,14 @@ sealed record JavaPeerInfo /// public bool IsUnconditional { get; init; } + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; init; } = Array.Empty (); + /// /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. @@ -79,6 +95,99 @@ sealed record JavaPeerInfo public bool IsGenericDefinition { get; init; } } +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed record MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public required string JniName { get; init; } + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public required string JniSignature { get; init; } + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; init; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public required string ManagedMethodName { get; init; } + + /// + /// Full name of the type that declares the managed method (may be a base type). + /// Empty when the declaring type is the same as the peer type. + /// + public string DeclaringTypeName { get; init; } = ""; + + /// + /// Assembly name of the type that declares the managed method. + /// Needed for cross-assembly UCO wrapper generation. + /// Empty when the declaring type is the same as the peer type. + /// + public string DeclaringAssemblyName { get; init; } = ""; + + /// + /// The native callback method name, e.g., "n_onCreate". + /// This is the actual method the UCO wrapper delegates to. + /// + public required string NativeCallbackName { get; init; } + + /// + /// JNI parameter types for UCO generation. + /// + public IReadOnlyList Parameters { get; init; } = Array.Empty (); + + /// + /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". + /// + public required string JniReturnType { get; init; } + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; init; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; init; } + + /// + /// For [Export] methods: super constructor arguments string. + /// Null for [Register] methods. + /// + public string? SuperArgumentsString { get; init; } +} + +/// +/// Describes a JNI parameter for UCO method generation. +/// +sealed record JniParameterInfo +{ + /// + /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z". + /// + public required string JniType { get; init; } + + /// + /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32". + /// + public string ManagedType { get; init; } = ""; +} + /// /// Describes how to call the activation constructor for a Java peer type. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 86cb2340a37..442f01229ce 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -164,6 +165,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // 3. Extends a known Java peer → auto-compute JNI name via CRC64 // 4. None of the above → not a Java peer, skip string? jniName = null; + string? compatJniName = null; bool doNotGenerateAcw = false; index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); @@ -171,15 +173,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary results if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; + compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] jniName = attrInfo.JniName; + compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. // If so, auto-compute JNI name from the managed type name via CRC64. if (ExtendsJavaPeer (typeDef, index)) { - jniName = ComputeAutoJniName (typeDef, index); + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); } else { continue; } @@ -194,6 +198,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isUnconditional = attrInfo is not null; string? invokerTypeName = null; + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -214,6 +221,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var peer = new JavaPeerInfo { JavaName = jniName, + CompatJniName = compatJniName, ManagedTypeName = fullName, ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), @@ -222,6 +230,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -233,6 +242,186 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister is null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature is null && registerInfo.Connector is null) { + return; + } + + bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; + string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}"; + if (isConstructor) { + int ctorIndex = 0; + foreach (var method in methods) { + if (method.IsConstructor) { + ctorIndex++; + } + } + nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}"; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + NativeCallbackName = nativeCallbackName, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), + Parameters = JniSignatureHelper.ParseParameterTypes (registerInfo.Signature ?? "()V"), + IsConstructor = isConstructor, + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, + }); + } + + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + { + exportInfo = null; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = index.ParseRegisterAttribute (ca); + return true; + } + + if (attrName == "ExportAttribute") { + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; + } + } + registerInfo = null; + return false; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return index.ParseRegisterAttribute (ca); + } + } + return null; + } + + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = index.DecodeAttribute (ca); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (string.IsNullOrEmpty (exportName)) { + exportName = index.Reader.GetString (methodDef.Name); + } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + } + return "Ljava/lang/Object;"; + } + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); @@ -453,21 +642,29 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) } /// - /// Compute JNI name for a type without [Register] or component Name. + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. - /// If a declaring type has [Register], its JNI name is used as prefix. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. /// - static string ComputeAutoJniName (TypeDefinition typeDef, AssemblyIndex index) + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); if (parentJniName is not null) { - return $"{parentJniName}_{typeName}"; + var name = $"{parentJniName}_{typeName}"; + return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - return $"{packageName}/{typeName}"; + var jniName = $"{packageName}/{typeName}"; + + string compatName = ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + + return (jniName, compatName); } /// diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 0a29a1b9231..4ffb44c7705 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -57,6 +57,7 @@ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string manage var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, @@ -76,17 +77,32 @@ private protected static JavaPeerInfo MakePeerWithActivation (string jniName, st } private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) - => MakePeerWithActivation (jniName, managedName, asmName); + { + return MakePeerWithActivation (jniName, managedName, asmName) with { + DoNotGenerateAcw = false, + MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + JniReturnType = "V", + ManagedMethodName = ".ctor", + IsConstructor = true, + }, + }, + }; + } private protected static JavaPeerInfo MakeInterfacePeer ( - string jniName, - string managedName, - string asmName, - string invokerName) + string jniName = "android/view/View$OnClickListener", + string managedName = "Android.Views.View+IOnClickListener", + string asmName = "Mono.Android", + string invokerName = "Android.Views.View+IOnClickListenerInvoker") { var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, @@ -107,4 +123,16 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) .Select (m => reader.GetString (m.Name)) .ToList (); + + private protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig), + ManagedMethodName = jniName, + IsConstructor = isConstructor, + }; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 9b2090fcf5c..18d427744ed 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -106,6 +106,7 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut var peers = new List { new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate1", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate1", @@ -118,6 +119,7 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut }, new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate2", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate2", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9308b6cc41d..74f27ac0a22 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -183,7 +183,7 @@ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string m [Fact] public void Build_PeerWithInvoker_CreatesProxy () { - var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker"); + var peer = MakeInterfacePeer (); var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); @@ -434,7 +434,7 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName); Assert.NotNull (proxy); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 6e2795c303b..3ed0c175cf6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -1,9 +1,58 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public partial class JavaPeerScannerTests { + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var method = FindFixtureByJavaName (javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var ctors = FindFixtureByJavaName ("my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindFixtureByJavaName ("my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindFixtureByJavaName ("my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindFixtureByJavaName ("android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] @@ -40,6 +89,22 @@ public void Scan_MultipleInterfaces_AllResolved () Assert.Empty (FindFixtureByJavaName ("my/app/MyHelper").ImplementedInterfaceJavaNames); } + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + Assert.Equal (expectedCompat, FindFixtureByJavaName (javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 916431945f5..1e7b0c29f16 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -36,6 +37,8 @@ public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, public void Scan_EmptyNamespace_Handled () { Assert.Equal ("GlobalType", FindFixtureByJavaName ("my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindFixtureByManagedName ("GlobalUnregisteredType").CompatJniName); } [Theory] @@ -47,4 +50,13 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) { Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var exportMethod = FindFixtureByManagedName ("MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } }