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..87885ecdce3
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -0,0 +1,714 @@
+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 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) {
+ if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
+ ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent);
+ ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity);
+ }
+ }
+ }
+ }
+
+ static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName)
+ {
+ if (managedTypeName is 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)) {
+ resultsByManagedName [managedTypeName] = peer with { 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 is not null && !registerInfo.JniName.IsNullOrEmpty ()) {
+ jniName = registerInfo.JniName;
+ compatJniName = jniName;
+ doNotGenerateAcw = registerInfo.DoNotGenerateAcw;
+ } else if (attrInfo?.JniName is not null) {
+ // User type with [Activity(Name = "...")] but no [Register]
+ jniName = attrInfo.JniName;
+ compatJniName = jniName;
+ } else {
+ // No explicit JNI name — check if this type extends a known Java peer.
+ // If so, auto-compute JNI name from the managed type name via CRC64.
+ if (ExtendsJavaPeer (typeDef, index)) {
+ (jniName, 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 is not null;
+ 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);
+ if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo)) {
+ continue;
+ }
+
+ AddMarshalMethod (methods, registerInfo!, methodDef, index, exportInfo);
+ }
+
+ // Collect [Register] from properties (attribute is on the property, not the getter)
+ foreach (var propHandle in typeDef.GetProperties ()) {
+ var propDef = index.Reader.GetPropertyDefinition (propHandle);
+ var propRegister = TryGetPropertyRegisterInfo (propDef, index);
+ if (propRegister is null) {
+ continue;
+ }
+
+ var accessors = propDef.GetAccessors ();
+ if (!accessors.Getter.IsNil) {
+ var getterDef = index.Reader.GetMethodDefinition (accessors.Getter);
+ AddMarshalMethod (methods, propRegister, getterDef, index);
+ }
+ }
+
+ return methods;
+ }
+
+ static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null)
+ {
+ // Skip methods that are just the JNI name (type-level [Register])
+ if (registerInfo.Signature is null && registerInfo.Connector is null) {
+ return;
+ }
+
+ 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 = exportInfo?.ThrownNames,
+ SuperArgumentsString = exportInfo?.SuperArgumentsString,
+ });
+ }
+
+ string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
+ {
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo is null) {
+ return null;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ // First try [Register] attribute
+ var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
+ if (registerJniName is not 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 is not null) {
+ result.Add (ifaceJniName);
+ }
+ }
+
+ return result;
+ }
+
+ string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
+ {
+ var resolved = ResolveEntityHandle (interfaceHandle, index);
+ return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
+ }
+
+ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
+ {
+ exportInfo = null;
+ foreach (var caHandle in methodDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ return true;
+ }
+
+ if (attrName == "ExportAttribute") {
+ (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index);
+ return true;
+ }
+ }
+ registerInfo = null;
+ return false;
+ }
+
+ static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index)
+ {
+ foreach (var caHandle in propDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ }
+ }
+ return null;
+ }
+
+ static (RegisterInfo registerInfo, ExportInfo exportInfo) 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.IsNullOrEmpty ()) {
+ 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 { JniName = exportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false },
+ new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments }
+ );
+ }
+
+ static string BuildJniSignatureFromManaged (MethodSignature sig)
+ {
+ var sb = new System.Text.StringBuilder ();
+ sb.Append ('(');
+ foreach (var param in sig.ParameterTypes) {
+ sb.Append (ManagedTypeToJniDescriptor (param));
+ }
+ sb.Append (')');
+ sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType));
+ return sb.ToString ();
+ }
+
+ static string ManagedTypeToJniDescriptor (string managedType)
+ {
+ switch (managedType) {
+ case "System.Void": return "V";
+ case "System.Boolean": return "Z";
+ case "System.Byte":
+ case "System.SByte": return "B";
+ case "System.Char": return "C";
+ case "System.Int16":
+ case "System.UInt16": return "S";
+ case "System.Int32":
+ case "System.UInt32": return "I";
+ case "System.Int64":
+ case "System.UInt64": return "J";
+ case "System.Single": return "F";
+ case "System.Double": return "D";
+ case "System.String": return "Ljava/lang/String;";
+ default:
+ if (managedType.EndsWith ("[]")) {
+ return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}";
+ }
+ return "Ljava/lang/Object;";
+ }
+ }
+
+ ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index)
+ {
+ if (activationCtorCache.TryGetValue (typeName, out var cached)) {
+ return cached;
+ }
+
+ // Check this type's constructors
+ var ownCtor = FindActivationCtorOnType (typeDef, index);
+ if (ownCtor is not 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 is not 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 is not 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 is not 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 is 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.ContainsKey (baseHandle)) {
+ 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 is not 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) && !parentRegister.JniName.IsNullOrEmpty ()) {
+ parentJniName = parentRegister.JniName;
+ break;
+ }
+ if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) {
+ parentJniName = parentAttr.JniName;
+ 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 ()}";
+ }
+}