diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
index 9c0867c0875..93f8ce5c5fc 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
@@ -41,6 +41,7 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data)
writer.Write (proxy.TargetType.ManagedTypeName);
writer.Write (proxy.TargetType.AssemblyName);
writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0));
+ writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0));
}
foreach (var assoc in data.Associations) {
writer.Write (assoc.SourceTypeReference);
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
index 679423576f2..5f945a8ac05 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -110,6 +110,11 @@ sealed class JavaPeerProxyData
///
public TypeRefData? InvokerType { get; set; }
+ ///
+ /// Activation constructor style to use when creating .
+ ///
+ public ActivationCtorStyle? InvokerActivationCtorStyle { get; set; }
+
///
/// Whether this proxy has a CreateInstance that can actually create instances.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
index 79570a14bda..7439f68173a 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -277,6 +277,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash
ManagedTypeName = peer.InvokerTypeName,
AssemblyName = peer.AssemblyName,
};
+ proxy.InvokerActivationCtorStyle = peer.InvokerActivationCtorStyle ?? ActivationCtorStyle.XamarinAndroid;
}
if (peer.ActivationCtor != null) {
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
index 417b1096f60..d489edcdc07 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -604,25 +604,28 @@ void EmitCreateInstance (JavaPeerProxyData proxy)
return;
}
- // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions)
- // require parameter conversion from (IntPtr, JniHandleOwnership).
- if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) {
- if (proxy.InvokerType != null) {
- EmitCreateInstanceViaJavaInteropNewobj (_pe.ResolveTypeRef (proxy.InvokerType));
+ if (proxy.InvokerType != null) {
+ var invokerType = _pe.ResolveTypeRef (proxy.InvokerType);
+ if (proxy.InvokerActivationCtorStyle == ActivationCtorStyle.JavaInterop) {
+ EmitCreateInstanceViaJavaInteropNewobj (invokerType);
} else {
- var targetRef = _pe.ResolveTypeRef (proxy.TargetType);
- var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null");
- if (jiCtor.IsOnLeafType) {
- EmitCreateInstanceViaJavaInteropNewobj (targetRef);
- } else {
- EmitCreateInstanceInheritedJavaInteropCtor (targetRef, jiCtor);
- }
+ EmitCreateInstanceViaNewobj (invokerType);
}
return;
}
- if (proxy.InvokerType != null) {
- EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType));
+ // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions)
+ // require parameter conversion from (IntPtr, JniHandleOwnership).
+ if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) {
+ var targetRef = _pe.ResolveTypeRef (proxy.TargetType);
+ var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null");
+ if (jiCtor.IsOnLeafType) {
+ EmitCreateInstanceViaJavaInteropNewobj (targetRef);
+ } else {
+ // Legacy GetConstructor() doesn't find inherited ctors —
+ // match that behavior by returning null.
+ EmitCreateInstanceNoActivation ();
+ }
return;
}
@@ -633,7 +636,9 @@ void EmitCreateInstance (JavaPeerProxyData proxy)
if (activationCtor.IsOnLeafType) {
EmitCreateInstanceViaNewobj (targetTypeRef);
} else {
- EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor);
+ // Legacy GetConstructor() doesn't find inherited ctors —
+ // match that behavior by returning null.
+ EmitCreateInstanceNoActivation ();
}
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
index 1db8dfd8309..9bf542f6b7b 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -235,10 +235,13 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca)
doNotGenerateAcw = !generateJavaPeer;
}
+ var isArrayType = TryGetNamedArgument (value, "ArrayRank", out var rank) && rank > 0;
+
return new RegisterInfo {
JniName = jniName.Replace ('.', '/'),
DoNotGenerateAcw = doNotGenerateAcw,
IsFromJniTypeSignature = true,
+ IsArrayType = isArrayType,
};
}
@@ -529,6 +532,7 @@ sealed record RegisterInfo
public string? Connector { get; init; }
public bool DoNotGenerateAcw { get; init; }
public bool IsFromJniTypeSignature { get; init; }
+ public bool IsArrayType { get; init; }
}
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index bcc45d1b1c5..ee285b42798 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -125,6 +125,13 @@ public sealed record JavaPeerInfo
///
public string? InvokerTypeName { get; init; }
+ ///
+ /// Activation constructor style declared by .
+ /// Kept separate from , which describes the
+ /// target type or its base types.
+ ///
+ public ActivationCtorStyle? InvokerActivationCtorStyle { get; init; }
+
///
/// True if this is an open generic type definition.
/// Generic types get TypeMap entries but CreateInstance throws NotSupportedException.
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 2e09766a2fa..8b32abf3940 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -182,6 +182,15 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
index.AttributesByType.TryGetValue (typeHandle, out var attrInfo);
if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) {
+ // [JniTypeSignature] with ArrayRank > 0 represents a JNI array wrapper
+ // (e.g., JavaBooleanArray, JavaObjectArray, JavaPrimitiveArray).
+ // These are handled by the built-in tables in JniRuntime.JniTypeManager
+ // and must not be added to the typemap — keyword types (Z, B, etc.)
+ // would collide with GetPrimitiveArrayTypesForSimpleReference, and
+ // non-keyword array types would add unnecessary aliases.
+ if (registerInfo.IsArrayType) {
+ continue;
+ }
jniName = registerInfo.JniName;
compatJniName = jniName;
doNotGenerateAcw = registerInfo.DoNotGenerateAcw;
@@ -208,6 +217,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
var isUnconditional = attrInfo is not null;
var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo;
string? invokerTypeName = null;
+ ActivationCtorStyle? invokerActivationCtorStyle = null;
// Resolve base Java type name
var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
@@ -229,6 +239,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index);
}
+ // Interface/abstract peers create their invoker, not the target type.
+ // Keep ActivationCtor scoped to the target/base hierarchy for legacy parity,
+ // and store the invoker ctor style separately for CreateInstance emission.
+ if (invokerTypeName is not null) {
+ invokerActivationCtorStyle = TryResolveActivationCtorOnInvoker (invokerTypeName)?.Style;
+ }
+
var peer = new JavaPeerInfo {
JavaName = jniName,
CompatJniName = compatJniName,
@@ -249,6 +266,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
JavaFields = exportFields,
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
+ InvokerActivationCtorStyle = invokerActivationCtorStyle,
IsGenericDefinition = isGenericDefinition,
ComponentAttribute = ToComponentInfo (attrInfo),
};
@@ -1280,6 +1298,24 @@ string ManagedTypeToJniDescriptor (string managedType)
return null;
}
+ ///
+ /// Resolve the activation ctor on a known invoker type (search all loaded assemblies).
+ /// Used for interface peers, whose own type definition has no constructors.
+ /// The assemblyCache typically contains 10–30 entries (app + framework assemblies),
+ /// and each lookup is an O(1) dictionary probe, so the linear scan is cheap.
+ ///
+ ActivationCtorInfo? TryResolveActivationCtorOnInvoker (string invokerTypeName)
+ {
+ foreach (var assembly in assemblyCache.Values) {
+ if (!assembly.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) {
+ continue;
+ }
+ var invokerDef = assembly.Reader.GetTypeDefinition (invokerHandle);
+ return ResolveActivationCtor (invokerTypeName, invokerDef, assembly);
+ }
+ return null;
+ }
+
public void Dispose ()
{
foreach (var index in assemblyCache.Values) {
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
index 2250cb24d67..b4791cfdde0 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs
@@ -514,9 +514,19 @@ void ProcessContext (HandleContext* context)
if (RuntimeFeature.TrimmableTypeMap) {
try {
+ // Map universal request types to concrete peers before proxy lookup.
+ var resolvedTargetType = ResolvePeerType (targetType);
+
var typeMap = TrimmableTypeMap.Instance;
- var proxy = typeMap.GetProxyForJavaObject (reference.Handle, targetType);
- var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
+ var proxy = typeMap.GetProxyForJavaObject (reference.Handle, resolvedTargetType);
+
+ // Open-generic proxies cannot instantiate closed targets.
+ IJavaPeerable? peer;
+ if (ShouldActivateClosedGenericTarget (proxy, resolvedTargetType)) {
+ peer = ActivateUsingReflection (resolvedTargetType, reference.Handle, JniHandleOwnership.DoNotTransfer);
+ } else {
+ peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
+ }
if (peer is not null) {
var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable;
if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) {
@@ -526,7 +536,13 @@ void ProcessContext (HandleContext* context)
return peer;
}
- var targetName = targetType?.AssemblyQualifiedName ?? "";
+ // Preserve CreatePeer's bad-cast vs missing-mapping behavior.
+ if (resolvedTargetType is not null &&
+ IsIncompatibleCast (typeMap, ref reference, resolvedTargetType)) {
+ return null;
+ }
+
+ var targetName = resolvedTargetType?.AssemblyQualifiedName ?? "";
var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference);
throw new NotSupportedException (
@@ -541,6 +557,85 @@ void ProcessContext (HandleContext* context)
return base.CreatePeer (ref reference, transfer, targetType);
}
+ [return: DynamicallyAccessedMembers (Constructors)]
+ static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type)
+ {
+ if (type is null) {
+ return null;
+ }
+ if (type == typeof (object) || type == typeof (IJavaPeerable)) {
+ return typeof (global::Java.Interop.JavaObject);
+ }
+ if (type == typeof (Exception)) {
+ return typeof (JavaException);
+ }
+ return type;
+ }
+
+ static bool ShouldActivateClosedGenericTarget (
+ [NotNullWhen (true)] JavaPeerProxy? proxy,
+ [NotNullWhen (true)] Type? resolvedTargetType)
+ {
+ return proxy is not null &&
+ proxy.TargetType.IsGenericTypeDefinition &&
+ resolvedTargetType is not null &&
+ resolvedTargetType.IsGenericType &&
+ !resolvedTargetType.IsGenericTypeDefinition;
+ }
+
+ static IJavaPeerable? ActivateUsingReflection (
+ [DynamicallyAccessedMembers (Constructors)]
+ Type closedType,
+ IntPtr handle,
+ JniHandleOwnership transfer)
+ {
+ var ctor = closedType.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null);
+ if (ctor is null) {
+ return null;
+ }
+
+ return (IJavaPeerable) ctor.Invoke ([handle, transfer]);
+ }
+
+ ///
+ /// Returns true when 's Java class is not assignable from
+ /// . Throws when has no usable mapping.
+ ///
+ static bool IsIncompatibleCast (
+ TrimmableTypeMap typeMap,
+ ref JniObjectReference reference,
+ Type targetType)
+ {
+ if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) {
+ throw new ArgumentException (
+ $"Could not determine Java type corresponding to '{targetType.AssemblyQualifiedName}'.",
+ nameof (targetType));
+ }
+
+ var instanceClass = JniEnvironment.Types.GetObjectClass (reference);
+ JniObjectReference targetClass = default;
+ try {
+ try {
+ targetClass = JniEnvironment.Types.FindClass (targetJniName);
+ } catch (Java.Lang.ClassNotFoundException e) {
+ throw new ArgumentException (
+ $"Could not find Java class '{targetJniName}'.",
+ nameof (targetType), e);
+ }
+
+ if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) {
+ // Bad cast: callers translate null to the expected result.
+ return true;
+ }
+ } finally {
+ JniObjectReference.Dispose (ref instanceClass);
+ JniObjectReference.Dispose (ref targetClass);
+ }
+
+ // Compatible classes mean a proxy/activation gap.
+ return false;
+ }
+
protected override bool TryConstructPeer (
IJavaPeerable self,
ref JniObjectReference reference,
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
index eefb7cc2ac5..4f7dbb27c69 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
@@ -238,7 +238,16 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
var targetClass = default (JniObjectReference);
try {
objClass = JniEnvironment.Types.GetObjectClass (selfRef);
- targetClass = JniEnvironment.Types.FindClass (targetJniName);
+ try {
+ targetClass = JniEnvironment.Types.FindClass (targetJniName);
+ } catch (Java.Lang.ClassNotFoundException) {
+ // FindClass throws for managed types whose Java peer class is
+ // not present in the APK (e.g. test types annotated with
+ // [JniTypeSignature("__missing__")]). Treat as "no match" so
+ // JavaMarshalValueManager.CreatePeer can surface the correct
+ // ArgumentException instead of leaking ClassNotFoundException.
+ return null;
+ }
var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass);
return isAssignable ? proxy : null;
} finally {
@@ -273,22 +282,26 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
///
internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType)
{
- if (targetType.IsAssignableFrom (proxyTargetType)) {
+ if (targetType == proxyTargetType) {
return true;
}
- if (!proxyTargetType.IsGenericTypeDefinition) {
- return false;
- }
-
- for (Type? t = targetType; t is not null; t = t.BaseType) {
- if (t.IsGenericType && !t.IsGenericTypeDefinition &&
- t.GetGenericTypeDefinition () == proxyTargetType) {
- return true;
+ // Open generic proxy: match only when targetType is a closed instantiation
+ // of this generic (e.g. JavaList matches the JavaList<> proxy).
+ // IsAssignableFrom alone would incorrectly match unrelated open generics
+ // that are technically subclasses (e.g. JavaArray<> is assignable to
+ // JavaObject), and proxy.CreateInstance for an open generic always throws.
+ if (proxyTargetType.IsGenericTypeDefinition) {
+ for (Type? t = targetType; t is not null; t = t.BaseType) {
+ if (t.IsGenericType && !t.IsGenericTypeDefinition &&
+ t.GetGenericTypeDefinition () == proxyTargetType) {
+ return true;
+ }
}
+ return false;
}
- return false;
+ return targetType.IsAssignableFrom (proxyTargetType);
}
///
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index 6994ce45ade..404d997345d 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -471,6 +471,36 @@ sig.ParameterTypes [0].Contains ("JniObjectReference")) {
Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter");
}
+ [Fact]
+ public void Generate_JiStyleInvoker_FirstParamIsByRef ()
+ {
+ var peer = MakeInterfacePeer ("test/IJiInvoker", "Test.IJiInvoker", "TestAsm", "Test.IJiInvokerInvoker") with {
+ InvokerActivationCtorStyle = ActivationCtorStyle.JavaInterop,
+ };
+
+ using var stream = GenerateAssembly (new [] { peer }, "JiInvokerByRefTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+
+ bool foundByRefCtor = false;
+ foreach (var ctor in ctorRefs) {
+ var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ if (sig.ParameterTypes.Length == 2 &&
+ sig.ParameterTypes [0].Contains ("JniObjectReference")) {
+ Assert.True (sig.ParameterTypes [0].EndsWith ("&"),
+ $"JI-style invoker .ctor first param must be byref, got: {sig.ParameterTypes [0]}");
+ foundByRefCtor = true;
+ }
+ }
+
+ Assert.True (foundByRefCtor, "Expected to find a JI-style invoker .ctor with byref JniObjectReference parameter");
+ }
+
[Fact]
public void Generate_JiStyleCtor_EmitsDeleteRefCall ()
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
index ec8ba3e7ed0..659de452634 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -44,13 +44,14 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases ()
Assert.NotNull (onStart);
Assert.Equal ("", onStart.Connector);
- var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener")
- .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick");
+ var listener = FindFixtureByManagedName ("Android.Views.IOnClickListener");
+ var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick");
Assert.NotNull (onClick);
Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature);
- Assert.Equal ("Android.Views.IOnClickListenerInvoker",
- FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName);
+ Assert.Null (listener.ActivationCtor);
+ Assert.Equal (ActivationCtorStyle.XamarinAndroid, listener.InvokerActivationCtorStyle);
}
[Theory]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index 7d57a37657c..fddfaae55a9 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -131,4 +131,20 @@ public void Scan_JniTypeSignature_SubclassExtendsJavaPeer ()
var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject");
Assert.NotNull (peer);
}
+
+ [Fact]
+ public void Scan_JniTypeSignature_ArrayRank_IsExcluded ()
+ {
+ // Types with [JniTypeSignature(ArrayRank > 0)] represent JNI array wrappers
+ // (e.g., JavaBooleanArray with IsKeyword=true, or JavaObjectArray without).
+ // The scanner must skip all of them — they are handled by the built-in tables
+ // in JniRuntime.JniTypeManager, not the typemap.
+ var peers = ScanFixtures ();
+
+ // Keyword primitive array (e.g., JavaBooleanArray with "Z")
+ Assert.DoesNotContain (peers, p => p.ManagedTypeName == "Java.Interop.TestTypes.KeywordPrimitiveArray");
+
+ // Non-keyword array (e.g., JavaObjectArray with "java/lang/Object", ArrayRank=1)
+ Assert.DoesNotContain (peers, p => p.ManagedTypeName == "Java.Interop.TestTypes.NonKeywordArrayType");
+ }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index 9b220bb6d03..a4363fe9877 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -969,4 +969,23 @@ public class NonGeneratedJavaObject : JavaObject
{
public NonGeneratedJavaObject () { }
}
+
+ ///
+ /// Mimics Java.Interop.JavaBooleanArray — a primitive array type with IsKeyword=true.
+ /// The scanner must skip all ArrayRank > 0 types because they are handled by the
+ /// built-in tables in JniRuntime.JniTypeManager.
+ ///
+ [Java.Interop.JniTypeSignature ("Z", IsKeyword = true, ArrayRank = 1, GenerateJavaPeer = false)]
+ public sealed class KeywordPrimitiveArray : JavaObject
+ {
+ }
+
+ ///
+ /// Mimics Java.Interop.JavaObjectArray — a non-keyword array type with ArrayRank=1.
+ /// The scanner must also skip these to avoid adding unnecessary aliases.
+ ///
+ [Java.Interop.JniTypeSignature ("java/lang/Object", ArrayRank = 1, GenerateJavaPeer = false)]
+ public class NonKeywordArrayType : JavaObject
+ {
+ }
}
diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
index 2854ac5b224..b1fa7783883 100644
--- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
+++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
@@ -10,7 +10,40 @@
-
+
+
+
+
+
+
+
+
+
+
managedReferences = new ArrayList