diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs index a31be12bfef..86b825fc6bb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -176,7 +176,7 @@ internal static void ApplyApplicationProperties ( XElement app, Dictionary properties, IReadOnlyList allPeers, - Action? warn = null) + Action? warn = null) { PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true); @@ -191,7 +191,7 @@ static void ApplyTypeProperty ( IReadOnlyList allPeers, string propertyName, string xmlAttrName, - Action? warn) + Action? warn) { if (app.Attribute (AndroidNs + xmlAttrName) is not null) { return; @@ -213,7 +213,8 @@ static void ApplyTypeProperty ( } } - warn?.Invoke ($"Could not resolve {propertyName} type '{managedName}' to a Java peer for android:{xmlAttrName}."); + // Code 0 = no XA code assigned; the caller may silently ignore it. + warn?.Invoke (0, $"Could not resolve {propertyName} type '{managedName}' to a Java peer for android:{xmlAttrName}."); } internal static void AddInternetPermission (XElement manifest) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index a323bd885b4..1a4fb259ded 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -25,6 +25,9 @@ class ManifestGenerator "provider", }; + /// Warning code for library-manifest merge failures (maps to XA4302). + internal const int LibraryManifestMergeWarningCode = 4302; + int appInitOrder = 2000000000; public string PackageName { get; set; } = ""; @@ -41,7 +44,7 @@ class ManifestGenerator public bool ForceExtractNativeLibs { get; set; } public string? ManifestPlaceholders { get; set; } public string? ApplicationJavaClass { get; set; } - public Action? Warn { get; set; } + public Action? Warn { get; set; } public Action? WarnInvalidPlaceholder { get; set; } /// @@ -194,7 +197,7 @@ void MergeLibraryManifests (XElement manifest) try { libDoc = XDocument.Load (path); } catch (Exception ex) { - Warn?.Invoke ($"Unable to merge library manifest '{path}': {ex.Message}"); + Warn?.Invoke (LibraryManifestMergeWarningCode, $"Unable to merge library manifest '{path}': {ex.Message}"); continue; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index cdc5cf040fe..993cb949a1c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -12,6 +12,7 @@ public interface ITrimmableTypeMapLogger void LogGeneratedJcwFilesInfo (int sourceCount); void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); + void LogLibraryManifestMergeWarning (string message); void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 1aca9a1993a..f59e99e0334 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -149,6 +149,13 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes ForceExtractNativeLibs = forceDebuggable, ManifestPlaceholders = config.ManifestPlaceholders, ApplicationJavaClass = config.ApplicationJavaClass, + Warn = (code, message) => { + if (code == ManifestGenerator.LibraryManifestMergeWarningCode) + logger.LogLibraryManifestMergeWarning (message); + // Other codes (e.g. unresolvable type properties) are not yet assigned XA codes + // and are intentionally not surfaced here. + }, + LibraryManifests = config.LibraryManifests ?? [], }; var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 2e162d0d61b..d2a3c1e7122 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -46,4 +46,5 @@ public record ManifestConfig ( bool EmbedAssemblies = false, string? ManifestPlaceholders = null, string? CheckedBuild = null, - string? ApplicationJavaClass = null); + string? ApplicationJavaClass = null, + IReadOnlyList? LibraryManifests = null); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index d2b827016e1..5090ba7fa6a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -98,7 +98,8 @@ @@ -112,6 +113,9 @@ <_TypeMapFrameworkAssemblies Include="@(FrameworkAssemblies)" /> <_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)" Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" /> + + <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); + public void LogLibraryManifestMergeWarning (string message) => + log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, @@ -79,6 +81,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public string? MergedAndroidManifestOutput { get; set; } + /// + /// Absolute paths to extracted library (.aar) AndroidManifest.xml documents that must be + /// merged into the application manifest. Only populated on the legacy manifest-merger path; + /// manifestmerger.jar handles this downstream in the _ManifestMerger target. + /// + public string []? MergedManifestDocuments { get; set; } + public string? PackageName { get; set; } public string? ApplicationLabel { get; set; } public string? VersionCode { get; set; } @@ -184,7 +193,8 @@ public override bool RunTask () EmbedAssemblies: EmbedAssemblies, ManifestPlaceholders: ManifestPlaceholders, CheckedBuild: CheckedBuild, - ApplicationJavaClass: ApplicationJavaClass); + ApplicationJavaClass: ApplicationJavaClass, + LibraryManifests: MergedManifestDocuments); } var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index d1442c2c1b7..56c1b2fa3f6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -1436,4 +1436,89 @@ public void AssemblyLevel_ApplicationManageSpaceActivity () Assert.NotNull (app); Assert.Equal ("com.example.app.ManageActivity", (string?)app?.Attribute (AndroidNs + "manageSpaceActivity")); } + + [Fact] + public void LibraryManifests_MergedWithApplicationIdAndRelativeNamesResolved () + { + // Mirrors ManifestTest.MergeLibraryManifest: a library (.aar) manifest is merged into the + // app manifest, ${applicationId} resolves to the app package, and relative component names + // (".Type") are qualified with the library's own package attribute. + var libManifest = Path.Combine (Path.GetTempPath (), $"lib-manifest-{Path.GetRandomFileName ()}.xml"); + File.WriteAllText (libManifest, """ + + + + + + + + + + + + """); + try { + var gen = CreateDefaultGenerator (); + gen.PackageName = "com.xamarin.manifest"; + gen.LibraryManifests = [libManifest]; + var template = ParseTemplate (""" + + + + + + """); + + var doc = GenerateAndLoad (gen, template: template); + + // ${applicationId} resolves to the app package on the merged permission. + var permission = doc.Root?.Elements ("permission") + .FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.manifest.permission.C2D_MESSAGE"); + Assert.NotNull (permission); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + + // Relative ".Type" names are qualified with the library package (com.xamarin.test). + var activity = app?.Elements ("activity") + .FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.test.signin.internal.SignInHubActivity"); + Assert.NotNull (activity); + + var provider = app?.Elements ("provider") + .FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.test.internal.FacebookInitProvider"); + Assert.NotNull (provider); + // authorities uses ${applicationId} -> app package. + Assert.Equal ("com.xamarin.manifest.FacebookInitProvider", (string?) provider?.Attribute (AndroidNs + "authorities")); + + // The two identical meta-data elements collapse to a single element. + var versionMeta = app?.Elements ("meta-data") + .Where (e => (string?) e.Attribute (AttName) == "android.support.VERSION") + .ToList (); + Assert.NotNull (versionMeta); + Assert.Single (versionMeta); + } finally { + File.Delete (libManifest); + } + } + + [Fact] + public void LibraryManifests_MissingFileIgnored () + { + // A non-existent library manifest path is skipped without throwing. + var gen = CreateDefaultGenerator (); + gen.LibraryManifests = [Path.Combine (Path.GetTempPath (), $"does-not-exist-{Path.GetRandomFileName ()}.xml")]; + var template = ParseTemplate (""" + + + + + + """); + + var doc = GenerateAndLoad (gen, template: template); + Assert.NotNull (doc.Root?.Element ("application")); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index a60a6343e72..a17715f8d68 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -36,6 +36,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma 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."); + public void LogLibraryManifestMergeWarning (string message) => + warnings?.Add (message); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName,