diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
new file mode 100644
index 00000000000..ac24040ffa4
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
@@ -0,0 +1,19 @@
+
+
+
+
+ $(TargetFrameworkNETStandard)
+ latest
+ enable
+ Nullable
+ Microsoft.Android.Sdk.TrimmableTypeMap
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
new file mode 100644
index 00000000000..57dc986a994
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -0,0 +1,413 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions,
+/// all subsequent lookups are O(1) dictionary lookups.
+///
+sealed class AssemblyIndex : IDisposable
+{
+ readonly PEReader peReader;
+ internal readonly CustomAttributeTypeProvider customAttributeTypeProvider;
+
+ public MetadataReader Reader { get; }
+ public string AssemblyName { get; }
+ public string FilePath { get; }
+
+ ///
+ /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle.
+ ///
+ public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Cached [Register] attribute data per type.
+ ///
+ public Dictionary RegisterInfoByType { get; } = new ();
+
+ ///
+ /// All custom attribute data per type, pre-parsed for the attributes we care about.
+ ///
+ public Dictionary AttributesByType { get; } = new ();
+
+ ///
+ /// Type names of attributes that implement Java.Interop.IJniNameProviderAttribute
+ /// in this assembly. Used to detect JNI name providers without hardcoding attribute names.
+ ///
+ public HashSet JniNameProviderAttributes { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Merged set of all JNI name provider attribute type names across all loaded assemblies.
+ /// Set by after all assemblies are indexed.
+ ///
+ HashSet? allJniNameProviderAttributes;
+
+ AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath)
+ {
+ this.peReader = peReader;
+ this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader);
+ Reader = reader;
+ AssemblyName = assemblyName;
+ FilePath = filePath;
+ }
+
+ public static AssemblyIndex Create (string filePath)
+ {
+ var peReader = new PEReader (File.OpenRead (filePath));
+ var reader = peReader.GetMetadataReader ();
+ var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
+ var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
+ index.Build ();
+ return index;
+ }
+
+ void Build ()
+ {
+ FindJniNameProviderAttributes ();
+
+ foreach (var typeHandle in Reader.TypeDefinitions) {
+ var typeDef = Reader.GetTypeDefinition (typeHandle);
+
+ var fullName = GetFullName (typeDef, Reader);
+ if (fullName.Length == 0) {
+ continue;
+ }
+
+ TypesByFullName [fullName] = typeHandle;
+
+ var (registerInfo, attrInfo) = ParseAttributes (typeDef);
+
+ if (attrInfo != null) {
+ AttributesByType [typeHandle] = attrInfo;
+ }
+
+ if (registerInfo != null) {
+ RegisterInfoByType [typeHandle] = registerInfo;
+ }
+ }
+ }
+
+ ///
+ /// Finds all types in this assembly that implement Java.Interop.IJniNameProviderAttribute.
+ ///
+ void FindJniNameProviderAttributes ()
+ {
+ foreach (var typeHandle in Reader.TypeDefinitions) {
+ var typeDef = Reader.GetTypeDefinition (typeHandle);
+ if (ImplementsIJniNameProviderAttribute (typeDef)) {
+ var name = Reader.GetString (typeDef.Name);
+ JniNameProviderAttributes.Add (name);
+ }
+ }
+ }
+
+ bool ImplementsIJniNameProviderAttribute (TypeDefinition typeDef)
+ {
+ foreach (var implHandle in typeDef.GetInterfaceImplementations ()) {
+ var impl = Reader.GetInterfaceImplementation (implHandle);
+ if (impl.Interface.Kind == HandleKind.TypeReference) {
+ var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface);
+ var name = Reader.GetString (typeRef.Name);
+ var ns = Reader.GetString (typeRef.Namespace);
+ if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") {
+ return true;
+ }
+ } else if (impl.Interface.Kind == HandleKind.TypeDefinition) {
+ var ifaceTypeDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface);
+ var name = Reader.GetString (ifaceTypeDef.Name);
+ var ns = Reader.GetString (ifaceTypeDef.Namespace);
+ if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Sets the merged set of JNI name provider attributes from all loaded assemblies
+ /// and re-classifies any attributes that weren't recognized in the initial pass.
+ ///
+ public void ReclassifyAttributes (HashSet mergedJniNameProviders)
+ {
+ allJniNameProviderAttributes = mergedJniNameProviders;
+
+ foreach (var typeHandle in Reader.TypeDefinitions) {
+ var typeDef = Reader.GetTypeDefinition (typeHandle);
+
+ // Skip types that already have component attribute info
+ if (AttributesByType.TryGetValue (typeHandle, out var existing) && existing.HasComponentAttribute) {
+ continue;
+ }
+
+ // Re-check custom attributes with the full set of known providers
+ foreach (var caHandle in typeDef.GetCustomAttributes ()) {
+ var ca = Reader.GetCustomAttribute (caHandle);
+ var attrName = GetCustomAttributeName (ca, Reader);
+
+ if (attrName == null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") {
+ continue;
+ }
+
+ if (mergedJniNameProviders.Contains (attrName) && !IsKnownComponentAttribute (attrName)) {
+ var componentName = TryGetNameProperty (ca);
+ if (componentName != null) {
+ var attrInfo = existing ?? new TypeAttributeInfo ();
+ attrInfo.HasComponentAttribute = true;
+ attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/');
+ AttributesByType [typeHandle] = attrInfo;
+ }
+ }
+ }
+ }
+ }
+
+ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader)
+ {
+ var name = reader.GetString (typeDef.Name);
+ var ns = reader.GetString (typeDef.Namespace);
+
+ if (typeDef.IsNested) {
+ var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ());
+ var parentName = GetFullName (declaringType, reader);
+ return parentName + "+" + name;
+ }
+
+ if (ns.Length == 0) {
+ return name;
+ }
+
+ return ns + "." + name;
+ }
+
+ (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef)
+ {
+ RegisterInfo? registerInfo = null;
+ TypeAttributeInfo? attrInfo = null;
+
+ foreach (var caHandle in typeDef.GetCustomAttributes ()) {
+ var ca = Reader.GetCustomAttribute (caHandle);
+ var attrName = GetCustomAttributeName (ca, Reader);
+
+ if (attrName == null) {
+ continue;
+ }
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider);
+ } else if (attrName == "ExportAttribute") {
+ // [Export] methods are detected per-method in CollectMarshalMethods
+ } else if (IsJniNameProviderAttribute (attrName)) {
+ attrInfo ??= new TypeAttributeInfo ();
+ attrInfo.HasComponentAttribute = true;
+ var componentName = TryGetNameProperty (ca);
+ if (componentName != null) {
+ attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/');
+ }
+ if (attrName == "ApplicationAttribute") {
+ attrInfo.ApplicationBackupAgent = TryGetTypeProperty (ca, "BackupAgent");
+ attrInfo.ApplicationManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity");
+ }
+ }
+ }
+
+ return (registerInfo, attrInfo);
+ }
+
+ ///
+ /// Checks if an attribute type name is a known IJniNameProviderAttribute implementor.
+ /// Uses the local set first (from this assembly), then falls back to the merged set
+ /// (populated after all assemblies are loaded), then falls back to hardcoded names
+ /// for the well-known Android component attributes.
+ ///
+ bool IsJniNameProviderAttribute (string attrName)
+ {
+ if (JniNameProviderAttributes.Contains (attrName)) {
+ return true;
+ }
+
+ if (allJniNameProviderAttributes != null && allJniNameProviderAttributes.Contains (attrName)) {
+ return true;
+ }
+
+ // Fallback for the case where we haven't loaded the assembly defining the attribute yet.
+ // This covers the common case where user assemblies reference Mono.Android attributes.
+ return IsKnownComponentAttribute (attrName);
+ }
+
+ static bool IsKnownComponentAttribute (string attrName)
+ {
+ return attrName == "ActivityAttribute"
+ || attrName == "ServiceAttribute"
+ || attrName == "BroadcastReceiverAttribute"
+ || attrName == "ContentProviderAttribute"
+ || attrName == "ApplicationAttribute"
+ || attrName == "InstrumentationAttribute";
+ }
+
+ internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader)
+ {
+ if (ca.Constructor.Kind == HandleKind.MemberReference) {
+ var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor);
+ if (memberRef.Parent.Kind == HandleKind.TypeReference) {
+ var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent);
+ return reader.GetString (typeRef.Name);
+ }
+ } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) {
+ var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor);
+ var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ());
+ return reader.GetString (declaringType.Name);
+ }
+ return null;
+ }
+
+ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider)
+ {
+ var value = ca.DecodeValue (provider);
+
+ string jniName = "";
+ string? signature = null;
+ string? connector = null;
+ bool doNotGenerateAcw = false;
+
+ if (value.FixedArguments.Length > 0) {
+ jniName = (string?)value.FixedArguments [0].Value ?? "";
+ }
+ if (value.FixedArguments.Length > 1) {
+ signature = (string?)value.FixedArguments [1].Value;
+ }
+ if (value.FixedArguments.Length > 2) {
+ connector = (string?)value.FixedArguments [2].Value;
+ }
+
+ if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) {
+ doNotGenerateAcw = doNotGenerateAcwValue;
+ }
+
+ return new RegisterInfo (jniName, signature, connector, doNotGenerateAcw);
+ }
+
+ string? TryGetTypeProperty (CustomAttribute ca, string propertyName)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+ var typeName = TryGetNamedStringArgument (value, propertyName);
+ if (!string.IsNullOrEmpty (typeName)) {
+ return typeName;
+ }
+ return null;
+ }
+
+ string? TryGetNameProperty (CustomAttribute ca)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+
+ // Check named arguments first (e.g., [Activity(Name = "...")])
+ var name = TryGetNamedStringArgument (value, "Name");
+ if (!string.IsNullOrEmpty (name)) {
+ return name;
+ }
+
+ // Fall back to first constructor argument (e.g., [CustomJniName("...")])
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) {
+ return ctorName;
+ }
+
+ return null;
+ }
+
+ static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue)
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is bool boolValue) {
+ argumentValue = boolValue;
+ return true;
+ }
+ }
+
+ argumentValue = false;
+ return false;
+ }
+
+ static string? TryGetNamedStringArgument (CustomAttributeValue value, string argumentName)
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is string stringValue) {
+ return stringValue;
+ }
+ }
+
+ return null;
+ }
+
+ public void Dispose ()
+ {
+ peReader.Dispose ();
+ }
+}
+
+///
+/// Parsed [Register] or [Export] attribute data for a type or method.
+///
+sealed class RegisterInfo
+{
+ public string JniName { get; }
+ public string? Signature { get; }
+ public string? Connector { get; }
+ public bool DoNotGenerateAcw { get; }
+
+ ///
+ /// For [Export] methods: Java exception type names the method declares it can throw.
+ ///
+ public IReadOnlyList? ThrownNames { get; }
+
+ ///
+ /// For [Export] methods: super constructor arguments string.
+ ///
+ public string? SuperArgumentsString { get; }
+
+ public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw,
+ IReadOnlyList? thrownNames = null, string? superArgumentsString = null)
+ {
+ JniName = jniName;
+ Signature = signature;
+ Connector = connector;
+ DoNotGenerateAcw = doNotGenerateAcw;
+ ThrownNames = thrownNames;
+ SuperArgumentsString = superArgumentsString;
+ }
+}
+
+///
+/// Aggregated attribute information for a type, beyond [Register].
+///
+sealed class TypeAttributeInfo
+{
+ ///
+ /// Type has [Activity], [Service], [BroadcastReceiver], [ContentProvider],
+ /// [Application], or [Instrumentation].
+ ///
+ public bool HasComponentAttribute { get; set; }
+
+ ///
+ /// The JNI name from the Name property of a component attribute
+ /// (e.g., [Activity(Name = "my.app.MainActivity")] → "my/app/MainActivity").
+ /// Null if no Name was specified on the component attribute.
+ ///
+ public string? ComponentAttributeJniName { get; set; }
+
+ ///
+ /// If the type has [Application(BackupAgent = typeof(X))],
+ /// this is the full name of X.
+ ///
+ public string? ApplicationBackupAgent { get; set; }
+
+ ///
+ /// If the type has [Application(ManageSpaceActivity = typeof(X))],
+ /// this is the full name of X.
+ ///
+ public string? ApplicationManageSpaceActivity { get; set; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs
new file mode 100644
index 00000000000..2152e557bb8
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Minimal ICustomAttributeTypeProvider implementation for decoding
+/// custom attribute values via System.Reflection.Metadata.
+///
+sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider
+{
+ readonly MetadataReader reader;
+
+ public CustomAttributeTypeProvider (MetadataReader reader)
+ {
+ this.reader = reader;
+ }
+
+ public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString ();
+
+ public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind)
+ {
+ var typeDef = metadataReader.GetTypeDefinition (handle);
+ var name = metadataReader.GetString (typeDef.Name);
+ if (typeDef.IsNested) {
+ var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind);
+ return parent + "+" + name;
+ }
+ var ns = metadataReader.GetString (typeDef.Namespace);
+ return ns.Length > 0 ? ns + "." + name : name;
+ }
+
+ public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind)
+ {
+ var typeRef = metadataReader.GetTypeReference (handle);
+ var name = metadataReader.GetString (typeRef.Name);
+ if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) {
+ var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind);
+ return parent + "+" + name;
+ }
+ var ns = metadataReader.GetString (typeRef.Namespace);
+ return ns.Length > 0 ? ns + "." + name : name;
+ }
+
+ public string GetTypeFromSerializedName (string name) => name;
+
+ public PrimitiveTypeCode GetUnderlyingEnumType (string type)
+ {
+ // Find the enum type in this assembly's metadata and read its value__ field type.
+ foreach (var typeHandle in reader.TypeDefinitions) {
+ var typeDef = reader.GetTypeDefinition (typeHandle);
+ var name = reader.GetString (typeDef.Name);
+ var ns = reader.GetString (typeDef.Namespace);
+ var fullName = ns.Length > 0 ? ns + "." + name : name;
+
+ if (fullName != type)
+ continue;
+
+ // For enums, the first instance field is the underlying value__ field
+ foreach (var fieldHandle in typeDef.GetFields ()) {
+ var field = reader.GetFieldDefinition (fieldHandle);
+ if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0)
+ continue;
+
+ var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);
+ return sig switch {
+ "System.Byte" => PrimitiveTypeCode.Byte,
+ "System.SByte" => PrimitiveTypeCode.SByte,
+ "System.Int16" => PrimitiveTypeCode.Int16,
+ "System.UInt16" => PrimitiveTypeCode.UInt16,
+ "System.Int32" => PrimitiveTypeCode.Int32,
+ "System.UInt32" => PrimitiveTypeCode.UInt32,
+ "System.Int64" => PrimitiveTypeCode.Int64,
+ "System.UInt64" => PrimitiveTypeCode.UInt64,
+ _ => PrimitiveTypeCode.Int32,
+ };
+ }
+ }
+
+ // Default to Int32 for enums defined in other assemblies
+ return PrimitiveTypeCode.Int32;
+ }
+
+ public string GetSystemType () => "System.Type";
+
+ public string GetSZArrayType (string elementType) => elementType + "[]";
+
+ public bool IsSystemType (string type) => type == "System.Type" || type == "Type";
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
new file mode 100644
index 00000000000..5b926186d9f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Represents a Java peer type discovered during assembly scanning.
+/// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources).
+/// Generators consume this data model — they never touch PEReader/MetadataReader.
+///
+sealed class JavaPeerInfo
+{
+ ///
+ /// JNI type name, e.g., "android/app/Activity".
+ /// Extracted from the [Register] attribute.
+ ///
+ public string JavaName { get; set; } = "";
+
+ ///
+ /// 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 string CompatJniName { get; set; } = "";
+
+ ///
+ /// Full managed type name, e.g., "Android.App.Activity".
+ ///
+ public string ManagedTypeName { get; set; } = "";
+
+ ///
+ /// Assembly name the type belongs to, e.g., "Mono.Android".
+ ///
+ public string AssemblyName { get; set; } = "";
+
+ ///
+ /// JNI name of the base Java type, e.g., "android/app/Activity" for a type
+ /// that extends Activity. Null for java/lang/Object or types without a Java base.
+ /// Needed by JCW Java source generation ("extends" clause).
+ ///
+ public string? BaseJavaName { get; set; }
+
+ ///
+ /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"].
+ /// Needed by JCW Java source generation ("implements" clause).
+ ///
+ public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty ();
+
+ public bool IsInterface { get; set; }
+ public bool IsAbstract { get; set; }
+
+ ///
+ /// If true, this is a Managed Callable Wrapper (MCW) binding type.
+ /// No JCW or RegisterNatives will be generated for it.
+ ///
+ public bool DoNotGenerateAcw { get; set; }
+
+ ///
+ /// Types with component attributes ([Activity], [Service], etc.),
+ /// custom views from layout XML, or manifest-declared components
+ /// are unconditionally preserved (not trimmable).
+ ///
+ public bool IsUnconditional { get; set; }
+
+ ///
+ /// 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; set; } = 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.
+ ///
+ public ActivationCtorInfo? ActivationCtor { get; set; }
+
+ ///
+ /// For interfaces and abstract types, the name of the invoker type
+ /// used to instantiate instances from Java.
+ ///
+ public string? InvokerTypeName { get; set; }
+
+ ///
+ /// True if this is an open generic type definition.
+ /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException.
+ ///
+ public bool IsGenericDefinition { get; set; }
+}
+
+///
+/// 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 class MarshalMethodInfo
+{
+ ///
+ /// JNI method name, e.g., "onCreate".
+ /// This is the Java method name (without n_ prefix).
+ ///
+ public string JniName { get; set; } = "";
+
+ ///
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V".
+ /// Contains both parameter types and return type.
+ ///
+ public string JniSignature { get; set; } = "";
+
+ ///
+ /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler".
+ /// Null for [Export] methods.
+ ///
+ public string? Connector { get; set; }
+
+ ///
+ /// Name of the managed method this maps to, e.g., "OnCreate".
+ ///
+ public string ManagedMethodName { get; set; } = "";
+
+ ///
+ /// True if this is a constructor registration.
+ ///
+ public bool IsConstructor { get; set; }
+
+ ///
+ /// For [Export] methods: Java exception types that the method declares it can throw.
+ /// Null for [Register] methods.
+ ///
+ public IReadOnlyList? ThrownNames { get; set; }
+
+ ///
+ /// For [Export] methods: super constructor arguments string.
+ /// Null for [Register] methods.
+ ///
+ public string? SuperArgumentsString { get; set; }
+}
+
+///
+/// Describes how to call the activation constructor for a Java peer type.
+///
+sealed class ActivationCtorInfo
+{
+ ///
+ /// The type that declares the activation constructor.
+ /// May be the type itself or a base type.
+ ///
+ public string DeclaringTypeName { get; set; } = "";
+
+ ///
+ /// The assembly containing the declaring type.
+ ///
+ public string DeclaringAssemblyName { get; set; } = "";
+
+ ///
+ /// The style of activation constructor found.
+ ///
+ public ActivationCtorStyle Style { get; set; }
+}
+
+enum ActivationCtorStyle
+{
+ ///
+ /// Xamarin.Android style: (IntPtr handle, JniHandleOwnership transfer)
+ ///
+ XamarinAndroid,
+
+ ///
+ /// Java.Interop style: (ref JniObjectReference reference, JniObjectReferenceOptions options)
+ ///
+ JavaInterop,
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
new file mode 100644
index 00000000000..fe15dfb159b
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -0,0 +1,718 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Scans assemblies for Java peer types using System.Reflection.Metadata.
+/// Two-phase architecture:
+/// Phase 1: Build per-assembly indices (fast, O(1) lookups)
+/// Phase 2: Analyze types using cached indices
+///
+sealed class JavaPeerScanner : IDisposable
+{
+ readonly Dictionary assemblyCache = new (StringComparer.Ordinal);
+ readonly Dictionary activationCtorCache = new (StringComparer.Ordinal);
+
+ ///
+ /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex.
+ /// Checks the specified assembly (by name) in the assembly cache.
+ ///
+ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex)
+ {
+ if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) &&
+ resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) {
+ return true;
+ }
+ handle = default;
+ resolvedIndex = null!;
+ return false;
+ }
+
+ ///
+ /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling
+ /// nested types whose ResolutionScope is another TypeReference.
+ ///
+ static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index)
+ {
+ var typeRef = index.Reader.GetTypeReference (handle);
+ var name = index.Reader.GetString (typeRef.Name);
+ var ns = index.Reader.GetString (typeRef.Namespace);
+
+ var scope = typeRef.ResolutionScope;
+ switch (scope.Kind) {
+ case HandleKind.AssemblyReference: {
+ var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope);
+ var fullName = ns.Length > 0 ? ns + "." + name : name;
+ return (fullName, index.Reader.GetString (asmRef.Name));
+ }
+ case HandleKind.TypeReference: {
+ // Nested type: recurse to get the declaring type's full name and assembly
+ var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index);
+ return (parentFullName + "+" + name, assemblyName);
+ }
+ default: {
+ var fullName = ns.Length > 0 ? ns + "." + name : name;
+ return (fullName, index.AssemblyName);
+ }
+ }
+ }
+
+ ///
+ /// Looks up the [Register] JNI name for a type identified by name + assembly.
+ ///
+ string? ResolveRegisterJniName (string typeName, string assemblyName)
+ {
+ if (TryResolveType (typeName, assemblyName, out var handle, out var resolvedIndex) &&
+ resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo)) {
+ return regInfo.JniName;
+ }
+ return null;
+ }
+
+ ///
+ /// Phase 1: Build indices for all assemblies.
+ /// Phase 2: Scan all types and produce JavaPeerInfo records.
+ ///
+ public List Scan (IReadOnlyList assemblyPaths)
+ {
+ // Phase 1: Build indices for all assemblies
+ foreach (var path in assemblyPaths) {
+ var index = AssemblyIndex.Create (path);
+ assemblyCache [index.AssemblyName] = index;
+ }
+
+ // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies
+ // and re-classify any attributes that weren't recognized in the initial pass
+ // (e.g., user assembly references ActivityAttribute from Mono.Android.dll).
+ var mergedJniNameProviders = new HashSet (StringComparer.Ordinal);
+ foreach (var index in assemblyCache.Values) {
+ mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes);
+ }
+ foreach (var index in assemblyCache.Values) {
+ index.ReclassifyAttributes (mergedJniNameProviders);
+ }
+
+ // Phase 2: Analyze types using cached indices
+ var resultsByManagedName = new Dictionary (StringComparer.Ordinal);
+
+ foreach (var index in assemblyCache.Values) {
+ ScanAssembly (index, resultsByManagedName);
+ }
+
+ // Phase 3: Force unconditional on types referenced by [Application] attributes
+ ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache);
+
+ return new List (resultsByManagedName.Values);
+ }
+
+ ///
+ /// Types referenced by [Application(BackupAgent = typeof(X))] or
+ /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional,
+ /// because the manifest will reference them even if nothing else does.
+ ///
+ static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache)
+ {
+ foreach (var index in assemblyCache.Values) {
+ foreach (var attrInfo in index.AttributesByType.Values) {
+ ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent);
+ ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity);
+ }
+ }
+ }
+
+ static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName)
+ {
+ if (managedTypeName == null) {
+ return;
+ }
+
+ // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..."
+ // Strip to just the type name for lookup
+ var commaIndex = managedTypeName.IndexOf (',');
+ if (commaIndex > 0) {
+ managedTypeName = managedTypeName.Substring (0, commaIndex).Trim ();
+ }
+
+ if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) {
+ peer.IsUnconditional = true;
+ }
+ }
+
+ void ScanAssembly (AssemblyIndex index, Dictionary results)
+ {
+ foreach (var typeHandle in index.Reader.TypeDefinitions) {
+ var typeDef = index.Reader.GetTypeDefinition (typeHandle);
+
+ // Skip module-level types
+ if (index.Reader.GetString (typeDef.Name) == "") {
+ continue;
+ }
+
+ // Determine the JNI name and whether this is a known Java peer.
+ // Priority:
+ // 1. [Register] attribute → use JNI name from attribute
+ // 2. Component attribute Name property → convert dots to slashes
+ // 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);
+ index.AttributesByType.TryGetValue (typeHandle, out var attrInfo);
+
+ if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) {
+ jniName = registerInfo.JniName;
+ compatJniName = jniName;
+ doNotGenerateAcw = registerInfo.DoNotGenerateAcw;
+ } else if (attrInfo?.ComponentAttributeJniName != null) {
+ // User type with [Activity(Name = "...")] but no [Register]
+ jniName = attrInfo.ComponentAttributeJniName;
+ 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, compatJniName) = ComputeAutoJniNames (typeDef, index);
+ } else {
+ continue;
+ }
+ }
+
+ var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader);
+
+ var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0;
+ var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0;
+ var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0;
+
+ var isUnconditional = attrInfo?.HasComponentAttribute ?? false;
+ string? invokerTypeName = null;
+
+ // Resolve base Java type name
+ var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
+
+ // Resolve implemented Java interface names
+ var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index);
+
+ // 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);
+
+ // For interfaces/abstract types, try to find invoker type name
+ if (isInterface || isAbstract) {
+ invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index);
+ }
+
+ var peer = new JavaPeerInfo {
+ JavaName = jniName,
+ CompatJniName = compatJniName,
+ ManagedTypeName = fullName,
+ AssemblyName = index.AssemblyName,
+ BaseJavaName = baseJavaName,
+ ImplementedInterfaceJavaNames = implementedInterfaces,
+ IsInterface = isInterface,
+ IsAbstract = isAbstract,
+ DoNotGenerateAcw = doNotGenerateAcw,
+ IsUnconditional = isUnconditional,
+ MarshalMethods = marshalMethods,
+ ActivationCtor = activationCtor,
+ InvokerTypeName = invokerTypeName,
+ IsGenericDefinition = isGenericDefinition,
+ };
+
+ results [fullName] = peer;
+ }
+ }
+
+ 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);
+ var registerInfo = TryGetMethodRegisterInfo (methodDef, index);
+ if (registerInfo == null) {
+ continue;
+ }
+
+ AddMarshalMethod (methods, registerInfo, methodDef, index);
+ }
+
+ // 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 == 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)
+ {
+ // Skip methods that are just the JNI name (type-level [Register])
+ if (registerInfo.Signature == null && registerInfo.Connector == null) {
+ return;
+ }
+
+ methods.Add (new MarshalMethodInfo {
+ JniName = registerInfo.JniName,
+ JniSignature = registerInfo.Signature ?? "()V",
+ Connector = registerInfo.Connector,
+ ManagedMethodName = index.Reader.GetString (methodDef.Name),
+ IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor",
+ ThrownNames = registerInfo.ThrownNames,
+ SuperArgumentsString = registerInfo.SuperArgumentsString,
+ });
+ }
+
+ string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
+ {
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo == null) {
+ return null;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ // First try [Register] attribute
+ var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
+ if (registerJniName != null) {
+ return registerJniName;
+ }
+
+ // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
+ if (results.TryGetValue (baseTypeName, out var basePeer)) {
+ return basePeer.JavaName;
+ }
+
+ return null;
+ }
+
+ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var result = new List ();
+ var interfaceImpls = typeDef.GetInterfaceImplementations ();
+
+ foreach (var implHandle in interfaceImpls) {
+ var impl = index.Reader.GetInterfaceImplementation (implHandle);
+ var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index);
+ if (ifaceJniName != null) {
+ result.Add (ifaceJniName);
+ }
+ }
+
+ return result;
+ }
+
+ string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
+ {
+ var resolved = ResolveEntityHandle (interfaceHandle, index);
+ return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
+ }
+
+ static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index)
+ {
+ foreach (var caHandle in methodDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ }
+
+ if (attrName == "ExportAttribute") {
+ return ParseExportAttribute (ca, methodDef, index);
+ }
+ }
+ return null;
+ }
+
+ 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 AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ }
+ }
+ return null;
+ }
+
+ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
+ {
+ var value = ca.DecodeValue (index.customAttributeTypeProvider);
+
+ // [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 string[] names) {
+ thrownNames = new List (names);
+ } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) {
+ superArguments = superArgs;
+ }
+ }
+
+ if (exportName == null || exportName.Length == 0) {
+ exportName = index.Reader.GetString (methodDef.Name);
+ }
+
+ // Build JNI signature from method signature
+ var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+ var jniSig = BuildJniSignatureFromManaged (sig);
+
+ return new RegisterInfo (exportName, jniSig, null, false,
+ 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)
+ {
+ if (activationCtorCache.TryGetValue (typeName, out var cached)) {
+ return cached;
+ }
+
+ // Check this type's constructors
+ var ownCtor = FindActivationCtorOnType (typeDef, index);
+ if (ownCtor != null) {
+ var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value };
+ activationCtorCache [typeName] = info;
+ return info;
+ }
+
+ // Walk base type hierarchy
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo != null) {
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+ if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) {
+ var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
+ var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex);
+ if (result != null) {
+ activationCtorCache [typeName] = result;
+ }
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var method = index.Reader.GetMethodDefinition (methodHandle);
+ var name = index.Reader.GetString (method.Name);
+
+ if (name != ".ctor") {
+ continue;
+ }
+
+ var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+
+ // XI style: (IntPtr, JniHandleOwnership)
+ if (sig.ParameterTypes.Length == 2 &&
+ sig.ParameterTypes [0] == "System.IntPtr" &&
+ sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") {
+ return ActivationCtorStyle.XamarinAndroid;
+ }
+
+ // JI style: (ref JniObjectReference, JniObjectReferenceOptions)
+ if (sig.ParameterTypes.Length == 2 &&
+ (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") &&
+ sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") {
+ return ActivationCtorStyle.JavaInterop;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying
+ /// type's (fullName, assemblyName) by reading the raw signature blob.
+ ///
+ static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index)
+ {
+ var typeSpec = index.Reader.GetTypeSpecification (specHandle);
+ var blobReader = index.Reader.GetBlobReader (typeSpec.Signature);
+
+ // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args...
+ var elementType = blobReader.ReadByte ();
+ if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST
+ return null;
+ }
+
+ var classOrValueType = blobReader.ReadByte ();
+ if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE
+ return null;
+ }
+
+ // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec)
+ var codedToken = blobReader.ReadCompressedInteger ();
+ var tag = codedToken & 0x3;
+ var row = codedToken >> 2;
+
+ switch (tag) {
+ case 0: { // TypeDef
+ var handle = MetadataTokens.TypeDefinitionHandle (row);
+ var baseDef = index.Reader.GetTypeDefinition (handle);
+ return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName);
+ }
+ case 1: // TypeRef
+ return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index);
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName).
+ /// Shared by base type resolution, interface resolution, and any handle-to-name lookup.
+ ///
+ (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index)
+ {
+ switch (handle.Kind) {
+ case HandleKind.TypeDefinition: {
+ var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle);
+ return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName);
+ }
+ case HandleKind.TypeReference:
+ return ResolveTypeReference ((TypeReferenceHandle)handle, index);
+ case HandleKind.TypeSpecification:
+ return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index);
+ default:
+ return null;
+ }
+ }
+
+ (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index);
+ }
+
+ string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index)
+ {
+ // First, check the [Register] attribute's connector arg (3rd arg).
+ // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")]
+ // where the connector contains the assembly-qualified invoker type name.
+ if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) {
+ var connector = registerInfo.Connector;
+ // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..."
+ // We want just the type name (before the first comma, if any)
+ var commaIndex = connector.IndexOf (',');
+ if (commaIndex > 0) {
+ return connector.Substring (0, commaIndex).Trim ();
+ }
+ if (connector.Length > 0) {
+ return connector;
+ }
+ }
+
+ // Fallback: convention-based lookup — invoker type is TypeName + "Invoker"
+ var invokerName = typeName + "Invoker";
+ if (index.TypesByFullName.ContainsKey (invokerName)) {
+ return invokerName;
+ }
+ return null;
+ }
+
+ public void Dispose ()
+ {
+ foreach (var index in assemblyCache.Values) {
+ index.Dispose ();
+ }
+ assemblyCache.Clear ();
+ }
+
+ readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal);
+
+ ///
+ /// Check if a type extends a known Java peer (has [Register] or component attribute)
+ /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles.
+ ///
+ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader);
+ var key = index.AssemblyName + ":" + fullName;
+
+ if (extendsJavaPeerCache.TryGetValue (key, out var cached)) {
+ return cached;
+ }
+
+ // Mark as false to prevent cycles, then compute
+ extendsJavaPeerCache [key] = false;
+
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo == null) {
+ return false;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) {
+ return false;
+ }
+
+ // Direct hit: base has [Register] or component attribute
+ if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) {
+ extendsJavaPeerCache [key] = true;
+ return true;
+ }
+ if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) {
+ extendsJavaPeerCache [key] = true;
+ return true;
+ }
+
+ // Recurse up the hierarchy
+ var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
+ var result = ExtendsJavaPeer (baseDef, baseIndex);
+ extendsJavaPeerCache [key] = result;
+ return result;
+ }
+
+ ///
+ /// 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.
+ /// 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 jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index);
+
+ if (parentJniName != null) {
+ var name = parentJniName + "_" + typeName;
+ return (name, name);
+ }
+
+ var packageName = GetCrc64PackageName (ns, index.AssemblyName);
+ var jniName = packageName + "/" + typeName;
+
+ string compatName = ns.Length == 0
+ ? typeName
+ : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName;
+
+ return (jniName, compatName);
+ }
+
+ ///
+ /// Builds the type name part (handling nesting) and returns either a parent's
+ /// registered JNI name or the outermost namespace.
+ /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types
+ /// and if a parent has [Register] or a component attribute JNI name, uses that
+ /// as prefix instead of computing CRC64 from the namespace.
+ ///
+ static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var firstName = index.Reader.GetString (typeDef.Name).Replace ('`', '_');
+
+ // Fast path: non-nested types (the vast majority)
+ if (!typeDef.IsNested) {
+ return (firstName, null, index.Reader.GetString (typeDef.Namespace));
+ }
+
+ // Nested type: walk up declaring types, collecting name parts
+ var nameParts = new List (4) { firstName };
+ var current = typeDef;
+ string? parentJniName = null;
+
+ do {
+ var parentHandle = current.GetDeclaringType ();
+ current = index.Reader.GetTypeDefinition (parentHandle);
+
+ // Check if the parent has a registered JNI name
+ if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !string.IsNullOrEmpty (parentRegister.JniName)) {
+ parentJniName = parentRegister.JniName;
+ break;
+ }
+ if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) {
+ parentJniName = parentAttr.ComponentAttributeJniName;
+ break;
+ }
+
+ nameParts.Add (index.Reader.GetString (current.Name).Replace ('`', '_'));
+ } while (current.IsNested);
+
+ nameParts.Reverse ();
+ var typeName = string.Join ("_", nameParts);
+ var ns = index.Reader.GetString (current.Namespace);
+
+ return (typeName, parentJniName, ns);
+ }
+
+ static string GetCrc64PackageName (string ns, string assemblyName)
+ {
+ // Only Mono.Android preserves the namespace directly
+ if (assemblyName == "Mono.Android") {
+ return ns.ToLowerInvariant ().Replace ('.', '/');
+ }
+
+ var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName);
+ var hash = System.IO.Hashing.Crc64.Hash (data);
+ return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ();
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs
new file mode 100644
index 00000000000..185f10c89bd
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Minimal ISignatureTypeProvider implementation for decoding method
+/// signatures via System.Reflection.Metadata.
+/// Returns fully qualified type name strings.
+///
+sealed class SignatureTypeProvider : ISignatureTypeProvider
+{
+ public static readonly SignatureTypeProvider Instance = new ();
+
+ public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch {
+ PrimitiveTypeCode.Void => "System.Void",
+ PrimitiveTypeCode.Boolean => "System.Boolean",
+ PrimitiveTypeCode.Char => "System.Char",
+ PrimitiveTypeCode.SByte => "System.SByte",
+ PrimitiveTypeCode.Byte => "System.Byte",
+ PrimitiveTypeCode.Int16 => "System.Int16",
+ PrimitiveTypeCode.UInt16 => "System.UInt16",
+ PrimitiveTypeCode.Int32 => "System.Int32",
+ PrimitiveTypeCode.UInt32 => "System.UInt32",
+ PrimitiveTypeCode.Int64 => "System.Int64",
+ PrimitiveTypeCode.UInt64 => "System.UInt64",
+ PrimitiveTypeCode.Single => "System.Single",
+ PrimitiveTypeCode.Double => "System.Double",
+ PrimitiveTypeCode.String => "System.String",
+ PrimitiveTypeCode.Object => "System.Object",
+ PrimitiveTypeCode.IntPtr => "System.IntPtr",
+ PrimitiveTypeCode.UIntPtr => "System.UIntPtr",
+ PrimitiveTypeCode.TypedReference => "System.TypedReference",
+ _ => typeCode.ToString (),
+ };
+
+ public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
+ {
+ var typeDef = reader.GetTypeDefinition (handle);
+ var ns = reader.GetString (typeDef.Namespace);
+ var name = reader.GetString (typeDef.Name);
+ if (typeDef.IsNested) {
+ var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind);
+ return parent + "+" + name;
+ }
+ return ns.Length > 0 ? ns + "." + name : name;
+ }
+
+ public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ {
+ var typeRef = reader.GetTypeReference (handle);
+ var name = reader.GetString (typeRef.Name);
+
+ // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively
+ if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) {
+ var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind);
+ return parent + "+" + name;
+ }
+
+ var ns = reader.GetString (typeRef.Namespace);
+ return ns.Length > 0 ? ns + "." + name : name;
+ }
+
+ public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind)
+ {
+ var typeSpec = reader.GetTypeSpecification (handle);
+ return typeSpec.DecodeSignature (this, genericContext);
+ }
+
+ public string GetSZArrayType (string elementType) => elementType + "[]";
+ public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]";
+ public string GetByReferenceType (string elementType) => elementType + "&";
+ public string GetPointerType (string elementType) => elementType + "*";
+ public string GetPinnedType (string elementType) => elementType;
+ public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType;
+
+ public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments)
+ {
+ return genericType + "<" + string.Join (",", typeArguments) + ">";
+ }
+
+ public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index;
+ public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index;
+
+ public string GetFunctionPointerType (MethodSignature signature) => "delegate*";
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj
new file mode 100644
index 00000000000..6370a77e680
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj
@@ -0,0 +1,38 @@
+
+
+
+ $(DotNetStableTargetFramework)
+ latest
+ enable
+ false
+ Microsoft.Android.Sdk.TrimmableTypeMap.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" />
+
+
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
new file mode 100644
index 00000000000..f9b77c84137
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -0,0 +1,357 @@
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public partial class JavaPeerScannerTests
+{
+ [Fact]
+ public void Scan_TypeWithRegisteredCtors_HasConstructorMarshalMethods ()
+ {
+ var peers = ScanFixtures ();
+ var customView = FindByJavaName (peers, "my/app/CustomView");
+ var ctors = 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);
+ }
+
+ [Fact]
+ public void Scan_TypeWithoutRegisteredCtors_HasNoConstructorMarshalMethods ()
+ {
+ var peers = ScanFixtures ();
+ var helper = FindByJavaName (peers, "my/app/MyHelper");
+ Assert.DoesNotContain (helper.MarshalMethods, m => m.IsConstructor);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_JniNameIsJavaMethodName ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ var onCreate = activity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate");
+ Assert.NotNull (onCreate);
+ Assert.Equal ("onCreate", onCreate.JniName);
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithActivityName_DiscoveredWithoutRegister ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.False (mainActivity.DoNotGenerateAcw);
+ Assert.True (mainActivity.IsUnconditional, "Types with [Activity] are unconditional");
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithServiceName_DiscoveredWithoutRegister ()
+ {
+ var peers = ScanFixtures ();
+ var service = FindByJavaName (peers, "my/app/MyService");
+ Assert.False (service.DoNotGenerateAcw);
+ Assert.True (service.IsUnconditional);
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithBroadcastReceiverName_DiscoveredWithoutRegister ()
+ {
+ var peers = ScanFixtures ();
+ var receiver = FindByJavaName (peers, "my/app/MyReceiver");
+ Assert.False (receiver.DoNotGenerateAcw);
+ Assert.True (receiver.IsUnconditional);
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithContentProviderName_DiscoveredWithoutRegister ()
+ {
+ var peers = ScanFixtures ();
+ var provider = FindByJavaName (peers, "my/app/MyProvider");
+ Assert.False (provider.DoNotGenerateAcw);
+ Assert.True (provider.IsUnconditional);
+ }
+
+ [Fact]
+ public void Scan_Throwable_IsMcwType ()
+ {
+ var peers = ScanFixtures ();
+ var throwable = FindByJavaName (peers, "java/lang/Throwable");
+ Assert.True (throwable.DoNotGenerateAcw);
+ Assert.Equal ("java/lang/Object", throwable.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_Exception_ExtendsThrowable ()
+ {
+ var peers = ScanFixtures ();
+ var exception = FindByJavaName (peers, "java/lang/Exception");
+ Assert.True (exception.DoNotGenerateAcw);
+ Assert.Equal ("java/lang/Throwable", exception.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_NestedType_IsDiscovered ()
+ {
+ var peers = ScanFixtures ();
+ var inner = FindByJavaName (peers, "my/app/Outer$Inner");
+ Assert.Equal ("MyApp.Outer+Inner", inner.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Scan_NestedTypeInInterface_IsDiscovered ()
+ {
+ var peers = ScanFixtures ();
+ var result = FindByJavaName (peers, "my/app/ICallback$Result");
+ Assert.Equal ("MyApp.ICallback+Result", result.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_BoolReturn_HasCorrectJniSignature ()
+ {
+ var peers = ScanFixtures ();
+ var handler = FindByJavaName (peers, "my/app/TouchHandler");
+ var onTouch = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onTouch");
+ Assert.NotNull (onTouch);
+ Assert.Equal ("(Landroid/view/View;I)Z", onTouch.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_BoolParam_HasCorrectJniSignature ()
+ {
+ var peers = ScanFixtures ();
+ var handler = FindByJavaName (peers, "my/app/TouchHandler");
+ var onFocus = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onFocusChange");
+ Assert.NotNull (onFocus);
+ Assert.Equal ("(Landroid/view/View;Z)V", onFocus.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_MultiplePrimitiveParams ()
+ {
+ var peers = ScanFixtures ();
+ var handler = FindByJavaName (peers, "my/app/TouchHandler");
+ var onScroll = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onScroll");
+ Assert.NotNull (onScroll);
+ Assert.Equal ("(IFJD)V", onScroll.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_ArrayParam ()
+ {
+ var peers = ScanFixtures ();
+ var handler = FindByJavaName (peers, "my/app/TouchHandler");
+ var setItems = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "setItems");
+ Assert.NotNull (setItems);
+ Assert.Equal ("([Ljava/lang/String;)V", setItems.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_ExportMethod_CollectedAsMarshalMethod ()
+ {
+ var peers = ScanFixtures ();
+ var export = FindByJavaName (peers, "my/app/ExportExample");
+ Assert.Single (export.MarshalMethods);
+ var method = export.MarshalMethods [0];
+ Assert.Equal ("myExportedMethod", method.JniName);
+ Assert.Null (method.Connector);
+ }
+
+ [Fact]
+ public void Scan_CustomView_DiscoveredAsRegularType ()
+ {
+ var peers = ScanFixtures ();
+ var customView = FindByJavaName (peers, "my/app/CustomView");
+ Assert.False (customView.IsUnconditional, "Custom views are not unconditional by attribute alone");
+ Assert.Equal ("android/view/View", customView.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_ApplicationType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var app = FindByJavaName (peers, "my/app/MyApplication");
+ Assert.True (app.IsUnconditional, "[Application] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_InstrumentationType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var instr = FindByJavaName (peers, "my/app/MyInstrumentation");
+ Assert.True (instr.IsUnconditional, "[Instrumentation] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_BackupAgent_ForcedUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var backupAgent = FindByJavaName (peers, "my/app/MyBackupAgent");
+ Assert.True (backupAgent.IsUnconditional, "BackupAgent referenced from [Application] should be forced unconditional");
+ }
+
+ [Fact]
+ public void Scan_ManageSpaceActivity_ForcedUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "my/app/MyManageSpaceActivity");
+ Assert.True (activity.IsUnconditional, "ManageSpaceActivity referenced from [Application] should be forced unconditional");
+ }
+
+ [Fact]
+ public void Scan_BackupAgent_NotUnconditional_WhenNotReferenced ()
+ {
+ // A type extending BackupAgent that is NOT referenced from [Application]
+ // should remain trimmable (not unconditional).
+ var peers = ScanFixtures ();
+ var helper = FindByJavaName (peers, "my/app/MyHelper");
+ Assert.False (helper.IsUnconditional, "Unreferenced type should remain trimmable");
+ }
+
+ [Fact]
+ public void Scan_McwBinding_CompatJniNameEqualsJavaName ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.Equal (activity.JavaName, activity.CompatJniName);
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithRegister_CompatJniNameEqualsJavaName ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.Equal (mainActivity.JavaName, mainActivity.CompatJniName);
+ }
+
+ [Fact]
+ public void Scan_UserTypeWithoutRegister_CompatJniNameUsesRawNamespace ()
+ {
+ var peers = ScanFixtures ();
+ var unregistered = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredHelper");
+ Assert.NotNull (unregistered);
+
+ // JavaName should use CRC64 package
+ Assert.StartsWith ("crc64", unregistered.JavaName);
+
+ // CompatJniName should use the raw namespace
+ Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName);
+ }
+
+ [Fact]
+ public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var widget = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.CustomWidget");
+ Assert.NotNull (widget);
+
+ // The custom attribute provides the JNI name via IJniNameProviderAttribute
+ Assert.Equal ("com/example/CustomWidget", widget.JavaName);
+ Assert.Equal ("com/example/CustomWidget", widget.CompatJniName);
+ }
+
+ [Fact]
+ public void Scan_DeepHierarchy_ResolvesBaseJavaName ()
+ {
+ var peers = ScanFixtures ();
+ var myButton = FindByJavaName (peers, "my/app/MyButton");
+ Assert.Equal ("android/widget/Button", myButton.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_DeepHierarchy_InheritsActivationCtor ()
+ {
+ var peers = ScanFixtures ();
+ var myButton = FindByJavaName (peers, "my/app/MyButton");
+ Assert.NotNull (myButton.ActivationCtor);
+ // MyButton → Button → View → Java.Lang.Object — should find XI ctor from View or Object
+ Assert.Equal (ActivationCtorStyle.XamarinAndroid, myButton.ActivationCtor.Style);
+ }
+
+ [Fact]
+ public void Scan_MultipleInterfaces_AllResolved ()
+ {
+ var peers = ScanFixtures ();
+ var multi = FindByJavaName (peers, "my/app/MultiInterfaceView");
+ Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames);
+ Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames);
+ Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count);
+ }
+
+ [Fact]
+ public void Scan_AbstractMethod_CollectedAsMarshalMethod ()
+ {
+ var peers = ScanFixtures ();
+ var abstractBase = FindByJavaName (peers, "my/app/AbstractBase");
+ var doWork = abstractBase.MarshalMethods.FirstOrDefault (m => m.JniName == "doWork");
+ Assert.NotNull (doWork);
+ Assert.Equal ("()V", doWork.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_PropertyRegister_CollectedAsMarshalMethod ()
+ {
+ var peers = ScanFixtures ();
+ var throwable = FindByJavaName (peers, "java/lang/Throwable");
+ var getMessage = throwable.MarshalMethods.FirstOrDefault (m => m.JniName == "getMessage");
+ Assert.NotNull (getMessage);
+ Assert.Equal ("()Ljava/lang/String;", getMessage.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_MethodWithEmptyConnector_Collected ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ var onStart = activity.MarshalMethods.FirstOrDefault (m => m.JniName == "onStart");
+ Assert.NotNull (onStart);
+ Assert.Equal ("", onStart.Connector);
+ }
+
+ [Fact]
+ public void Scan_InvokerWithRegisterAndDoNotGenerateAcw_IsIncluded ()
+ {
+ var peers = ScanFixtures ();
+ // IOnClickListenerInvoker has [Register("android/view/View$OnClickListener", DoNotGenerateAcw=true)]
+ // It should be included in the scanner output — generators will filter it later
+ var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker");
+ Assert.NotNull (invoker);
+ Assert.True (invoker.DoNotGenerateAcw);
+ Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName);
+ }
+
+ [Fact]
+ public void Scan_Interface_HasInvokerTypeNameFromRegisterConnector ()
+ {
+ var peers = ScanFixtures ();
+ var listener = FindByManagedName (peers, "Android.Views.IOnClickListener");
+ Assert.NotNull (listener.InvokerTypeName);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName);
+ }
+
+ [Fact]
+ public void Scan_Interface_IsNotMarkedDoNotGenerateAcw ()
+ {
+ var peers = ScanFixtures ();
+ var listener = FindByManagedName (peers, "Android.Views.IOnClickListener");
+ // Interfaces have [Register("name", "", "connector")] — the 3-arg form doesn't set DoNotGenerateAcw
+ Assert.False (listener.DoNotGenerateAcw, "Interfaces should not have DoNotGenerateAcw");
+ }
+
+ [Fact]
+ public void Scan_InterfaceMethod_CollectedAsMarshalMethod ()
+ {
+ var peers = ScanFixtures ();
+ var listener = FindByManagedName (peers, "Android.Views.IOnClickListener");
+ var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick");
+ Assert.NotNull (onClick);
+ Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_GenericType_HasCorrectManagedTypeName ()
+ {
+ var peers = ScanFixtures ();
+ var generic = FindByJavaName (peers, "my/app/GenericHolder");
+ Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName);
+ }
+
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
new file mode 100644
index 00000000000..34e5976b198
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public partial class JavaPeerScannerTests
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!;
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ List ScanFixtures ()
+ {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ }
+
+ JavaPeerInfo FindByJavaName (List peers, string javaName)
+ {
+ var peer = peers.FirstOrDefault (p => p.JavaName == javaName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ JavaPeerInfo FindByManagedName (List peers, string managedName)
+ {
+ var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ [Fact]
+ public void Scan_FindsAllJavaPeerTypes ()
+ {
+ var peers = ScanFixtures ();
+ Assert.NotEmpty (peers);
+ // MCW types with [Register]
+ Assert.Contains (peers, p => p.JavaName == "java/lang/Object");
+ Assert.Contains (peers, p => p.JavaName == "android/app/Activity");
+ // User type with JNI name from [Activity(Name="...")]
+ Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity");
+ // Exception/Throwable hierarchy
+ Assert.Contains (peers, p => p.JavaName == "java/lang/Throwable");
+ Assert.Contains (peers, p => p.JavaName == "java/lang/Exception");
+ }
+
+ [Fact]
+ public void Scan_McwTypes_HaveDoNotGenerateAcw ()
+ {
+ var peers = ScanFixtures ();
+
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.True (activity.DoNotGenerateAcw, "Activity should be MCW (DoNotGenerateAcw=true)");
+
+ var button = FindByJavaName (peers, "android/widget/Button");
+ Assert.True (button.DoNotGenerateAcw, "Button should be MCW");
+ }
+
+ [Fact]
+ public void Scan_UserTypes_DoNotGenerateAcwIsFalse ()
+ {
+ var peers = ScanFixtures ();
+
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.False (mainActivity.DoNotGenerateAcw, "MainActivity should not have DoNotGenerateAcw");
+ }
+
+ [Fact]
+ public void Scan_ActivityType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.True (mainActivity.IsUnconditional, "MainActivity with [Activity] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_ServiceType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var service = FindByJavaName (peers, "my/app/MyService");
+ Assert.True (service.IsUnconditional, "MyService with [Service] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_BroadcastReceiverType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var receiver = FindByJavaName (peers, "my/app/MyReceiver");
+ Assert.True (receiver.IsUnconditional, "MyReceiver with [BroadcastReceiver] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_ContentProviderType_IsUnconditional ()
+ {
+ var peers = ScanFixtures ();
+ var provider = FindByJavaName (peers, "my/app/MyProvider");
+ Assert.True (provider.IsUnconditional, "MyProvider with [ContentProvider] should be unconditional");
+ }
+
+ [Fact]
+ public void Scan_TypeWithoutComponentAttribute_IsTrimmable ()
+ {
+ var peers = ScanFixtures ();
+ var helper = FindByJavaName (peers, "my/app/MyHelper");
+ Assert.False (helper.IsUnconditional, "MyHelper without component attr should be trimmable");
+ }
+
+ [Fact]
+ public void Scan_McwBinding_IsTrimmable ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.False (activity.IsUnconditional, "MCW Activity should be trimmable (no component attr on MCW type)");
+ }
+
+ [Fact]
+ public void Scan_InterfaceType_IsMarkedAsInterface ()
+ {
+ var peers = ScanFixtures ();
+ var listener = FindByManagedName (peers, "Android.Views.IOnClickListener");
+ Assert.True (listener.IsInterface, "IOnClickListener should be marked as interface");
+ }
+
+ [Fact]
+ public void Scan_InvokerTypes_AreIncluded ()
+ {
+ var peers = ScanFixtures ();
+ var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker");
+ Assert.NotNull (invoker);
+ Assert.True (invoker.DoNotGenerateAcw, "Invoker should have DoNotGenerateAcw=true");
+ Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName);
+ }
+
+ [Fact]
+ public void Scan_GenericType_IsGenericDefinition ()
+ {
+ var peers = ScanFixtures ();
+ var generic = FindByJavaName (peers, "my/app/GenericHolder");
+ Assert.True (generic.IsGenericDefinition, "GenericHolder should be marked as generic definition");
+ }
+
+ [Fact]
+ public void Scan_AbstractType_IsMarkedAbstract ()
+ {
+ var peers = ScanFixtures ();
+ var abstractBase = FindByJavaName (peers, "my/app/AbstractBase");
+ Assert.True (abstractBase.IsAbstract, "AbstractBase should be marked as abstract");
+ }
+
+ [Fact]
+ public void Scan_MarshalMethods_Collected ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.NotEmpty (activity.MarshalMethods);
+ }
+
+ [Fact]
+ public void Scan_UserTypeOverride_CollectsMarshalMethods ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.NotEmpty (mainActivity.MarshalMethods);
+
+ var onCreate = mainActivity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate");
+ Assert.NotNull (onCreate);
+ Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_TypeWithOwnActivationCtor_ResolvesToSelf ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.NotNull (activity.ActivationCtor);
+ Assert.Equal ("Android.App.Activity", activity.ActivationCtor.DeclaringTypeName);
+ Assert.Equal (ActivationCtorStyle.XamarinAndroid, activity.ActivationCtor.Style);
+ }
+
+ [Fact]
+ public void Scan_TypeWithoutOwnActivationCtor_InheritsFromBase ()
+ {
+ var peers = ScanFixtures ();
+ var simpleActivity = FindByJavaName (peers, "my/app/SimpleActivity");
+ Assert.NotNull (simpleActivity.ActivationCtor);
+ Assert.Equal ("Android.App.Activity", simpleActivity.ActivationCtor.DeclaringTypeName);
+ Assert.Equal (ActivationCtorStyle.XamarinAndroid, simpleActivity.ActivationCtor.Style);
+ }
+
+ [Fact]
+ public void Scan_TypeWithOwnActivationCtor_DoesNotLookAtBase ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.NotNull (mainActivity.ActivationCtor);
+ Assert.Equal ("Android.App.Activity", mainActivity.ActivationCtor.DeclaringTypeName);
+ }
+
+ [Fact]
+ public void Scan_AllTypes_HaveAssemblyName ()
+ {
+ var peers = ScanFixtures ();
+ Assert.All (peers, peer =>
+ Assert.False (string.IsNullOrEmpty (peer.AssemblyName),
+ $"Type {peer.ManagedTypeName} should have assembly name"));
+ }
+
+ [Fact]
+ public void Scan_InvokerSharesJavaNameWithInterface ()
+ {
+ var peers = ScanFixtures ();
+ var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList ();
+ // Interface + Invoker share the same JNI name (this is expected — they're aliases)
+ Assert.Equal (2, clickListenerPeers.Count);
+ Assert.Contains (clickListenerPeers, p => p.IsInterface);
+ Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw);
+ }
+
+ [Fact]
+ public void Scan_ActivityBaseJavaName_IsJavaLangObject ()
+ {
+ var peers = ScanFixtures ();
+ var activity = FindByJavaName (peers, "android/app/Activity");
+ Assert.Equal ("java/lang/Object", activity.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_MainActivityBaseJavaName_IsActivity ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ Assert.Equal ("android/app/Activity", mainActivity.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_JavaLangObjectBaseJavaName_IsNull ()
+ {
+ var peers = ScanFixtures ();
+ var jlo = FindByJavaName (peers, "java/lang/Object");
+ Assert.Null (jlo.BaseJavaName);
+ }
+
+ [Fact]
+ public void Scan_TypeImplementingInterface_HasInterfaceJavaNames ()
+ {
+ var peers = ScanFixtures ();
+ var clickable = FindByJavaName (peers, "my/app/ClickableView");
+ Assert.Contains ("android/view/View$OnClickListener", clickable.ImplementedInterfaceJavaNames);
+ }
+
+ [Fact]
+ public void Scan_TypeNotImplementingInterface_HasEmptyList ()
+ {
+ var peers = ScanFixtures ();
+ var helper = FindByJavaName (peers, "my/app/MyHelper");
+ Assert.Empty (helper.ImplementedInterfaceJavaNames);
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
new file mode 100644
index 00000000000..fcd4519d277
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
@@ -0,0 +1,143 @@
+// Minimal stub attributes mirroring the real Mono.Android attributes.
+// These exist solely so the test fixture assembly can have types
+// with the same attribute shapes the scanner expects.
+
+using System;
+
+namespace Java.Interop
+{
+ public interface IJniNameProviderAttribute
+ {
+ string Name { get; }
+ }
+}
+
+namespace Android.Runtime
+{
+ [AttributeUsage (
+ AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field |
+ AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property,
+ AllowMultiple = false)]
+ public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string Name { get; }
+ public string? Signature { get; set; }
+ public string? Connector { get; set; }
+ public bool DoNotGenerateAcw { get; set; }
+ public int ApiSince { get; set; }
+
+ public RegisterAttribute (string name)
+ {
+ Name = name;
+ }
+
+ public RegisterAttribute (string name, string signature, string connector)
+ {
+ Name = name;
+ Signature = signature;
+ Connector = connector;
+ }
+ }
+
+ public enum JniHandleOwnership
+ {
+ DoNotTransfer = 0,
+ TransferLocalRef = 1,
+ TransferGlobalRef = 2,
+ }
+}
+
+namespace Android.App
+{
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class ActivityAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public bool MainLauncher { get; set; }
+ public string? Label { get; set; }
+ public string? Icon { get; set; }
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+ }
+
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class ServiceAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+ }
+
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class InstrumentationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+ }
+
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class ApplicationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public Type? BackupAgent { get; set; }
+ public Type? ManageSpaceActivity { get; set; }
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+ }
+}
+
+namespace Android.Content
+{
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class BroadcastReceiverAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+ }
+
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string []? Authorities { get; set; }
+ public string? Name { get; set; }
+ string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
+
+ public ContentProviderAttribute (string [] authorities)
+ {
+ Authorities = authorities;
+ }
+ }
+}
+
+namespace Java.Interop
+{
+ [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class ExportAttribute : Attribute
+ {
+ public string? Name { get; set; }
+
+ public ExportAttribute ()
+ {
+ }
+
+ public ExportAttribute (string name)
+ {
+ Name = name;
+ }
+ }
+}
+
+namespace MyApp
+{
+ ///
+ /// Custom attribute implementing IJniNameProviderAttribute — the scanner
+ /// should detect this dynamically via interface resolution, not hardcoded names.
+ ///
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class CustomJniNameAttribute : Attribute, Java.Interop.IJniNameProviderAttribute
+ {
+ public string Name { get; }
+
+ public CustomJniNameAttribute (string name)
+ {
+ Name = name;
+ }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj
new file mode 100644
index 00000000000..f7f4c72139b
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(DotNetStableTargetFramework)
+ latest
+ enable
+ Microsoft.Android.Sdk.TrimmableTypeMap.Tests.TestFixtures
+
+ false
+ true
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
new file mode 100644
index 00000000000..e2b909e2317
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -0,0 +1,628 @@
+// Test fixture types that exercise all scanner code paths.
+// Each type is annotated with comments explaining which classification
+// and behavior the scanner should produce.
+
+using System;
+using Android.App;
+using Android.Content;
+using Android.Runtime;
+
+namespace Java.Lang
+{
+ [Register ("java/lang/Object", DoNotGenerateAcw = true)]
+ public class Object
+ {
+ public Object ()
+ {
+ }
+
+ protected Object (IntPtr handle, JniHandleOwnership transfer)
+ {
+ }
+ }
+}
+
+namespace Java.Lang
+{
+ [Register ("java/lang/Throwable", DoNotGenerateAcw = true)]
+ public class Throwable : Java.Lang.Object
+ {
+ protected Throwable (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")]
+ public virtual string? Message { get; }
+ }
+
+ [Register ("java/lang/Exception", DoNotGenerateAcw = true)]
+ public class Exception : Throwable
+ {
+ protected Exception (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.App
+{
+ [Register ("android/app/Activity", DoNotGenerateAcw = true)]
+ public class Activity : Java.Lang.Object
+ {
+ public Activity ()
+ {
+ }
+
+ protected Activity (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
+ protected virtual void OnCreate (/* Bundle? */ object? savedInstanceState)
+ {
+ }
+
+ [Register ("onStart", "()V", "")]
+ protected virtual void OnStart ()
+ {
+ }
+ }
+
+ [Register ("android/app/Service", DoNotGenerateAcw = true)]
+ public class Service : Java.Lang.Object
+ {
+ protected Service (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.Content
+{
+ [Register ("android/content/Context", DoNotGenerateAcw = true)]
+ public class Context : Java.Lang.Object
+ {
+ protected Context (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.Views
+{
+ [Register ("android/view/View", DoNotGenerateAcw = true)]
+ public class View : Java.Lang.Object
+ {
+ protected View (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.Widget
+{
+ [Register ("android/widget/Button", DoNotGenerateAcw = true)]
+ public class Button : Android.Views.View
+ {
+ protected Button (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+
+ [Register ("android/widget/TextView", DoNotGenerateAcw = true)]
+ public class TextView : Android.Views.View
+ {
+ protected TextView (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.Views
+{
+ [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")]
+ public interface IOnClickListener
+ {
+ [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")]
+ void OnClick (View v);
+ }
+
+ // Invoker types ARE internal implementation details.
+ // In real Mono.Android.dll, invokers DO have [Register] with DoNotGenerateAcw=true
+ // and the SAME JNI name as their interface.
+ // The scanner includes them — generators filter them later.
+ [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)]
+ internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener
+ {
+ public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ public void OnClick (View v)
+ {
+ }
+ }
+}
+
+namespace MyApp
+{
+ // User types get their JNI name from [Activity(Name = "...")]
+ // NOT from [Register] — that's only on MCW binding types.
+ [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")]
+ public class MainActivity : Android.App.Activity
+ {
+ public MainActivity ()
+ {
+ }
+
+ [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
+ protected override void OnCreate (object? savedInstanceState)
+ {
+ base.OnCreate (savedInstanceState);
+ }
+ }
+
+ // User type without component attribute: TRIMMABLE
+ [Register ("my/app/MyHelper")]
+ public class MyHelper : Java.Lang.Object
+ {
+ [Register ("doSomething", "()V", "GetDoSomethingHandler")]
+ public virtual void DoSomething ()
+ {
+ }
+ }
+
+ // User service: UNCONDITIONAL — gets JNI name from [Service(Name = "...")]
+ [Service (Name = "my.app.MyService")]
+ public class MyService : Android.App.Service
+ {
+ protected MyService (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+
+ // User broadcast receiver: UNCONDITIONAL — gets JNI name from [BroadcastReceiver(Name = "...")]
+ [BroadcastReceiver (Name = "my.app.MyReceiver")]
+ public class MyReceiver : Java.Lang.Object
+ {
+ }
+
+ // User content provider: UNCONDITIONAL — gets JNI name from [ContentProvider(Name = "...")]
+ [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider")]
+ public class MyProvider : Java.Lang.Object
+ {
+ }
+}
+
+namespace MyApp.Generic
+{
+ [Register ("my/app/GenericHolder")]
+ public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object
+ {
+ [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")]
+ public virtual T? GetItem ()
+ {
+ return default;
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/AbstractBase")]
+ public abstract class AbstractBase : Java.Lang.Object
+ {
+ protected AbstractBase (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("doWork", "()V", "")]
+ public abstract void DoWork ();
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/SimpleActivity")]
+ public class SimpleActivity : Android.App.Activity
+ {
+ // No (IntPtr, JniHandleOwnership) ctor — scanner should
+ // resolve to Activity's activation ctor
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/ClickableView")]
+ public class ClickableView : Android.Views.View, Android.Views.IOnClickListener
+ {
+ protected ClickableView (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("onClick", "(Landroid/view/View;)V", "")]
+ public void OnClick (Android.Views.View v)
+ {
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/CustomView")]
+ public class CustomView : Android.Views.View
+ {
+ protected CustomView (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("", "()V", "")]
+ public CustomView ()
+ : base (default!, default)
+ {
+ }
+
+ [Register ("", "(Landroid/content/Context;)V", "")]
+ public CustomView (Context context)
+ : base (default!, default)
+ {
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/Outer")]
+ public class Outer : Java.Lang.Object
+ {
+ [Register ("my/app/Outer$Inner")]
+ public class Inner : Java.Lang.Object
+ {
+ protected Inner (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")]
+ public interface ICallback
+ {
+ [Register ("my/app/ICallback$Result")]
+ public class Result : Java.Lang.Object
+ {
+ protected Result (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/TouchHandler")]
+ public class TouchHandler : Java.Lang.Object
+ {
+ // bool return type (non-blittable, needs byte wrapper in UCO)
+ [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")]
+ public virtual bool OnTouch (Android.Views.View v, int action)
+ {
+ return false;
+ }
+
+ // bool parameter (non-blittable)
+ [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")]
+ public virtual void OnFocusChange (Android.Views.View v, bool hasFocus)
+ {
+ }
+
+ // Multiple params of different JNI types
+ [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")]
+ public virtual void OnScroll (int x, float y, long timestamp, double velocity)
+ {
+ }
+
+ // Object return type
+ [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")]
+ public virtual string? GetText ()
+ {
+ return null;
+ }
+
+ // Array parameter
+ [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")]
+ public virtual void SetItems (string[]? items)
+ {
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/ExportExample")]
+ public class ExportExample : Java.Lang.Object
+ {
+ [Java.Interop.Export ("myExportedMethod")]
+ public void MyExportedMethod ()
+ {
+ }
+ }
+}
+
+namespace Android.App.Backup
+{
+ [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)]
+ public class BackupAgent : Java.Lang.Object
+ {
+ protected BackupAgent (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace MyApp
+{
+ [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))]
+ public class MyApplication : Java.Lang.Object
+ {
+ }
+
+ [Instrumentation (Name = "my.app.MyInstrumentation")]
+ public class MyInstrumentation : Java.Lang.Object
+ {
+ }
+
+ // BackupAgent without a component attribute — would normally be trimmable,
+ // but [Application(BackupAgent = typeof(...))] should force it unconditional.
+ [Register ("my/app/MyBackupAgent")]
+ public class MyBackupAgent : Android.App.Backup.BackupAgent
+ {
+ protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+
+ // Activity without [Activity] attribute — would normally be trimmable,
+ // but [Application(ManageSpaceActivity = typeof(...))] should force it unconditional.
+ [Register ("my/app/MyManageSpaceActivity")]
+ public class MyManageSpaceActivity : Android.App.Activity
+ {
+ protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+
+ // User type WITHOUT [Register] — gets CRC64-computed JNI name.
+ // CompatJniName should use raw namespace instead of CRC64.
+ public class UnregisteredHelper : Java.Lang.Object
+ {
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/MyButton")]
+ public class MyButton : Android.Widget.Button
+ {
+ protected MyButton (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+namespace Android.Views
+{
+ [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")]
+ public interface IOnLongClickListener
+ {
+ [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")]
+ bool OnLongClick (View v);
+ }
+}
+
+namespace MyApp
+{
+ [Register ("my/app/MultiInterfaceView")]
+ public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener
+ {
+ protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+
+ [Register ("onClick", "(Landroid/view/View;)V", "")]
+ public void OnClick (Android.Views.View v) { }
+
+ [Register ("onLongClick", "(Landroid/view/View;)Z", "")]
+ public bool OnLongClick (Android.Views.View v) { return false; }
+ }
+
+ // User type with a custom IJniNameProviderAttribute — the scanner
+ // should detect this via interface resolution, not hardcoded attribute names.
+ [CustomJniName ("com.example.CustomWidget")]
+ public class CustomWidget : Java.Lang.Object
+ {
+ }
+}
+
+// ================================================================
+// Edge case: generic base type (TypeSpecification resolution)
+// ================================================================
+namespace MyApp.Generic
+{
+ [Register ("my/app/GenericBase", DoNotGenerateAcw = true)]
+ public class GenericBase : Java.Lang.Object where T : class
+ {
+ protected GenericBase (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+
+ [Register ("my/app/ConcreteFromGeneric")]
+ public class ConcreteFromGeneric : GenericBase
+ {
+ protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+// ================================================================
+// Edge case: generic interface (TypeSpecification resolution)
+// ================================================================
+namespace MyApp.Generic
+{
+ [Register ("my/app/IGenericCallback", "", "")]
+ public interface IGenericCallback
+ {
+ }
+
+ [Register ("my/app/GenericCallbackImpl")]
+ public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback
+ {
+ protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+// ================================================================
+// Edge case: component-only base detection
+// ================================================================
+namespace MyApp
+{
+ [Activity (Name = "my.app.BaseActivityNoRegister")]
+ public class BaseActivityNoRegister : Android.App.Activity
+ {
+ }
+
+ public class DerivedFromComponentBase : BaseActivityNoRegister
+ {
+ }
+}
+
+// ================================================================
+// Edge case: unregistered nested type inside [Register] parent
+// ================================================================
+namespace MyApp
+{
+ [Register ("my/app/RegisteredParent")]
+ public class RegisteredParent : Java.Lang.Object
+ {
+ public class UnregisteredChild : Java.Lang.Object
+ {
+ }
+ }
+}
+
+// ================================================================
+// Edge case: 3-level deep nesting
+// ComputeTypeNameParts must walk multiple levels, collecting names.
+// ================================================================
+namespace MyApp
+{
+ [Register ("my/app/DeepOuter")]
+ public class DeepOuter : Java.Lang.Object
+ {
+ public class Middle : Java.Lang.Object
+ {
+ public class DeepInner : Java.Lang.Object
+ {
+ }
+ }
+ }
+}
+
+// ================================================================
+// Edge case: plain Java peer subclass — no [Register], no component attribute
+// ExtendsJavaPeer must detect it via base type chain, gets CRC64 name.
+// ================================================================
+namespace MyApp
+{
+ public class PlainActivitySubclass : Android.App.Activity
+ {
+ }
+}
+
+// ================================================================
+// Edge case: component attribute WITHOUT Name property
+// HasComponentAttribute = true but ComponentAttributeJniName = null.
+// Type should still get a CRC64 JNI name (not null).
+// ================================================================
+namespace MyApp
+{
+ [Activity (Label = "Unnamed")]
+ public class UnnamedActivity : Android.App.Activity
+ {
+ }
+}
+
+// ================================================================
+// Edge case: interface implementation on unregistered type
+// Type gets CRC64 JNI name but still resolves interface names.
+// ================================================================
+namespace MyApp
+{
+ public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener
+ {
+ [Register ("onClick", "(Landroid/view/View;)V", "")]
+ public void OnClick (Android.Views.View v)
+ {
+ }
+ }
+}
+
+// ================================================================
+// Edge case: [Export] method on unregistered type
+// ParseExportAttribute runs on a type that gets CRC64 JNI name.
+// ================================================================
+namespace MyApp
+{
+ public class UnregisteredExporter : Java.Lang.Object
+ {
+ [Java.Interop.Export ("doExportedWork")]
+ public void DoExportedWork ()
+ {
+ }
+ }
+}
+
+// ================================================================
+// Edge case: type in empty namespace
+// ================================================================
+[Register ("my/app/GlobalType")]
+public class GlobalType : Java.Lang.Object
+{
+ protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+}
+
+public class GlobalUnregisteredType : Java.Lang.Object
+{
+}