diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 517d04d0364..29e8d56742a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -45,14 +45,14 @@ public sealed class JcwJavaSourceGenerator /// Generates .java source content for all ACW types and returns them as in-memory /// (relativePath, content) pairs. No filesystem IO is performed. /// - public IReadOnlyList GenerateContent (IReadOnlyList types) + public IReadOnlyList GenerateContent (IReadOnlyList types, string? applicationJavaClass = null) { if (types is null) throw new ArgumentNullException (nameof (types)); var results = new List (); foreach (var type in types) { if (type.DoNotGenerateAcw || type.IsInterface) continue; using var writer = new StringWriter (); - Generate (type, writer); + Generate (type, writer, applicationJavaClass); results.Add (new GeneratedJavaSource (GetRelativePath (type), writer.ToString ())); } return results; @@ -61,11 +61,11 @@ public IReadOnlyList GenerateContent (IReadOnlyList /// Generates a single .java source file for the given type. /// - public void Generate (JavaPeerInfo type, TextWriter writer) + public void Generate (JavaPeerInfo type, TextWriter writer, string? applicationJavaClass = null) { writer.NewLine = "\n"; WritePackageDeclaration (type, writer); - WriteClassDeclaration (type, writer); + WriteClassDeclaration (type, writer, applicationJavaClass); WriteStaticInitializer (type, writer); WriteConstructors (type, writer); WriteFields (type, writer); @@ -97,7 +97,7 @@ static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer) } } - static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) + static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer, string? applicationJavaClass) { string abstractModifier = type.IsAbstract && !type.IsInterface ? "abstract " : ""; string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); @@ -106,7 +106,14 @@ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) // extends clause if (type.BaseJavaName != null) { - writer.WriteLine ($"\textends {JniSignatureHelper.JniNameToJavaName (type.BaseJavaName)}"); + string baseName = JniSignatureHelper.JniNameToJavaName (type.BaseJavaName); + // A user Application subclass normally extends android.app.Application, but when + // $(AndroidApplicationJavaClass) is set (e.g. android.support.multidex.MultiDexApplication + // when $(AndroidEnableMultiDex) is true) it must extend that class instead. + if (!applicationJavaClass.IsNullOrEmpty () && baseName == "android.app.Application") { + baseName = applicationJavaClass; + } + writer.WriteLine ($"\textends {baseName}"); } // implements clause — always includes IGCUserPeer, plus any implemented interfaces diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 95d0446b205..0070c2345e6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -51,7 +51,7 @@ public TrimmableTypeMapResult Execute ( } MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); - RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig), manifestConfig?.ApplicationJavaClass); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); @@ -60,7 +60,7 @@ public TrimmableTypeMapResult Execute ( : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); - var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); + var generatedJavaSources = GenerateJcwJavaSources (jcwPeers, manifestConfig?.ApplicationJavaClass); var appRegTypes = CollectApplicationRegistrationTypes (allPeers); if (appRegTypes.Count > 0) { @@ -289,15 +289,15 @@ static void MarkFrameworkAssemblyPeers (List allPeers, HashSet GenerateJcwJavaSources (List allPeers) + List GenerateJcwJavaSources (List allPeers, string? applicationJavaClass) { var jcwGenerator = new JcwJavaSourceGenerator (); - var sources = jcwGenerator.GenerateContent (allPeers); + var sources = jcwGenerator.GenerateContent (allPeers, applicationJavaClass); logger.LogGeneratedJcwFilesInfo (sources.Count); return sources.ToList (); } - internal void RootManifestReferencedTypes (List allPeers, XDocument? doc) + internal void RootManifestReferencedTypes (List allPeers, XDocument? doc, string? applicationJavaClass = null) { if (doc?.Root is not { } root) { return; @@ -357,6 +357,13 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen } } } else { + // $(AndroidApplicationJavaClass) (e.g. android.support.multidex.MultiDexApplication + // when $(AndroidEnableMultiDex) is true) is a Java framework type with no managed + // peer, so it is expected to be absent from the scanned assemblies — don't warn. + if (!applicationJavaClass.IsNullOrEmpty () && + string.Equals (name, ManifestNameResolver.Resolve (applicationJavaClass, packageName), StringComparison.Ordinal)) { + continue; + } logger.LogManifestReferencedTypeNotFoundWarning (name); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 81fc6457f35..1dd0d60081f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -121,6 +121,46 @@ public void Generate_ClickableView_UsesDotsForNestedInterfaceName () Assert.DoesNotContain ("View$OnClickListener", java); } + [Fact] + public void Generate_ApplicationSubclass_WithApplicationJavaClass_ExtendsThatClass () + { + // When $(AndroidApplicationJavaClass) is set (e.g. android.support.multidex.MultiDexApplication + // under $(AndroidEnableMultiDex)), an android.app.Application subclass must extend it. + var type = new JavaPeerInfo { + JavaName = "com/foxsports/test/CustomApp", + CompatJniName = "com/foxsports/test/CustomApp", + ManagedTypeName = "UnnamedProject.CustomApp", + ManagedTypeNamespace = "UnnamedProject", + ManagedTypeShortName = "CustomApp", + AssemblyName = "App", + BaseJavaName = "android/app/Application", + CannotRegisterInStaticConstructor = true, + }; + var generator = new JcwJavaSourceGenerator (); + using var writer = new StringWriter (); + generator.Generate (type, writer, "android.support.multidex.MultiDexApplication"); + var java = writer.ToString (); + Assert.Contains ("extends android.support.multidex.MultiDexApplication", java); + Assert.DoesNotContain ("extends android.app.Application", java); + } + + [Fact] + public void Generate_ApplicationSubclass_WithoutApplicationJavaClass_ExtendsApplication () + { + var type = new JavaPeerInfo { + JavaName = "com/foxsports/test/CustomApp", + CompatJniName = "com/foxsports/test/CustomApp", + ManagedTypeName = "UnnamedProject.CustomApp", + ManagedTypeNamespace = "UnnamedProject", + ManagedTypeShortName = "CustomApp", + AssemblyName = "App", + BaseJavaName = "android/app/Application", + CannotRegisterInStaticConstructor = true, + }; + var java = GenerateToString (type); + Assert.Contains ("extends android.app.Application", java); + } + } public class StaticInitializer diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index b3eba523be1..f79fcb3aa4a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -366,6 +366,34 @@ public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService")); } + [Fact] + public void RootManifestReferencedTypes_DoesNotWarnForApplicationJavaClass () + { + // android.support.multidex.MultiDexApplication (injected via $(AndroidApplicationJavaClass) + // when $(AndroidEnableMultiDex) is true) is a Java framework type with no managed peer, + // so it must not produce an XA4250 "not found in any scanned assembly" warning. + 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, "android.support.multidex.MultiDexApplication"); + + Assert.DoesNotContain (warnings, w => w.Contains ("MultiDexApplication")); + } + [Fact] public void RootManifestReferencedTypes_SkipsAlreadyUnconditional () {