diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 0ef4f1febfa..ae77f286553 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -207,6 +207,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4247](xa4247.md): Could not resolve POM file for artifact '{artifact}'. + [XA4248](xa4248.md): Could not find NuGet package '{nugetId}' version '{version}' in lock file. Ensure NuGet Restore has run since this `` was added. + [XA4235](xa4249.md): Maven artifact specification '{artifact}' is invalid. The correct format is 'group_id:artifact_id:version'. ++ [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4250.md b/Documentation/docs-mobile/messages/xa4250.md new file mode 100644 index 00000000000..3ef74ddc5e5 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4250.md @@ -0,0 +1,33 @@ +--- +title: .NET for Android warning XA4250 +description: XA4250 warning code +ms.date: 04/07/2026 +f1_keywords: + - "XA4250" +--- + +# .NET for Android warning XA4250 + +## Example message + +Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + +```text +warning XA4250: Manifest-referenced type 'com.example.MainActivity' was not found in any scanned assembly. It may be a framework type. +``` + +## Issue + +The build found a type name in `AndroidManifest.xml`, but it could not match that name to any Java peer discovered in the app's managed assemblies. + +This can be expected for framework-provided types, but it can also indicate that the manifest entry does not match the name generated for a managed Android component. + +## Solution + +If the manifest entry refers to an Android framework type, this warning can usually be ignored. + +Otherwise: + +1. Verify the `android:name` value in the manifest. +2. Ensure the managed type is included in the app build. +3. Check for namespace, `[Register]`, or nested-type naming mismatches between the manifest and the managed type. diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index 869ac45cee1..c4b1cf25d6b 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -199,10 +199,20 @@ stages: testName: Mono.Android.NET_Tests-CoreCLR project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj testResultsFiles: TestResult-Mono.Android.NET_Tests-$(XA.Build.Configuration)CoreCLR.xml - extraBuildArgs: -p:TestsFlavor=CoreCLR -p:UseMonoRuntime=false + extraBuildArgs: -p:MonoAndroidTypeMapFlavor=legacy artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab artifactFolder: $(DotNetTargetFramework)-CoreCLR + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml + parameters: + configuration: $(XA.Build.Configuration) + testName: Mono.Android.NET_Tests-CoreCLRTrimmable + project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj + testResultsFiles: TestResult-Mono.Android.NET_Tests-$(XA.Build.Configuration)CoreCLRTrimmable.xml + extraBuildArgs: -p:MonoAndroidTypeMapFlavor=trimmable + artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab + artifactFolder: $(DotNetTargetFramework)-CoreCLRTrimmable + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml parameters: configuration: $(XA.Build.Configuration) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index e382556c748..6745e9b2b2e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -165,7 +165,7 @@ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings); } - internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) + internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer, string packageName) { string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); var element = new XElement ("instrumentation", @@ -176,6 +176,9 @@ internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) return; } PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings); + if (element.Attribute (AndroidNs + "targetPackage") is null && !string.IsNullOrEmpty (packageName)) { + element.SetAttributeValue (AndroidNs + "targetPackage", packageName); + } manifest.Add (element); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 7e41fc570d7..d629fc83d2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -122,15 +122,28 @@ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) { + string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); + // Application and Instrumentation types cannot call registerNatives in their - // static initializer — the native library isn't loaded yet at that point. - // Their registerNatives call is emitted in the generated - // ApplicationRegistration.registerApplications() method instead. + // static initializer — the runtime isn't ready yet at that point. Emit a + // lazy one-time helper instead so the first managed callback can register + // the class just before invoking its native method. if (type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" + private static boolean __md_natives_registered; + private static synchronized void __md_registerNatives () + { + if (!__md_natives_registered) { + mono.android.Runtime.registerNatives ({{className}}.class); + __md_natives_registered = true; + } + } + + +"""); return; } - string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); writer.Write ($$""" static { mono.android.Runtime.registerNatives ({{className}}.class); @@ -154,7 +167,17 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) public {{simpleClassName}} ({{parameters}}) { super ({{superArgs}}); + +"""); + + if (!type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}}); + +"""); + } + + writer.Write ($$""" } @@ -197,6 +220,10 @@ static void WriteFields (JavaPeerInfo type, TextWriter writer) static void WriteMethods (JavaPeerInfo type, TextWriter writer) { + string registerNativesLine = type.CannotRegisterInStaticConstructor + ? "\t\t__md_registerNatives ();\n" + : ""; + foreach (var method in type.MarshalMethods) { if (method.IsConstructor) { continue; @@ -222,7 +249,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) @Override public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); @@ -233,7 +260,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) {{access}} {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } {{access}} native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 27806a679bd..9f64a305023 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -73,7 +73,7 @@ class ManifestGenerator } if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) { - ComponentElementBuilder.AddInstrumentation (manifest, peer); + ComponentElementBuilder.AddInstrumentation (manifest, peer, PackageName); continue; } @@ -116,10 +116,7 @@ class ManifestGenerator } // Apply manifest placeholders - string? placeholders = ManifestPlaceholders; - if (placeholders is not null && placeholders.Length > 0) { - ApplyPlaceholders (doc, placeholders); - } + ApplyPlaceholders (doc, ManifestPlaceholders); return (doc, providerNames); } @@ -250,8 +247,12 @@ XElement CreateRuntimeProvider (string name, string? processName, int initOrder) /// Replaces ${key} placeholders in all attribute values throughout the document. /// Placeholder format: "key1=value1;key2=value2" /// - static void ApplyPlaceholders (XDocument doc, string placeholders) + internal static void ApplyPlaceholders (XDocument doc, string? placeholders) { + if (placeholders.IsNullOrEmpty ()) { + return; + } + var replacements = new Dictionary (StringComparer.Ordinal); foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { var eqIndex = entry.IndexOf ('='); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 56484f7a99a..42cf48447ee 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -182,7 +182,7 @@ sealed record UcoMethodData /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Signature must match the full JNI native method signature (jnienv + self + ctor params) /// so the ABI is correct when JNI dispatches the call. -/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). +/// Body: TrimmableTypeMap.ActivateInstance(self, typeof(TargetType)). /// sealed record UcoConstructorData { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 3cb37a217d6..1fbc0ad276d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -173,7 +173,9 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet used { // Use managed type name for proxy naming to guarantee uniqueness across aliases // (two types with the same JNI name will have different managed names). - var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + // Replace generic arity markers too, because backticks would make the emitted + // proxy type itself look generic even though we don't emit generic parameters. + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_').Replace ('`', '_') + "_Proxy"; // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 56ae75638b2..eec632ad3d7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -239,7 +239,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; var handle = Metadata.AddTypeDefinition ( - TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, default, Metadata.GetOrAddString ($"__utf8_{size}"), Metadata.AddTypeReference (SystemRuntimeRef, @@ -259,7 +259,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) - => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false); /// /// Emits a method body and definition with optional local variable declarations. @@ -269,9 +269,14 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, /// and must write the full LOCAL_SIG blob (header 0x07, /// compressed count, then each variable type). /// + /// + /// If true, creates a so the emitted IL can use + /// , , + /// and . + /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, - Action? encodeLocals) + Action? encodeLocals, bool useBranches = false) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); @@ -287,7 +292,11 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, } _codeBlob.Clear (); - var encoder = new InstructionEncoder (_codeBlob); + ControlFlowBuilder? cfb = null; + if (useBranches) { + cfb = new ControlFlowBuilder (); + } + var encoder = cfb != null ? new InstructionEncoder (_codeBlob, cfb) : new InstructionEncoder (_codeBlob); emitIL (encoder); while (ILBuilder.Count % 4 != 0) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 34bfd15a9b6..0f277287888 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -43,7 +43,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) -/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// => TrimmableTypeMap.ActivateInstance(self, typeof(Activity)); /// /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) @@ -77,7 +77,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; @@ -87,7 +86,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; - MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _withinNewObjectScopeRef; + MemberReferenceHandle _throwIfOpenGenericActivationRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -184,8 +184,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -240,13 +238,22 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", - sig => sig.MethodSignature ().Parameters (2, + // JniEnvironment.get_WithinNewObjectScope() -> bool (static property) + // Used by UCO constructors to skip activation when the object is being + // created from managed code via JNIEnv.StartCreateInstance/NewObject. + _withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope", + sig => sig.MethodSignature ().Parameters (0, + rt => rt.Type ().Boolean (), + p => { })); + + // TrimmableTypeMap.ThrowIfOpenGenericActivation() — static, internal + // Called by no-op UCO constructors for open generic types. + var trimmableTypeMapRef = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, + _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), _pe.Metadata.GetOrAddString ("TrimmableTypeMap")); + _throwIfOpenGenericActivationRef = _pe.AddMemberRef (trimmableTypeMapRef, "ThrowIfOpenGenericActivation", + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); + p => { })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", @@ -347,6 +354,16 @@ void EmitTypeMapAssociationAttributeCtorRef () void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { + if (proxy.IsAcw) { + // RegisterNatives uses RVA-backed UTF-8 fields under . + // Materialize those helper types before adding the proxy TypeDef, otherwise the + // later RegisterNatives method can be attached to the helper type instead. + foreach (var reg in proxy.NativeRegistrations) { + _pe.GetOrAddUtf8Field (reg.JniMethodName); + _pe.GetOrAddUtf8Field (reg.JniSignature); + } + } + var metadata = _pe.Metadata; var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, @@ -361,7 +378,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { @@ -369,6 +386,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary), - // so the wrapper signature must include all parameters to match the ABI. // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because ActivateInstance creates the managed peer using the + // are not forwarded because we create the managed peer using the // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); int paramCount = 2 + jniParams.Count; - var handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (paramCount, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); // jnienv - p.AddParameter ().Type ().IntPtr (); // self - for (int j = 0; j < jniParams.Count; j++) - JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); - }), - encoder => { - encoder.LoadArgument (1); // self - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - encoder.OpCode (ILOpCode.Ret); + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // self + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); + // Open generic types can't be activated from Java — the type parameters are unknown. + // Emit a no-op UCO so the nctor native method is registered (avoiding UnsatisfiedLinkError). + // The WithinNewObjectScope guard will cause this to return immediately when called + // from JNIEnv.StartCreateInstance, and the legacy NewOpenGenericTypeThrows test + // expects NotSupportedException from the FinishCreateInstance path. + if (proxy.IsGenericDefinition) { + var noopHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + encoder.OpCode (ILOpCode.Ret); + }); + AddUnmanagedCallersOnlyAttribute (noopHandle); + return noopHandle; + } + + var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ( + $"UCO constructor wrapper requires an activation ctor for '{uco.TargetType.ManagedTypeName}'"); + + MethodDefinitionHandle handle; + if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { + var ctorRef = AddJavaInteropActivationCtorRef ( + activationCtor.IsOnLeafType ? targetRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + // Skip activation if the object is being created from managed code + // (e.g., JNIEnv.StartCreateInstance / JNIEnv.NewObject). + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + if (!activationCtor.IsOnLeafType) { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetRef); + } + + encoder.LoadLocalAddress (0); + encoder.LoadArgument (1); // self + encoder.Call (_jniObjectReferenceCtorRef); + + if (activationCtor.IsOnLeafType) { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.Call (ctorRef); + } + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + EncodeJniObjectReferenceLocal, + useBranches: true); + } else { + var ctorRef = AddActivationCtorRef ( + activationCtor.IsOnLeafType ? targetRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + // Skip activation if the object is being created from managed code + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + if (activationCtor.IsOnLeafType) { + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetRef); + + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.Call (ctorRef); + } + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: null, + useBranches: true); + } + AddUnmanagedCallersOnlyAttribute (handle); return handle; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs new file mode 100644 index 00000000000..8224e2d7427 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -0,0 +1,24 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +public interface ITrimmableTypeMapLogger +{ + void LogNoJavaPeerTypesFound (); + + void LogJavaPeerScanInfo (int assemblyCount, int peerCount); + + void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount); + + void LogDeferredRegistrationTypesInfo (int typeCount); + + void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount); + + void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount); + + void LogGeneratedTypeMapAssembliesInfo (int assemblyCount); + + void LogGeneratedJcwFilesInfo (int sourceCount); + + void LogUnresolvedTypeWarning (string name); + + void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName); +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 249bdc8def1..99399656317 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,6 +5,7 @@ $(TargetFrameworkNETStandard) enable Nullable + true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -18,6 +19,8 @@ + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index aed62453042..ad4c6b5178c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -12,9 +12,11 @@ public sealed record JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". - /// Extracted from the [Register] attribute. + /// Extracted from the [Register] attribute or auto-computed during scanning. + /// Manifest rooting may later promote this to when + /// a component is referenced by its managed-namespace form. /// - public required string JavaName { get; init; } + public required string JavaName { get; set; } /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). @@ -48,7 +50,7 @@ public sealed record JavaPeerInfo /// 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; init; } + public string? BaseJavaName { get; set; } /// /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. @@ -69,16 +71,23 @@ public sealed record JavaPeerInfo /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). + /// May be set to true after scanning when the manifest references a type + /// that the scanner did not mark as unconditional. Should only ever be set + /// to true, never back to false. /// - public bool IsUnconditional { get; init; } + public bool IsUnconditional { get; set; } /// - /// True for Application and Instrumentation types. These types cannot call + /// True for Application and Instrumentation types, plus any generated managed + /// base classes they rely on during startup. These types cannot call /// registerNatives in their static initializer because the native library /// (libmonodroid.so) is not loaded until after the Application class is instantiated. /// Registration is deferred to ApplicationRegistration.registerApplications(). + /// This may also be set after scanning when a type is only discovered from + /// manifest android:name usage on <application> or + /// <instrumentation>. /// - public bool CannotRegisterInStaticConstructor { get; init; } + public bool CannotRegisterInStaticConstructor { get; set; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5efa08b058e..fdaecc46077 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -732,7 +733,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, (RegisterInfo Info, string DeclaringTypeName, string DeclaringAssemblyName)? FindBaseRegisteredMethodInfo ( TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -760,10 +761,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up the hierarchy (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so overrides can inherit [Register] + // metadata declared above an intermediate MCW base type. return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod); } @@ -796,7 +795,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, string getterName, MethodDefinition derivedGetter) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -835,10 +834,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so property overrides can inherit + // [Register] metadata declared above an intermediate MCW base type. return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter); } @@ -1477,8 +1474,11 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } + // Keep this in sync with JavaNativeTypeManager.ToJniName(Type)/(TypeDefinition). + // The trimmable build path must emit the exact same CRC64 package names that the + // runtime later computes for FindClass(Type) and peer activation. var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = System.IO.Hashing.Crc64.Hash (data); + var hash = Crc64Helper.Compute (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index ed9ea04384f..c79e6f33cab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -9,11 +9,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; public class TrimmableTypeMapGenerator { - readonly Action log; + readonly ITrimmableTypeMapLogger? logger; - public TrimmableTypeMapGenerator (Action log) + public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger? logger = null) { - this.log = log ?? throw new ArgumentNullException (nameof (log)); + this.logger = logger; } /// @@ -34,24 +34,28 @@ public TrimmableTypeMapResult Execute ( var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); if (allPeers.Count == 0) { - log ("No Java peer types found, skipping typemap generation."); + logger?.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } + RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + PropagateDeferredRegistrationToManagedBaseTypes (allPeers); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); - log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); + logger?.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); // Collect Application/Instrumentation types that need deferred registerNatives var appRegTypes = allPeers - .Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract) + .Where (p => p.CannotRegisterInStaticConstructor && !p.DoNotGenerateAcw) .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) + .Distinct (StringComparer.Ordinal) .ToList (); if (appRegTypes.Count > 0) { - log ($"Found {appRegTypes.Count} Application/Instrumentation types for deferred registration."); + logger?.LogDeferredRegistrationTypesInfo (appRegTypes.Count); } var manifest = manifestConfig is not null @@ -102,7 +106,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes using var scanner = new JavaPeerScanner (); var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); - log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types."); + logger?.LogJavaPeerScanInfo (assemblies.Count, peers.Count); return (peers, manifestInfo); } @@ -120,15 +124,15 @@ List GenerateTypeMapAssemblies (List allPeers, generator.Generate (peers, stream, assemblyName); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); - log ($" {assemblyName}: {peers.Count} types"); + logger?.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); rootGenerator.Generate (perAssemblyNames, rootStream); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); - log ($" Root: {perAssemblyNames.Count} per-assembly refs"); - log ($"Generated {generatedAssemblies.Count} typemap assemblies."); + logger?.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + logger?.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; } @@ -136,7 +140,230 @@ List GenerateJcwJavaSources (List allPeers) { var jcwGenerator = new JcwJavaSourceGenerator (); var sources = jcwGenerator.GenerateContent (allPeers); - log ($"Generated {sources.Count} JCW Java source files."); + logger?.LogGeneratedJcwFilesInfo (sources.Count); return sources.ToList (); } + + internal void RootManifestReferencedTypes (List allPeers, XDocument? doc) + { + if (doc?.Root is not { } root) { + return; + } + + XNamespace androidNs = "http://schemas.android.com/apk/res/android"; + XName attName = androidNs + "name"; + var packageName = (string?) root.Attribute ("package") ?? ""; + + var componentNames = new HashSet (StringComparer.Ordinal); + var deferredRegistrationNames = new HashSet (StringComparer.Ordinal); + foreach (var element in root.Descendants ()) { + switch (element.Name.LocalName) { + case "application": + case "activity": + case "instrumentation": + case "service": + case "receiver": + case "provider": + var name = (string?) element.Attribute (attName); + if (name is not null) { + var resolvedName = ResolveManifestClassName (name, packageName); + componentNames.Add (resolvedName); + + if (element.Name.LocalName is "application" or "instrumentation") { + deferredRegistrationNames.Add (resolvedName); + } + } + break; + } + } + + if (componentNames.Count == 0) { + return; + } + + // Build lookup by both Java and compat dot-names. Keep '$' for nested types, + // because manifests commonly use '$', but also include the Java source form. + var peersByDotName = new Dictionary> (StringComparer.Ordinal); + foreach (var peer in allPeers) { + AddJniLookupNames (peersByDotName, peer.JavaName, peer); + + if (peer.CompatJniName != peer.JavaName) { + AddJniLookupNames (peersByDotName, peer.CompatJniName, peer); + } + } + + foreach (var name in componentNames) { + if (peersByDotName.TryGetValue (name, out var peers)) { + foreach (var peer in peers.Distinct ()) { + PromoteManifestCompatibleJavaName (allPeers, peer, name); + + if (deferredRegistrationNames.Contains (name)) { + peer.CannotRegisterInStaticConstructor = true; + } + + if (!peer.IsUnconditional) { + peer.IsUnconditional = true; + logger?.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); + } + } + } else { + logger?.LogUnresolvedTypeWarning (name); + } + } + } + + static void PromoteManifestCompatibleJavaName (List allPeers, JavaPeerInfo peer, string manifestName) + { + if (peer.JavaName == peer.CompatJniName || !MatchesManifestName (peer.CompatJniName, manifestName)) { + return; + } + + var promotedJavaName = NormalizeJniName (peer.CompatJniName); + var previousJavaName = peer.JavaName; + if (promotedJavaName == previousJavaName) { + return; + } + + peer.JavaName = promotedJavaName; + + foreach (var candidate in allPeers) { + if (candidate.BaseJavaName == previousJavaName) { + candidate.BaseJavaName = promotedJavaName; + } + } + } + + void PropagateDeferredRegistrationToManagedBaseTypes (List allPeers) + { + var peersByJavaName = new Dictionary (StringComparer.Ordinal); + foreach (var peer in allPeers) { + if (!peersByJavaName.ContainsKey (peer.JavaName)) { + peersByJavaName.Add (peer.JavaName, peer); + } + if (peer.CompatJniName != peer.JavaName && !peersByJavaName.ContainsKey (peer.CompatJniName)) { + peersByJavaName.Add (peer.CompatJniName, peer); + } + } + + foreach (var peer in allPeers.Where (p => p.CannotRegisterInStaticConstructor)) { + PropagateDeferredRegistrationToManagedBaseTypes (peer, peersByJavaName); + } + } + + void PropagateDeferredRegistrationToManagedBaseTypes (JavaPeerInfo peer, Dictionary peersByJavaName) + { + var visited = new HashSet (StringComparer.Ordinal); + var baseJavaName = peer.BaseJavaName; + + while (!baseJavaName.IsNullOrEmpty () && visited.Add (baseJavaName)) { + if (!peersByJavaName.TryGetValue (baseJavaName, out var basePeer) || basePeer.DoNotGenerateAcw) { + break; + } + + basePeer.CannotRegisterInStaticConstructor = true; + basePeer.IsUnconditional = true; + + baseJavaName = basePeer.BaseJavaName; + } + } + + static void AddPeerByDotName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) + { + if (!peersByDotName.TryGetValue (dotName, out var list)) { + list = []; + peersByDotName [dotName] = list; + } + + list.Add (peer); + } + + static XDocument? PrepareManifestForRooting (XDocument? manifestTemplate, ManifestConfig? manifestConfig) + { + if (manifestTemplate is null && manifestConfig is null) { + return null; + } + + var doc = manifestTemplate is not null + ? new XDocument (manifestTemplate) + : new XDocument ( + new XElement ( + "manifest", + new XAttribute (XNamespace.Xmlns + "android", ManifestConstants.AndroidNs.NamespaceName))); + + if (doc.Root is not { } root) { + return doc; + } + + if (manifestConfig is null) { + return doc; + } + + if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) { + root.SetAttributeValue ("package", manifestConfig.PackageName); + } + + ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders); + + if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) { + var app = root.Element ("application"); + if (app is null) { + app = new XElement ("application"); + root.Add (app); + } + + if (app.Attribute (ManifestConstants.AttName) is null) { + app.SetAttributeValue (ManifestConstants.AttName, manifestConfig.ApplicationJavaClass); + } + } + + return doc; +} + + static bool MatchesManifestName (string jniOrJavaName, string manifestName) + { + var normalizedName = NormalizeJniName (jniOrJavaName); + var simpleName = JniSignatureHelper.GetJavaSimpleName (normalizedName); + var packageName = JniSignatureHelper.GetJavaPackageName (normalizedName); + var manifestStyleName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName; + + return manifestStyleName == manifestName || JniSignatureHelper.JniNameToJavaName (normalizedName) == manifestName; + } + + static string NormalizeJniName (string jniOrJavaName) + { + return jniOrJavaName.IndexOf ('/') >= 0 + ? jniOrJavaName + : jniOrJavaName.Replace ('.', '/'); + } + + static void AddJniLookupNames (Dictionary> peersByDotName, string jniName, JavaPeerInfo peer) + { + var simpleName = JniSignatureHelper.GetJavaSimpleName (jniName); + var packageName = JniSignatureHelper.GetJavaPackageName (jniName); + var manifestName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName; + AddPeerByDotName (peersByDotName, manifestName, peer); + + var javaSourceName = JniSignatureHelper.JniNameToJavaName (jniName); + if (javaSourceName != manifestName) { + AddPeerByDotName (peersByDotName, javaSourceName, peer); + } + } + + /// + /// Resolves an android:name value to a fully-qualified class name. + /// Names starting with '.' are relative to the package. Names with no '.' at all + /// are also treated as relative (Android tooling convention). + /// + static string ResolveManifestClassName (string name, string packageName) + { + if (name.StartsWith (".", StringComparison.Ordinal)) { + return packageName + name; + } + + if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) { + return packageName + "." + name; + } + + return name; + } } diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8b004855ba8..aa91db31463 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -442,6 +442,12 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) internal static unsafe string? TypemapManagedToJava (Type type) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + return TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName) ? jniName : null; + } + if (mvid_bytes == null) mvid_bytes = new byte[16]; diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 097c8e5d0ab..8bb98c4a993 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -110,6 +110,7 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args) internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args) { androidRuntime = runtime; + JniRuntime.SetCurrent (runtime); SetSynchronizationContext (); } @@ -159,6 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) valueManager, args->jniAddNativeMethodRegistrationAttributePresent != 0 ); + JniRuntime.SetCurrent (androidRuntime); if (RuntimeFeature.TrimmableTypeMap) { TrimmableTypeMap.Initialize (); diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 379558e3265..7152e911b3e 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -268,7 +268,13 @@ static Type monovm_typemap_java_to_managed (string java_type_name) return type; } - if (RuntimeFeature.IsMonoRuntime) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + if (!TrimmableTypeMap.Instance.TryGetType (class_name, out type)) { + return null; + } + } else if (RuntimeFeature.IsMonoRuntime) { type = monovm_typemap_java_to_managed (class_name); } else if (RuntimeFeature.IsCoreClrRuntime) { type = clr_typemap_java_to_managed (class_name); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e1b7e975059..fa7aa786925 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -509,16 +509,11 @@ void ProcessContext (HandleContext* context) { if (RuntimeFeature.TrimmableTypeMap) { var typeMap = TrimmableTypeMap.Instance; - if (typeMap is not null && targetType is not null) { - var proxy = typeMap.GetProxyForManagedType (targetType); - if (proxy is not null) { - var peer = proxy.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); - if (peer is not null) { - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - JniObjectReference.Dispose (ref reference, transfer); - return peer; - } - } + var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); + if (peer is not null) { + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + JniObjectReference.Dispose (ref reference, transfer); + return peer; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f2c535dd8c7..a85233420ed 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -9,6 +9,7 @@ using System.Threading; using Android.Runtime; using Java.Interop; +using Java.Interop.Tools.TypeNameMappings; namespace Microsoft.Android.Runtime; @@ -28,6 +29,8 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly ConcurrentDictionary _proxyCache = new (); + readonly ConcurrentDictionary _jniNameCache = new (); + readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () { @@ -67,7 +70,24 @@ unsafe void RegisterNatives () } internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) - => _typeMap.TryGetValue (jniSimpleReference, out type); + { + if (!_typeMap.TryGetValue (jniSimpleReference, out var mappedType)) { + type = null; + return false; + } + + // External typemap entries point at the generated proxy for ACW-backed types. + // The JniTypeManager, however, must surface the real managed peer type so + // Java object activation and virtual dispatch resolve to the user's override + // instead of the bound Android base type. + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is null) { + type = null; + return false; + } + type = proxy.TargetType; + return true; + } /// /// Finds the proxy for a managed type by resolving its JNI name (from [Register] or @@ -83,8 +103,10 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty return direct; } - // Resolve the JNI name from the managed type's attributes - if (!TryGetJniNameForType (type, out var jniName)) { + // Resolve the JNI name from attributes first, then fall back to the + // generated TypeMap entries for ACW/component types which don't carry + // [Register]/[JniTypeSignature] themselves. + if (!self.TryGetJniName (type, out var jniName)) { return null; } @@ -97,6 +119,104 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty }, this); } + internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) + { + if (_jniNameCache.TryGetValue (type, out jniName)) { + return jniName != null; + } + + if (TryGetJniNameForType (type, out jniName)) { + _jniNameCache [type] = jniName; + return true; + } + + if (TryGetCompatJniNameForAndroidComponent (type, out jniName)) { + _jniNameCache [type] = jniName; + return true; + } + + // Prefer the JavaNativeTypeManager calculation for user/application types, + // as it matches the ACW generation rules used during the build. + if (typeof (IJavaPeerable).IsAssignableFrom (type)) { + jniName = JavaNativeTypeManager.ToJniName (type); + if (!string.IsNullOrEmpty (jniName) && jniName != "java/lang/Object") { + _jniNameCache [type] = jniName; + return true; + } + } + + jniName = null; + return false; + } + + internal JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) + { + if (handle == IntPtr.Zero) { + return null; + } + + var selfRef = new JniObjectReference (handle); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + + try { + while (jniClass.IsValid) { + var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + if (className != null) { + if (_peerProxyCache.TryGetValue (className, out var cached)) { + if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { + return cached; + } + } else if (_typeMap.TryGetValue (className, out var mappedType)) { + var proxy = mappedType.GetCustomAttribute (inherit: false); + _peerProxyCache [className] = proxy; + if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + return proxy; + } + } + } + + var super = JniEnvironment.Types.GetSuperclass (jniClass); + JniObjectReference.Dispose (ref jniClass); + jniClass = super; + } + } finally { + JniObjectReference.Dispose (ref jniClass); + } + + return null; + } + + internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) + { + var proxy = GetProxyForPeer (handle, targetType); + if (proxy is null && targetType is not null) { + proxy = GetProxyForManagedType (targetType); + // Verify the Java object is actually assignable to the target Java type + // before creating the peer. Without this, we'd create invalid peers + // (e.g., IAppendableInvoker wrapping a java.lang.Integer). + if (proxy is not null && TryGetJniName (targetType, out var targetJniName)) { + var selfRef = new JniObjectReference (handle); + var objClass = JniEnvironment.Types.GetObjectClass (selfRef); + JniObjectReference targetClass; + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + } catch { + JniObjectReference.Dispose (ref objClass); + proxy = null; + return null; + } + bool isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass); + JniObjectReference.Dispose (ref objClass); + JniObjectReference.Dispose (ref targetClass); + if (!isAssignable) { + proxy = null; + } + } + } + + return proxy?.CreateInstance (handle, transfer); + } + /// /// Resolves a managed type's JNI name from its /// (implemented by both [Register] and [JniTypeSignature]). @@ -113,18 +233,66 @@ internal static bool TryGetJniNameForType (Type type, [NotNullWhen (true)] out s return false; } + static bool TryGetCompatJniNameForAndroidComponent (Type type, [NotNullWhen (true)] out string? jniName) + { + if (!IsAndroidComponentType (type)) { + jniName = null; + return false; + } + + var (typeName, parentJniName, ns) = GetCompatTypeNameParts (type); + jniName = parentJniName is not null + ? $"{parentJniName}_{typeName}" + : ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + return true; + } + + static bool IsAndroidComponentType (Type type) + { + return type.IsDefined (typeof (global::Android.App.ActivityAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ApplicationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.InstrumentationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ServiceAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.BroadcastReceiverAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.ContentProviderAttribute), inherit: false); + } + + static (string TypeName, string? ParentJniName, string Namespace) GetCompatTypeNameParts (Type type) + { + var nameParts = new List { SanitizeTypeName (type.Name) }; + var current = type; + string? parentJniName = null; + + while (current.DeclaringType is Type parentType) { + if (TryGetJniNameForType (parentType, out var explicitJniName) || + TryGetCompatJniNameForAndroidComponent (parentType, out explicitJniName)) { + parentJniName = explicitJniName; + break; + } + + nameParts.Add (SanitizeTypeName (parentType.Name)); + current = parentType; + } + + nameParts.Reverse (); + return (string.Join ("_", nameParts), parentJniName, current.Namespace ?? ""); + } + + static string SanitizeTypeName (string name) + { + var tick = name.IndexOf ('`'); + return (tick >= 0 ? name.Substring (0, tick) : name).Replace ('+', '_'); + } + /// /// Creates a peer instance using the proxy's CreateInstance method. /// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance. /// internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer) { - var proxy = GetProxyForManagedType (type); - if (proxy is null) { - return false; - } - - return proxy.CreateInstance (handle, transfer) != null; + return CreatePeer (handle, transfer, type) != null; } const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; @@ -147,39 +315,6 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf return GetProxyForManagedType (type)?.GetContainerFactory (); } - /// - /// Creates a managed peer instance for a Java object being constructed. - /// Called from generated UCO constructor wrappers (nctor_*_uco). - /// - internal static void ActivateInstance (IntPtr self, Type targetType) - { - var instance = s_instance; - if (instance is null) { - throw new InvalidOperationException ("TrimmableTypeMap has not been initialized."); - } - - // Look up the proxy via JNI class name → TypeMap dictionary. - // We can't use targetType.GetCustomAttribute() because the - // self-application attribute is on the proxy type, not the target type. - var selfRef = new JniObjectReference (self); - var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); - var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); - JniObjectReference.Dispose (ref jniClass); - - if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - - var proxy = proxyType.GetCustomAttribute (inherit: false); - if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}'. " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - } - [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 220be6aae78..c77b4219b85 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -32,7 +32,7 @@ protected override IEnumerable GetSimpleReferences (Type type) yield return r; } - if (TrimmableTypeMap.TryGetJniNameForType (type, out var jniName)) { + if (TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName)) { yield return jniName; yield break; } @@ -40,7 +40,7 @@ protected override IEnumerable GetSimpleReferences (Type type) // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable // extends Java.Lang.Error but has no [Register] attribute itself). for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) { - if (TrimmableTypeMap.TryGetJniNameForType (baseType, out var baseJniName)) { + if (TrimmableTypeMap.Instance.TryGetJniName (baseType, out var baseJniName)) { yield return baseJniName; yield break; } diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index bc2d9a7d4d4..c278679f4ee 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1427,6 +1427,15 @@ public static string XA4249 { } } + /// + /// Looks up a localized string similar to Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.. + /// + public static string XA4250 { + get { + return ResourceManager.GetString("XA4250", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index d1ee02dc051..ca75ec196de 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1068,6 +1068,11 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Maven artifact specification '{0}' is invalid. The correct format is 'group_id:artifact_id:version'. The following are literal names and should not be translated: Maven, group_id, artifact_id {0} - A Maven artifact specification + + + Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + The following are literal names and should not be translated: Manifest, framework. +{0} - Java type name from AndroidManifest.xml Command '{0}' failed.\n{1} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 5e420bd0251..7667ddb2a2c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -15,6 +15,66 @@ namespace Xamarin.Android.Tasks; public class GenerateTrimmableTypeMap : AndroidTask { + sealed class MSBuildTrimmableTypeMapLogger : ITrimmableTypeMapLogger + { + readonly TaskLoggingHelper log; + + public MSBuildTrimmableTypeMapLogger (TaskLoggingHelper log) + { + this.log = log ?? throw new ArgumentNullException (nameof (log)); + } + + public void LogNoJavaPeerTypesFound () + { + log.LogMessage (MessageImportance.Low, "No Java peer types found, skipping typemap generation."); + } + + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) + { + log.LogMessage (MessageImportance.Low, $"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); + } + + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) + { + log.LogMessage (MessageImportance.Low, $"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); + } + + public void LogDeferredRegistrationTypesInfo (int typeCount) + { + log.LogMessage (MessageImportance.Low, $"Found {typeCount} Application/Instrumentation types for deferred registration."); + } + + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) + { + log.LogMessage (MessageImportance.Low, $" {assemblyName}: {typeCount} types"); + } + + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) + { + log.LogMessage (MessageImportance.Low, $" Root: {assemblyReferenceCount} per-assembly refs"); + } + + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) + { + log.LogMessage (MessageImportance.Low, $"Generated {assemblyCount} typemap assemblies."); + } + + public void LogGeneratedJcwFilesInfo (int sourceCount) + { + log.LogMessage (MessageImportance.Low, $"Generated {sourceCount} JCW Java source files."); + } + + public void LogUnresolvedTypeWarning (string name) + { + log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, name); + } + + public void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName) + { + log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{name}' ({managedTypeName}) as unconditional."); + } + } + public override string TaskPrefix => "GTT"; [Required] @@ -94,7 +154,7 @@ public override bool RunTask () ApplicationJavaClass: ApplicationJavaClass); } - var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); + var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); XDocument? manifestTemplate = null; if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 95afe0971f0..30d81218883 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,16 +138,20 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_ApplicationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyApplication"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyApplication.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyApplication.class);\n", java); } [Fact] public void Generate_InstrumentationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyInstrumentation"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyInstrumentation.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyInstrumentation.class);\n", java); } } @@ -257,6 +261,61 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } + [Fact] + public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/SelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_OverrideAcrossGenericIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/GenericSelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_DeferredRegistrationType_LazilyRegistersBeforeNativeCallback () + { + var type = new JavaPeerInfo { + JavaName = "my/app/DeferredInstrumentation", + CompatJniName = "my/app/DeferredInstrumentation", + ManagedTypeName = "MyApp.DeferredInstrumentation", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "DeferredInstrumentation", + AssemblyName = "App", + BaseJavaName = "android/app/Instrumentation", + CannotRegisterInStaticConstructor = true, + MarshalMethods = new List { + new () { + JniName = "onCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + NativeCallbackName = "n_OnCreate_Landroid_os_Bundle_", + Connector = "GetOnCreate_Landroid_os_Bundle_Handler", + }, + new () { + JniName = "onStart", + JniSignature = "()V", + ManagedMethodName = "OnStart", + NativeCallbackName = "n_OnStart", + Connector = "GetOnStartHandler", + }, + }, + }; + + var java = GenerateToString (type); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnStart ();\n", java); + } + } public class NestedType diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 07929a9d5a4..a3e7904f654 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -253,6 +253,24 @@ public void Instrumentation_GoesToManifest () Assert.Null (appInstrumentation); } + [Fact] + public void Instrumentation_DefaultsTargetPackageToManifestPackage () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { + Kind = ComponentKind.Instrumentation, + Properties = new Dictionary { + ["Label"] = "My Test", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var instrumentation = doc.Root?.Element ("instrumentation"); + + Assert.NotNull (instrumentation); + Assert.Equal ("com.example.app", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); + } + [Fact] public void RuntimeProvider_Added () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 7be68db2eb4..b05d5b76fde 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -12,6 +12,68 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); + sealed class TestTrimmableTypeMapLogger : ITrimmableTypeMapLogger + { + readonly List logMessages; + readonly List? warnings; + + public TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) + { + this.logMessages = logMessages; + this.warnings = warnings; + } + + public void LogNoJavaPeerTypesFound () + { + logMessages.Add ("No Java peer types found, skipping typemap generation."); + } + + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) + { + logMessages.Add ($"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); + } + + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) + { + logMessages.Add ($"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); + } + + public void LogDeferredRegistrationTypesInfo (int typeCount) + { + logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); + } + + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) + { + logMessages.Add ($" {assemblyName}: {typeCount} types"); + } + + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) + { + logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); + } + + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) + { + logMessages.Add ($"Generated {assemblyCount} typemap assemblies."); + } + + public void LogGeneratedJcwFilesInfo (int sourceCount) + { + logMessages.Add ($"Generated {sourceCount} JCW Java source files."); + } + + public void LogUnresolvedTypeWarning (string name) + { + warnings?.Add (name); + } + + public void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName) + { + logMessages.Add ($"Rooting manifest-referenced type '{name}' ({managedTypeName}) as unconditional."); + } + } + [Fact] public void Execute_EmptyAssemblyList_ReturnsEmptyResults () { @@ -79,7 +141,249 @@ public void Execute_JavaSourcesHaveCorrectStructure () Assert.Contains ("class ", source.Content); } - TrimmableTypeMapGenerator CreateGenerator () => new (msg => logMessages.Add (msg)); + [Fact] + public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet (), + new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "21", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider", + ManifestPlaceholders: "applicationId=my.app"), + manifestTemplate); + + var peer = result.AllPeers.First (p => p.ManagedTypeName == "MyApp.SimpleActivity"); + Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); + } + + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); + + TrimmableTypeMapGenerator CreateGenerator (List warnings) => + new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + + [Theory] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] + [InlineData ("com/example/MyService", "com.example.MyService", "com.example", "service", "MyService")] + [InlineData ("crc64123456789abc/MyActivity", "my/app/MyActivity", "my.app", "activity", ".MyActivity")] + [InlineData ("com/example/Outer$Inner", "com.example.Outer$Inner", "com.example", "activity", "com.example.Outer$Inner")] + public void RootManifestReferencedTypes_RootsManifestReferencedTypes ( + string javaName, + string compatJniName, + string packageName, + string elementName, + string manifestName) + { + var peers = new List { + new JavaPeerInfo { + JavaName = javaName, CompatJniName = compatJniName, + ManagedTypeName = "MyApp.MyTarget", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyTarget", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/OtherType", CompatJniName = "com.example.OtherType", + ManagedTypeName = "MyApp.OtherType", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "OtherType", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse ($$""" + + + + <{{elementName}} android:name="{{manifestName}}" /> + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "The manifest-referenced type should be rooted as unconditional."); + Assert.False (peers [1].IsUnconditional, "Non-matching peers should remain conditional."); + Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyApplication", CompatJniName = "com.example.MyApplication", + ManagedTypeName = "MyApp.MyApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyApplication", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyInstrumentation", CompatJniName = "com.example.MyInstrumentation", + ManagedTypeName = "MyApp.MyInstrumentation", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyInstrumentation", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Application type should be rooted from ."); + Assert.True (peers [1].IsUnconditional, "Instrumentation type should be rooted from ."); + Assert.True (peers [0].CannotRegisterInStaticConstructor, "Application type should defer Runtime.registerNatives()."); + Assert.True (peers [1].CannotRegisterInStaticConstructor, "Instrumentation type should defer Runtime.registerNatives()."); + } + + [Fact] + public void RootManifestReferencedTypes_PromotesCompatJniNameForManifestDeclaredApplication () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyApplication", CompatJniName = "my/app/MyApplication", + ManagedTypeName = "MyApp.MyApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyApplication", + AssemblyName = "MyApp", BaseJavaName = "android/app/Application", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyDerivedApplication", CompatJniName = "my/app/MyDerivedApplication", + ManagedTypeName = "MyApp.MyDerivedApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyDerivedApplication", + AssemblyName = "MyApp", BaseJavaName = "crc64123456789abc/MyApplication", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.Equal ("my/app/MyApplication", peers [0].JavaName); + Assert.Equal ("my/app/MyApplication", peers [1].BaseJavaName); + Assert.True (peers [0].CannotRegisterInStaticConstructor); + } + + [Fact] + public void Execute_DeferredRegistrationIncludesManagedBaseHierarchy () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet ()); + + Assert.Contains ("my.app.BaseApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes); + + var baseInstrumentation = result.GeneratedJavaSources.Single (s => s.RelativePath == "my/app/BaseInstrumentation.java"); + var intermediateInstrumentation = result.GeneratedJavaSources.Single (s => s.RelativePath == "my/app/IntermediateInstrumentation.java"); + + Assert.DoesNotContain ("static {", baseInstrumentation.Content); + Assert.DoesNotContain ("static {", intermediateInstrumentation.Content); + Assert.Contains ("private static synchronized void __md_registerNatives ()", baseInstrumentation.Content); + Assert.Contains ("private static synchronized void __md_registerNatives ()", intermediateInstrumentation.Content); + Assert.DoesNotContain ("if (getClass () == BaseInstrumentation.class) nctor_0 ();", baseInstrumentation.Content); + Assert.DoesNotContain ("if (getClass () == IntermediateInstrumentation.class) nctor_0 ();", intermediateInstrumentation.Content); + } + + [Fact] + public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var warnings = new List (); + var generator = CreateGenerator (warnings); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService")); + } + + [Fact] + public void RootManifestReferencedTypes_SkipsAlreadyUnconditional () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = true, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional); + Assert.DoesNotContain (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_EmptyManifest_NoChanges () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.False (peers [0].IsUnconditional); + } static PEReader CreateTestFixturePEReader () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d892b0161da..6bb225f1d06 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -84,6 +84,26 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () Assert.Contains ("CreateInstance", methods); } + [Fact] + public void Generate_ProxyType_HasSelfAppliedJavaPeerProxyAttribute () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var objectProxy = reader.TypeDefinitions + .Select (h => (Handle: h, Type: reader.GetTypeDefinition (h))) + .First (t => reader.GetString (t.Type.Namespace) == "_TypeMap.Proxies" && + reader.GetString (t.Type.Name) == "Java_Lang_Object_Proxy"); + + var attrs = objectProxy.Type.GetCustomAttributes ().ToList (); + Assert.Single (attrs); + + var attr = reader.GetCustomAttribute (attrs [0]); + var ctor = reader.GetMethodDefinition ((MethodDefinitionHandle) attr.Constructor); + Assert.Equal (".ctor", reader.GetString (ctor.Name)); + } + [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { @@ -199,7 +219,21 @@ public void Generate_GenericType_ThrowsNotSupportedException () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeNames = GetTypeRefNames (reader); + var generatedTypeNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + var proxyMethods = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .Where (t => reader.GetString (t.Name) == "MyApp_Generic_GenericHolder_1_Proxy") + .SelectMany (t => t.GetMethods ()) + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); Assert.Contains ("NotSupportedException", typeNames); + Assert.Contains ("MyApp_Generic_GenericHolder_1_Proxy", generatedTypeNames); + Assert.DoesNotContain (generatedTypeNames, name => name.Contains ('`')); + Assert.Contains ("RegisterNatives", proxyMethods); } [Fact] @@ -422,14 +456,15 @@ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var memberNames = GetMemberRefNames (reader); - - // RegisterNatives is a method definition on the proxy type, not a member reference - var methodDefs = reader.MethodDefinitions - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) + var proxyMethods = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .Where (t => reader.GetString (t.Name) == "MyApp_MainActivity_Proxy") + .SelectMany (t => t.GetMethods ()) + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) .ToList (); - Assert.Contains ("RegisterNatives", methodDefs); + Assert.Contains ("RegisterNatives", proxyMethods); + Assert.Contains (proxyMethods, name => name.Contains ("_uco_")); } [Fact] @@ -545,6 +580,26 @@ public void Generate_AcwProxy_HasPrivateImplementationDetails () Assert.Contains ("", typeDefNames); } + [Fact] + public void Generate_AcwProxy_UsesAssemblyVisibleUtf8HelperTypes () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity"); + + using var stream = GenerateAssembly (new [] { acwPeer }, "Utf8VisibilityTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var utf8Helpers = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Name).StartsWith ("__utf8_", StringComparison.Ordinal)) + .ToList (); + + Assert.NotEmpty (utf8Helpers); + Assert.All (utf8Helpers, t => + Assert.Equal (TypeAttributes.NestedAssembly, t.Attributes & TypeAttributes.VisibilityMask)); + } + [Fact] public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index d76129379db..edc7df9683f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; +using System.Text; +using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -87,4 +90,28 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () Assert.False (peer.DoNotGenerateAcw); Assert.True (peer.IsUnconditional, "Should be unconditional due to [Activity]"); } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_MatchesJavaNativeTypeManager (string managedName) + { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssemblyPath = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + var fixtureAssembly = Assembly.LoadFrom (fixtureAssemblyPath); + var fixtureType = fixtureAssembly.GetType (managedName); + if (fixtureType is null) { + throw new InvalidOperationException ($"Could not load fixture type '{managedName}' from '{fixtureAssemblyPath}'."); + } + + var assemblyName = fixtureType.Assembly.GetName ().Name + ?? throw new InvalidOperationException ($"Could not determine assembly name for '{managedName}'."); + var data = Encoding.UTF8.GetBytes ($"{fixtureType.Namespace}:{assemblyName}"); + var hash = Crc64Helper.Compute (data); + var expectedJavaName = $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}/{fixtureType.Name}"; + + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 54f7e0e133a..0213fd3ebb6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -102,6 +102,24 @@ public void MultipleOverloads_PicksCorrectOne () Assert.DoesNotContain (nonCtorMethods, m => m.JniName == "process" && m.JniSignature == "()V"); } + [Fact] + public void OverrideAcrossIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/SelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + + [Fact] + public void OverrideAcrossGenericIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/GenericSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index eac273ba746..776ca77c494 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -49,6 +49,20 @@ public class Service : Java.Lang.Object { protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("android/app/Application", DoNotGenerateAcw = true)] + public class Application : Java.Lang.Object + { + public Application () { } + protected Application (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/app/Instrumentation", DoNotGenerateAcw = true)] + public class Instrumentation : Java.Lang.Object + { + public Instrumentation () { } + protected Instrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.App.Backup @@ -317,8 +331,11 @@ public void PublicMethod () { } protected void ProtectedMethod () { } } + [Register ("my/app/BaseApplication")] + public abstract class BaseApplication : Android.App.Application { } + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] - public class MyApplication : Java.Lang.Object { } + public class MyApplication : BaseApplication { } /// /// Has [ExportField] methods that should produce Java field declarations. @@ -335,8 +352,14 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } + [Register ("my/app/BaseInstrumentation")] + public abstract class BaseInstrumentation : Android.App.Instrumentation { } + + [Register ("my/app/IntermediateInstrumentation")] + public abstract class IntermediateInstrumentation : BaseInstrumentation { } + [Instrumentation (Name = "my.app.MyInstrumentation")] - public class MyInstrumentation : Java.Lang.Object { } + public class MyInstrumentation : IntermediateInstrumentation { } [Register ("my/app/MyBackupAgent")] public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -751,6 +774,72 @@ protected OverloadDerived (IntPtr handle, JniHandleOwnership transfer) : base (h public override void Process (int value) { } } + /// + /// Declares a registered abstract method above an intermediate MCW base type. + /// Mirrors AdapterView.SetSelection(int) for AbsListView-derived test fixtures. + /// + [Register ("my/app/SelectionHost", DoNotGenerateAcw = true)] + public abstract class SelectionHost : Java.Lang.Object + { + protected SelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that inherits the registered method without redeclaring it. + /// + [Register ("my/app/SelectionContainer", DoNotGenerateAcw = true)] + public abstract class SelectionContainer : SelectionHost + { + protected SelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Generic base used to verify override discovery through a generic-instantiated base type. + /// Mirrors AdapterView<T> in the real Mono.Android hierarchy. + /// + [Register ("my/app/GenericSelectionHost", DoNotGenerateAcw = true)] + public abstract class GenericSelectionHost : Java.Lang.Object where T : class + { + protected GenericSelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that closes the generic base. + /// + [Register ("my/app/GenericSelectionContainer", DoNotGenerateAcw = true)] + public abstract class GenericSelectionContainer : GenericSelectionHost + { + protected GenericSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method declared above the first MCW base in the hierarchy. + /// + [Register ("my/app/SelectableList")] + public class SelectableList : SelectionContainer + { + protected SelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + + /// + /// Overrides a registered method declared above a generic-instantiated MCW base. + /// + [Register ("my/app/GenericSelectableList")] + public class GenericSelectableList : GenericSelectionContainer + { + protected GenericSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs index 1b13316cc3d..1ef98e558e7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs @@ -15,7 +15,7 @@ namespace Java.InteropTests { [TestFixture] public class JavaObjectExtensionsTests { - [Test] + [Test, Category ("TrimmableIgnore")] public void JavaCast_BaseToGenericWrapper () { using (var list = new JavaList (new[]{ 1, 2, 3 })) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index a563f56fd3a..be5347a780a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -121,7 +121,7 @@ public void InvokingNullInstanceDoesNotCrashDalvik () } } - [Test] + [Test, Category ("TrimmableIgnore")] public void NewOpenGenericTypeThrows () { try { @@ -301,7 +301,7 @@ public void ActivatedDirectObjectSubclassesShouldBeRegistered () } } - [Test] + [Test, Category ("TrimmableIgnore")] public void ActivatedDirectThrowableSubclassesShouldBeRegistered () { if (Build.VERSION.SdkInt <= BuildVersionCodes.GingerbreadMr1) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs index 764fc416379..a5f61e6c54c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs @@ -72,9 +72,11 @@ public void JnienvCreateInstance_RegistersMultipleInstances () var intermediate = CreateInstance_OverrideAbsListView_Adapter.Intermediate; var registered = Java.Lang.Object.GetObject(adapter.Handle, JniHandleOwnership.DoNotTransfer); + var asBase = Java.Lang.Object.GetObject(adapter.Handle, JniHandleOwnership.DoNotTransfer); Assert.AreNotSame (adapter, intermediate); Assert.AreSame (adapter, registered); + Assert.AreSame (adapter, asBase); } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index ae33bef1571..37137220e0e 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -37,6 +37,16 @@ $(ExcludeCategories):InetAccess:NetworkInterfaces + + + false + <_AndroidTypeMapImplementation Condition=" '$(MonoAndroidTypeMapFlavor)' == 'legacy' and '$(_AndroidTypeMapImplementation)' == '' ">llvm-ir + <_AndroidTypeMapImplementation Condition=" '$(MonoAndroidTypeMapFlavor)' == 'trimmable' and '$(_AndroidTypeMapImplementation)' == '' ">trimmable + CoreCLR + CoreCLRTrimmable + $(ExcludeCategories):NativeTypeMap:TrimmableIgnore:SSL + + @@ -260,6 +270,18 @@ + + + + + + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index 215fde081a9..867dab2ffb9 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -25,6 +25,20 @@ protected override string LogTag protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + // Java.Interop-Tests fixtures that use JavaObject types (not Java.Lang.Object) + // don't have JCW Java classes in the trimmable APK, and method remapping + // tests require Java-side support not present in the trimmable path. + // Exclude these entire fixtures to prevent ClassNotFoundException crashes. + ExcludedTestNames = new [] { + "Java.InteropTests.JavaObjectTest", + "Java.InteropTests.InvokeVirtualFromConstructorTests", + "Java.InteropTests.JniPeerMembersTests", + "Java.InteropTests.JniTypeManagerTests", + "Java.InteropTests.JniValueMarshaler_object_ContractTests", + "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", + }; + } } protected override IList GetTestAssemblies()