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/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 27806a679bd..5b2d6204eb5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -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/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index ea1cd664a96..94e046c732f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -10,4 +10,6 @@ public interface ITrimmableTypeMapLogger void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount); void LogGeneratedTypeMapAssembliesInfo (int assemblyCount); void LogGeneratedJcwFilesInfo (int sourceCount); + void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); + void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index aed62453042..eff38fd1d51 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -69,16 +69,22 @@ 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 /// 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/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 0697f31ea54..edb0171758f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -38,6 +38,8 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult ([], [], allPeers); } + RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) @@ -139,4 +141,148 @@ List GenerateJcwJavaSources (List allPeers) 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) { + if (deferredRegistrationNames.Contains (name)) { + peer.CannotRegisterInStaticConstructor = true; + } + + if (!peer.IsUnconditional) { + peer.IsUnconditional = true; + logger.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); + } + } + } else { + logger.LogManifestReferencedTypeNotFoundWarning (name); + } + } + } + + 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 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) + { + return name switch { + _ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name, + _ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name, + _ => 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 715eac0b495..120feb3c59a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -33,6 +33,10 @@ 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 LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); } public override string TaskPrefix => "GTT"; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index f76d95f246b..096fd7d9f34 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -123,11 +123,60 @@ public void Execute_ParsesTargetFrameworkVersion (string tfv) Assert.IsTrue (task.Execute (), $"Task should succeed with TargetFrameworkVersion='{tfv}'."); } + [Test] + public void Execute_ManifestPlaceholdersAreResolvedForRooting () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var manifestTemplate = Path.Combine (Root, path, "AndroidManifest.xml"); + var mergedManifest = Path.Combine (Root, path, "obj", "android", "AndroidManifest.xml"); + var applicationRegistration = Path.Combine (Root, path, "src", "net", "dot", "android", "ApplicationRegistration.java"); + var warnings = new List (); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var manifestDirectory = Path.GetDirectoryName (manifestTemplate); + if (manifestDirectory is null) { + Assert.Fail ("Could not determine manifest template directory."); + } + Directory.CreateDirectory (manifestDirectory); + File.WriteAllText (manifestTemplate, """ + + + + + + """); + + var task = CreateTask (new [] { monoAndroidItem }, outputDir, javaDir, warnings: warnings); + task.ManifestTemplate = manifestTemplate; + task.MergedAndroidManifestOutput = mergedManifest; + task.ApplicationRegistrationOutputFile = applicationRegistration; + task.PackageName = "android.app"; + task.AndroidApiLevel = "35"; + task.SupportedOSPlatformVersion = "21"; + task.RuntimeProviderJavaName = "mono.MonoRuntimeProvider"; + task.ManifestPlaceholders = "applicationId=android.app"; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + FileAssert.Exists (applicationRegistration); + + var registrationText = File.ReadAllText (applicationRegistration); + StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Application.class);", registrationText); + StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Instrumentation.class);", registrationText); + Assert.IsFalse (warnings.Any (w => w.Code == "XA4250"), "Resolved placeholder-based manifest references should not log XA4250."); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, - IList? messages = null, string tfv = "v11.0") + IList? messages = null, IList? warnings = null, string tfv = "v11.0") { return new GenerateTrimmableTypeMap { - BuildEngine = new MockBuildEngine (TestContext.Out, messages: messages), + BuildEngine = new MockBuildEngine (TestContext.Out, warnings: warnings, messages: messages), ResolvedAssemblies = assemblies, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c07340ae981..54efaf3f6ef 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -12,7 +12,7 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); - sealed class TestTrimmableTypeMapLogger (List logMessages) : ITrimmableTypeMapLogger + sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : ITrimmableTypeMapLogger { public void LogNoJavaPeerTypesFound () => logMessages.Add ("No Java peer types found, skipping typemap generation."); @@ -30,6 +30,10 @@ 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 LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); } [Fact] @@ -99,8 +103,193 @@ public void Execute_JavaSourcesHaveCorrectStructure () Assert.Contains ("class ", source.Content); } + [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_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 () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)