From 21ecf3b59ca6b285fa00c657fe437d83b6f56467 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 10:09:01 +0200 Subject: [PATCH 1/6] [TrimmableTypeMap] Scanner and JCW generator fixes - CRC64 fix: Use Jones algorithm (Crc64Helper) matching the runtime, not System.IO.Hashing.Crc64 - Inherited override detection: Walk past DoNotGenerateAcw intermediate MCW base types when detecting method overrides - JCW lazy registerNatives: Application/Instrumentation types use deferred __md_registerNatives() helper instead of static initializer - Backtick sanitization: Clean generic arity markers in proxy type names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 39 ++++++-- .../Generator/Model/TypeMapAssemblyData.cs | 1 + .../Generator/ModelBuilder.cs | 4 +- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 + .../Scanner/JavaPeerScanner.cs | 22 ++--- .../Generator/JcwJavaSourceGeneratorTests.cs | 63 ++++++++++++- .../TypeMapAssemblyGeneratorTests.cs | 6 ++ .../Scanner/JavaPeerScannerTests.cs | 27 +++++- .../Scanner/OverrideDetectionTests.cs | 18 ++++ .../TestFixtures/TestTypes.cs | 93 ++++++++++++++++++- 10 files changed, 250 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 7e41fc570d7..d629fc83d2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -122,15 +122,28 @@ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) { + string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); + // Application and Instrumentation types cannot call registerNatives in their - // static initializer — the native library isn't loaded yet at that point. - // Their registerNatives call is emitted in the generated - // ApplicationRegistration.registerApplications() method instead. + // static initializer — the runtime isn't ready yet at that point. Emit a + // lazy one-time helper instead so the first managed callback can register + // the class just before invoking its native method. if (type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" + private static boolean __md_natives_registered; + private static synchronized void __md_registerNatives () + { + if (!__md_natives_registered) { + mono.android.Runtime.registerNatives ({{className}}.class); + __md_natives_registered = true; + } + } + + +"""); return; } - string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); writer.Write ($$""" static { mono.android.Runtime.registerNatives ({{className}}.class); @@ -154,7 +167,17 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) public {{simpleClassName}} ({{parameters}}) { super ({{superArgs}}); + +"""); + + if (!type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}}); + +"""); + } + + writer.Write ($$""" } @@ -197,6 +220,10 @@ static void WriteFields (JavaPeerInfo type, TextWriter writer) static void WriteMethods (JavaPeerInfo type, TextWriter writer) { + string registerNativesLine = type.CannotRegisterInStaticConstructor + ? "\t\t__md_registerNatives ();\n" + : ""; + foreach (var method in type.MarshalMethods) { if (method.IsConstructor) { continue; @@ -222,7 +249,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) @Override public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); @@ -233,7 +260,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) {{access}} {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } {{access}} native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index d21c2e56da2..8e2ddb0355f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -188,6 +188,7 @@ sealed record UcoMethodData /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Signature must match the full JNI native method signature (jnienv + self + ctor params) /// so the ABI is correct when JNI dispatches the call. +<<<<<<< HEAD /// Body: directly activates the target type using its generated activation ctor. /// sealed record UcoConstructorData diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index a8b01c45e28..c307e3bddc3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -175,7 +175,9 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash { // Use managed type name for proxy naming to guarantee uniqueness across aliases // (two types with the same JNI name will have different managed names). - var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + // Replace generic arity markers too, because backticks would make the emitted + // proxy type itself look generic even though we don't emit generic parameters. + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_').Replace ('`', '_') + "_Proxy"; // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 249bdc8def1..99399656317 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,6 +5,7 @@ $(TargetFrameworkNETStandard) enable Nullable + true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -18,6 +19,8 @@ + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5efa08b058e..fdaecc46077 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -732,7 +733,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, (RegisterInfo Info, string DeclaringTypeName, string DeclaringAssemblyName)? FindBaseRegisteredMethodInfo ( TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -760,10 +761,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up the hierarchy (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so overrides can inherit [Register] + // metadata declared above an intermediate MCW base type. return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod); } @@ -796,7 +795,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, string getterName, MethodDefinition derivedGetter) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -835,10 +834,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so property overrides can inherit + // [Register] metadata declared above an intermediate MCW base type. return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter); } @@ -1477,8 +1474,11 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } + // Keep this in sync with JavaNativeTypeManager.ToJniName(Type)/(TypeDefinition). + // The trimmable build path must emit the exact same CRC64 package names that the + // runtime later computes for FindClass(Type) and peer activation. var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = System.IO.Hashing.Crc64.Hash (data); + var hash = Crc64Helper.Compute (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 95afe0971f0..30d81218883 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,16 +138,20 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_ApplicationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyApplication"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyApplication.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyApplication.class);\n", java); } [Fact] public void Generate_InstrumentationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyInstrumentation"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyInstrumentation.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyInstrumentation.class);\n", java); } } @@ -257,6 +261,61 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } + [Fact] + public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/SelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_OverrideAcrossGenericIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/GenericSelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_DeferredRegistrationType_LazilyRegistersBeforeNativeCallback () + { + var type = new JavaPeerInfo { + JavaName = "my/app/DeferredInstrumentation", + CompatJniName = "my/app/DeferredInstrumentation", + ManagedTypeName = "MyApp.DeferredInstrumentation", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "DeferredInstrumentation", + AssemblyName = "App", + BaseJavaName = "android/app/Instrumentation", + CannotRegisterInStaticConstructor = true, + MarshalMethods = new List { + new () { + JniName = "onCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + NativeCallbackName = "n_OnCreate_Landroid_os_Bundle_", + Connector = "GetOnCreate_Landroid_os_Bundle_Handler", + }, + new () { + JniName = "onStart", + JniSignature = "()V", + ManagedMethodName = "OnStart", + NativeCallbackName = "n_OnStart", + Connector = "GetOnStartHandler", + }, + }, + }; + + var java = GenerateToString (type); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnStart ();\n", java); + } + } public class NestedType diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 4e7d17f3c6a..a99d95bfca0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -246,7 +246,13 @@ public void Generate_GenericType_ThrowsNotSupportedException () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeNames = GetTypeRefNames (reader); + var generatedTypeNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); Assert.Contains ("NotSupportedException", typeNames); + Assert.Contains ("MyApp_Generic_GenericHolder_1_Proxy", generatedTypeNames); + Assert.DoesNotContain (generatedTypeNames, name => name.Contains ('`')); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index a3b725caecb..edc7df9683f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; +using System.Text; +using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -88,11 +92,26 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () } [Theory] - [InlineData ("MyApp.PlainActivitySubclass", "crc64eb3df85c64aa1af6/PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener", "crc64eb3df85c64aa1af6/UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter", "crc64eb3df85c64aa1af6/UnregisteredExporter")] - public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, string expectedJavaName) + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_MatchesJavaNativeTypeManager (string managedName) { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssemblyPath = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + var fixtureAssembly = Assembly.LoadFrom (fixtureAssemblyPath); + var fixtureType = fixtureAssembly.GetType (managedName); + if (fixtureType is null) { + throw new InvalidOperationException ($"Could not load fixture type '{managedName}' from '{fixtureAssemblyPath}'."); + } + + var assemblyName = fixtureType.Assembly.GetName ().Name + ?? throw new InvalidOperationException ($"Could not determine assembly name for '{managedName}'."); + var data = Encoding.UTF8.GetBytes ($"{fixtureType.Namespace}:{assemblyName}"); + var hash = Crc64Helper.Compute (data); + var expectedJavaName = $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}/{fixtureType.Name}"; + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 54f7e0e133a..0213fd3ebb6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -102,6 +102,24 @@ public void MultipleOverloads_PicksCorrectOne () Assert.DoesNotContain (nonCtorMethods, m => m.JniName == "process" && m.JniSignature == "()V"); } + [Fact] + public void OverrideAcrossIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/SelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + + [Fact] + public void OverrideAcrossGenericIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/GenericSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index eac273ba746..776ca77c494 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -49,6 +49,20 @@ public class Service : Java.Lang.Object { protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("android/app/Application", DoNotGenerateAcw = true)] + public class Application : Java.Lang.Object + { + public Application () { } + protected Application (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/app/Instrumentation", DoNotGenerateAcw = true)] + public class Instrumentation : Java.Lang.Object + { + public Instrumentation () { } + protected Instrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.App.Backup @@ -317,8 +331,11 @@ public void PublicMethod () { } protected void ProtectedMethod () { } } + [Register ("my/app/BaseApplication")] + public abstract class BaseApplication : Android.App.Application { } + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] - public class MyApplication : Java.Lang.Object { } + public class MyApplication : BaseApplication { } /// /// Has [ExportField] methods that should produce Java field declarations. @@ -335,8 +352,14 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } + [Register ("my/app/BaseInstrumentation")] + public abstract class BaseInstrumentation : Android.App.Instrumentation { } + + [Register ("my/app/IntermediateInstrumentation")] + public abstract class IntermediateInstrumentation : BaseInstrumentation { } + [Instrumentation (Name = "my.app.MyInstrumentation")] - public class MyInstrumentation : Java.Lang.Object { } + public class MyInstrumentation : IntermediateInstrumentation { } [Register ("my/app/MyBackupAgent")] public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -751,6 +774,72 @@ protected OverloadDerived (IntPtr handle, JniHandleOwnership transfer) : base (h public override void Process (int value) { } } + /// + /// Declares a registered abstract method above an intermediate MCW base type. + /// Mirrors AdapterView.SetSelection(int) for AbsListView-derived test fixtures. + /// + [Register ("my/app/SelectionHost", DoNotGenerateAcw = true)] + public abstract class SelectionHost : Java.Lang.Object + { + protected SelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that inherits the registered method without redeclaring it. + /// + [Register ("my/app/SelectionContainer", DoNotGenerateAcw = true)] + public abstract class SelectionContainer : SelectionHost + { + protected SelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Generic base used to verify override discovery through a generic-instantiated base type. + /// Mirrors AdapterView<T> in the real Mono.Android hierarchy. + /// + [Register ("my/app/GenericSelectionHost", DoNotGenerateAcw = true)] + public abstract class GenericSelectionHost : Java.Lang.Object where T : class + { + protected GenericSelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that closes the generic base. + /// + [Register ("my/app/GenericSelectionContainer", DoNotGenerateAcw = true)] + public abstract class GenericSelectionContainer : GenericSelectionHost + { + protected GenericSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method declared above the first MCW base in the hierarchy. + /// + [Register ("my/app/SelectableList")] + public class SelectableList : SelectionContainer + { + protected SelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + + /// + /// Overrides a registered method declared above a generic-instantiated MCW base. + /// + [Register ("my/app/GenericSelectableList")] + public class GenericSelectableList : GenericSelectionContainer + { + protected GenericSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// From dc162a39a715a26faf60c9ccb5062431391ad577 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 16:49:49 +0200 Subject: [PATCH 2/6] [TrimmableTypeMap] Drop Crc64Helper scanner changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 -- .../Scanner/JavaPeerScanner.cs | 7 +---- .../Scanner/JavaPeerScannerTests.cs | 28 ------------------- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 99399656317..249bdc8def1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,7 +5,6 @@ $(TargetFrameworkNETStandard) enable Nullable - true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -19,8 +18,6 @@ - - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fdaecc46077..08ee7cd337d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,8 +6,6 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -using Java.Interop.Tools.JavaCallableWrappers; - namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -1474,11 +1472,8 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } - // Keep this in sync with JavaNativeTypeManager.ToJniName(Type)/(TypeDefinition). - // The trimmable build path must emit the exact same CRC64 package names that the - // runtime later computes for FindClass(Type) and peer activation. var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = Crc64Helper.Compute (data); + var hash = System.IO.Hashing.Crc64.Hash (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index edc7df9683f..d2994b5d8a7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -1,10 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Reflection; -using System.Text; -using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -90,28 +86,4 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () Assert.False (peer.DoNotGenerateAcw); Assert.True (peer.IsUnconditional, "Should be unconditional due to [Activity]"); } - - [Theory] - [InlineData ("MyApp.PlainActivitySubclass")] - [InlineData ("MyApp.UnregisteredClickListener")] - [InlineData ("MyApp.UnregisteredExporter")] - public void Scan_UnregisteredType_MatchesJavaNativeTypeManager (string managedName) - { - var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) - ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); - var fixtureAssemblyPath = Path.Combine (testAssemblyDir, "TestFixtures.dll"); - var fixtureAssembly = Assembly.LoadFrom (fixtureAssemblyPath); - var fixtureType = fixtureAssembly.GetType (managedName); - if (fixtureType is null) { - throw new InvalidOperationException ($"Could not load fixture type '{managedName}' from '{fixtureAssemblyPath}'."); - } - - var assemblyName = fixtureType.Assembly.GetName ().Name - ?? throw new InvalidOperationException ($"Could not determine assembly name for '{managedName}'."); - var data = Encoding.UTF8.GetBytes ($"{fixtureType.Namespace}:{assemblyName}"); - var hash = Crc64Helper.Compute (data); - var expectedJavaName = $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}/{fixtureType.Name}"; - - Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); - } } From 2aeb243f52813ce308d2fb9035f1cba438b17020 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 00:35:17 +0200 Subject: [PATCH 3/6] [TrimmableTypeMap] Add deferred registration regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TrimmableTypeMapGeneratorTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c07340ae981..665b98a94b9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -68,6 +68,19 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } + [Fact] + public void Execute_CollectsDeferredRegistrationTypes_ForConcreteApplicationAndInstrumentation () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + + Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.BaseApplication", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); + } + [Fact] public void Execute_NullAssemblyList_Throws () { From cf4945d680fd46dd068d47840ebfbd06d6ee5d43 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 11:01:13 +0200 Subject: [PATCH 4/6] [TrimmableTypeMap] Remove stray merge marker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 8e2ddb0355f..d21c2e56da2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -188,7 +188,6 @@ sealed record UcoMethodData /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Signature must match the full JNI native method signature (jnienv + self + ctor params) /// so the ABI is correct when JNI dispatches the call. -<<<<<<< HEAD /// Body: directly activates the target type using its generated activation ctor. /// sealed record UcoConstructorData From dc860f48cfa2b017f5a1d0873ed554533bc42134 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 11:13:49 +0200 Subject: [PATCH 5/6] [TrimmableTypeMap] Extract proxy name helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 23 +++++++++++++++++++ .../Generator/ModelBuilder.cs | 2 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 9 ++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 91670834b31..c8a47ce629f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -27,6 +28,8 @@ enum JniParamKind /// static class JniSignatureHelper { + const string ProxyTypeSuffix = "_Proxy"; + /// /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V". /// @@ -198,6 +201,26 @@ internal static string GetJavaSimpleName (string jniName) return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName; } + /// + /// Converts a managed type name into a legal generated proxy type name. + /// Replaces namespace/nesting/generic arity separators and appends "_Proxy". + /// + internal static string ManagedTypeNameToProxyTypeName (string managedTypeName) + { + if (managedTypeName is null) { + throw new ArgumentNullException (nameof (managedTypeName)); + } + + var builder = new StringBuilder (managedTypeName.Length + ProxyTypeSuffix.Length); + for (int i = 0; i < managedTypeName.Length; i++) { + char c = managedTypeName [i]; + builder.Append (c == '.' || c == '+' || c == '`' ? '_' : c); + } + + builder.Append (ProxyTypeSuffix); + return builder.ToString (); + } + /// /// Converts a JNI type descriptor to a Java source type. /// e.g., "V" \u2192 "void", "I" \u2192 "int", "Landroid/os/Bundle;" \u2192 "android.os.Bundle" diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index c307e3bddc3..4e1cd13e5ac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -177,7 +177,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash // (two types with the same JNI name will have different managed names). // Replace generic arity markers too, because backticks would make the emitted // proxy type itself look generic even though we don't emit generic parameters. - var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_').Replace ('`', '_') + "_Proxy"; + var proxyTypeName = JniSignatureHelper.ManagedTypeNameToProxyTypeName (peer.ManagedTypeName); // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 30d81218883..e08cd14f1d3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -68,6 +68,15 @@ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) Assert.Equal (expected, JniSignatureHelper.JniTypeToJava (jniType)); } + [Theory] + [InlineData ("MyApp.MainActivity", "MyApp_MainActivity_Proxy")] + [InlineData ("MyApp.Outer+Inner", "MyApp_Outer_Inner_Proxy")] + [InlineData ("MyApp.Generic.GenericHolder`1", "MyApp_Generic_GenericHolder_1_Proxy")] + public void ManagedTypeNameToProxyTypeName_ConvertsCorrectly (string managedTypeName, string expected) + { + Assert.Equal (expected, JniSignatureHelper.ManagedTypeNameToProxyTypeName (managedTypeName)); + } + } public class Filtering From 2ccac7debcad190af11ff7f36ffc45558215b614 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 11:17:47 +0200 Subject: [PATCH 6/6] [TrimmableTypeMap] Restore CRC64 test and move helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 23 ------------------- .../Generator/ModelBuilder.cs | 17 +++++++++++++- .../Generator/JcwJavaSourceGeneratorTests.cs | 9 -------- .../Generator/TypeMapModelBuilderTests.cs | 1 + .../Scanner/JavaPeerScannerTests.cs | 9 ++++++++ 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index c8a47ce629f..91670834b31 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -28,8 +27,6 @@ enum JniParamKind /// static class JniSignatureHelper { - const string ProxyTypeSuffix = "_Proxy"; - /// /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V". /// @@ -201,26 +198,6 @@ internal static string GetJavaSimpleName (string jniName) return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName; } - /// - /// Converts a managed type name into a legal generated proxy type name. - /// Replaces namespace/nesting/generic arity separators and appends "_Proxy". - /// - internal static string ManagedTypeNameToProxyTypeName (string managedTypeName) - { - if (managedTypeName is null) { - throw new ArgumentNullException (nameof (managedTypeName)); - } - - var builder = new StringBuilder (managedTypeName.Length + ProxyTypeSuffix.Length); - for (int i = 0; i < managedTypeName.Length; i++) { - char c = managedTypeName [i]; - builder.Append (c == '.' || c == '+' || c == '`' ? '_' : c); - } - - builder.Append (ProxyTypeSuffix); - return builder.ToString (); - } - /// /// Converts a JNI type descriptor to a Java source type. /// e.g., "V" \u2192 "void", "I" \u2192 "int", "Landroid/os/Bundle;" \u2192 "android.os.Bundle" diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4e1cd13e5ac..d61d716cbb3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -13,6 +14,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// static class ModelBuilder { + const string ProxyTypeSuffix = "_Proxy"; + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -171,13 +174,25 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } + static string ManagedTypeNameToProxyTypeName (string managedTypeName) + { + var builder = new StringBuilder (managedTypeName.Length + ProxyTypeSuffix.Length); + for (int i = 0; i < managedTypeName.Length; i++) { + char c = managedTypeName [i]; + builder.Append (c == '.' || c == '+' || c == '`' ? '_' : c); + } + + builder.Append (ProxyTypeSuffix); + return builder.ToString (); + } + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, HashSet usedProxyNames, bool isAcw) { // Use managed type name for proxy naming to guarantee uniqueness across aliases // (two types with the same JNI name will have different managed names). // Replace generic arity markers too, because backticks would make the emitted // proxy type itself look generic even though we don't emit generic parameters. - var proxyTypeName = JniSignatureHelper.ManagedTypeNameToProxyTypeName (peer.ManagedTypeName); + var proxyTypeName = ManagedTypeNameToProxyTypeName (peer.ManagedTypeName); // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index e08cd14f1d3..30d81218883 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -68,15 +68,6 @@ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) Assert.Equal (expected, JniSignatureHelper.JniTypeToJava (jniType)); } - [Theory] - [InlineData ("MyApp.MainActivity", "MyApp_MainActivity_Proxy")] - [InlineData ("MyApp.Outer+Inner", "MyApp_Outer_Inner_Proxy")] - [InlineData ("MyApp.Generic.GenericHolder`1", "MyApp_Generic_GenericHolder_1_Proxy")] - public void ManagedTypeNameToProxyTypeName_ConvertsCorrectly (string managedTypeName, string expected) - { - Assert.Equal (expected, JniSignatureHelper.ManagedTypeNameToProxyTypeName (managedTypeName)); - } - } public class Filtering diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 59cbf878229..df3a1256763 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -166,6 +166,7 @@ public class ProxyTypes [Theory] [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")] [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")] + [InlineData ("my/app/GenericHolder", "MyApp.Generic.GenericHolder`1", "App", "MyApp_Generic_GenericHolder_1_Proxy")] public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName) { var peer = MakePeerWithActivation (jniName, managedName, asmName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index d2994b5d8a7..a3b725caecb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -86,4 +86,13 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () Assert.False (peer.DoNotGenerateAcw); Assert.True (peer.IsUnconditional, "Should be unconditional due to [Activity]"); } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass", "crc64eb3df85c64aa1af6/PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener", "crc64eb3df85c64aa1af6/UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter", "crc64eb3df85c64aa1af6/UnregisteredExporter")] + public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, string expectedJavaName) + { + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + } }