From f01e5082e3e6c32e3cdf121f5c5ba2b1449ac4e7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:03 +0200 Subject: [PATCH] Generator fixes for trimmable typemap - Fix CRC64 algorithm (Jones instead of System.IO.Hashing) - Fix inherited override detection (walk past DoNotGenerateAcw bases) - Fix instrumentation targetPackage - JCW lazy registerNatives for App/Instrumentation types - Sanitize backtick in proxy names - Manifest name promotion and deferred registration propagation - Build targets and task updates - Typed logger interface - Unit test coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4250.md | 33 ++ .../Generator/ComponentElementBuilder.cs | 5 +- .../Generator/JcwJavaSourceGenerator.cs | 39 ++- .../Generator/ManifestGenerator.cs | 13 +- .../Generator/Model/TypeMapAssemblyData.cs | 2 +- .../Generator/ModelBuilder.cs | 4 +- .../ITrimmableTypeMapLogger.cs | 24 ++ ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 + .../Scanner/JavaPeerInfo.cs | 21 +- .../Scanner/JavaPeerScanner.cs | 22 +- .../TrimmableTypeMapGenerator.cs | 251 +++++++++++++- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 5 + .../Tasks/GenerateTrimmableTypeMap.cs | 62 +++- .../Generator/JcwJavaSourceGeneratorTests.cs | 63 +++- .../Generator/ManifestGeneratorTests.cs | 18 ++ .../TrimmableTypeMapGeneratorTests.cs | 306 +++++++++++++++++- .../TypeMapAssemblyGeneratorTests.cs | 6 + .../Scanner/JavaPeerScannerTests.cs | 27 ++ .../Scanner/OverrideDetectionTests.cs | 18 ++ .../TestFixtures/TestTypes.cs | 93 +++++- 22 files changed, 975 insertions(+), 50 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4250.md create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs 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/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/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/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..57ad34543dc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -199,7 +199,13 @@ public void Generate_GenericType_ThrowsNotSupportedException () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeNames = GetTypeRefNames (reader); + var generatedTypeNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); Assert.Contains ("NotSupportedException", typeNames); + Assert.Contains ("MyApp_Generic_GenericHolder_1_Proxy", generatedTypeNames); + Assert.DoesNotContain (generatedTypeNames, name => name.Contains ('`')); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 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. ///