diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs index 8ba00724f87..a31be12bfef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Globalization; @@ -21,18 +19,23 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, { var existingPermissions = new HashSet ( manifest.Elements ("permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); + var existingPermissionGroups = new HashSet ( + manifest.Elements ("permission-group").Select (e => (string?)e.Attribute (AttName)).OfType ()); + var existingPermissionTrees = new HashSet ( + manifest.Elements ("permission-tree").Select (e => (string?)e.Attribute (AttName)).OfType ()); var existingUsesPermissions = new HashSet ( manifest.Elements ("uses-permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); // elements foreach (var perm in info.Permissions) { - if (string.IsNullOrEmpty (perm.Name) || existingPermissions.Contains (perm.Name)) { + if (string.IsNullOrEmpty (perm.Name) || !existingPermissions.Add (perm.Name)) { continue; } var element = new XElement ("permission", new XAttribute (AttName, perm.Name)); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Description", "description"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "RoundIcon", "roundIcon"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "PermissionGroup", "permissionGroup"); PropertyMapper.MapDictionaryEnumProperty (element, perm.Properties, "ProtectionLevel", "protectionLevel", AndroidEnumConverter.ProtectionToString); manifest.Add (element); @@ -40,36 +43,41 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, // elements foreach (var pg in info.PermissionGroups) { - if (string.IsNullOrEmpty (pg.Name)) { + if (string.IsNullOrEmpty (pg.Name) || !existingPermissionGroups.Add (pg.Name)) { continue; } var element = new XElement ("permission-group", new XAttribute (AttName, pg.Name)); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Description", "description"); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, pg.Properties, "RoundIcon", "roundIcon"); manifest.Add (element); } // elements foreach (var pt in info.PermissionTrees) { - if (string.IsNullOrEmpty (pt.Name)) { + if (string.IsNullOrEmpty (pt.Name) || !existingPermissionTrees.Add (pt.Name)) { continue; } var element = new XElement ("permission-tree", new XAttribute (AttName, pt.Name)); PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, pt.Properties, "RoundIcon", "roundIcon"); manifest.Add (element); } // elements foreach (var up in info.UsesPermissions) { - if (string.IsNullOrEmpty (up.Name) || existingUsesPermissions.Contains (up.Name)) { + if (string.IsNullOrEmpty (up.Name) || !existingUsesPermissions.Add (up.Name)) { continue; } var element = new XElement ("uses-permission", new XAttribute (AttName, up.Name)); if (up.MaxSdkVersion.HasValue) { element.SetAttributeValue (AndroidNs + "maxSdkVersion", up.MaxSdkVersion.Value.ToString (CultureInfo.InvariantCulture)); } + if (!string.IsNullOrEmpty (up.UsesPermissionFlags)) { + element.SetAttributeValue (AndroidNs + "usesPermissionFlags", up.UsesPermissionFlags); + } manifest.Add (element); } @@ -77,7 +85,7 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, var existingFeatures = new HashSet ( manifest.Elements ("uses-feature").Select (e => (string?)e.Attribute (AttName)).OfType ()); foreach (var uf in info.UsesFeatures) { - if (uf.Name is not null && !existingFeatures.Contains (uf.Name)) { + if (uf.Name is not null && existingFeatures.Add (uf.Name)) { var element = new XElement ("uses-feature", new XAttribute (AttName, uf.Name), new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false")); @@ -153,11 +161,59 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, } manifest.Add (element); } + + // elements + var existingGLTextures = new HashSet ( + manifest.Elements ("supports-gl-texture").Select (e => (string?)e.Attribute (AttName)).OfType ()); + foreach (var gl in info.SupportsGLTextures) { + if (existingGLTextures.Add (gl.Name)) { + manifest.Add (new XElement ("supports-gl-texture", new XAttribute (AttName, gl.Name))); + } + } } - internal static void ApplyApplicationProperties (XElement app, Dictionary properties) + internal static void ApplyApplicationProperties ( + XElement app, + Dictionary properties, + IReadOnlyList allPeers, + Action? warn = null) { PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true); + + // BackupAgent and ManageSpaceActivity are Type properties — resolve managed type names to JNI names + ApplyTypeProperty (app, properties, allPeers, "BackupAgent", "backupAgent", warn); + ApplyTypeProperty (app, properties, allPeers, "ManageSpaceActivity", "manageSpaceActivity", warn); + } + + static void ApplyTypeProperty ( + XElement app, + Dictionary properties, + IReadOnlyList allPeers, + string propertyName, + string xmlAttrName, + Action? warn) + { + if (app.Attribute (AndroidNs + xmlAttrName) is not null) { + return; + } + if (!properties.TryGetValue (propertyName, out var value) || value is not string managedName || managedName.Length == 0) { + return; + } + + // Strip assembly qualification if present (e.g., "MyApp.MyAgent, MyAssembly") + var commaIndex = managedName.IndexOf (','); + if (commaIndex > 0) { + managedName = managedName.Substring (0, commaIndex).Trim (); + } + + foreach (var peer in allPeers) { + if (peer.ManagedTypeName == managedName) { + app.SetAttributeValue (AndroidNs + xmlAttrName, peer.JavaName.Replace ('/', '.')); + return; + } + } + + warn?.Invoke ($"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 1bea7928e30..6bb10a43686 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -33,6 +33,7 @@ class ManifestGenerator public bool ForceExtractNativeLibs { get; set; } public string? ManifestPlaceholders { get; set; } public string? ApplicationJavaClass { get; set; } + public Action? Warn { get; set; } /// /// Generates the merged manifest from an optional pre-loaded template and writes it to . @@ -55,7 +56,7 @@ public IList Generate ( // Apply assembly-level [Application] properties if (assemblyInfo.ApplicationProperties is not null) { - AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties); + AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties, allPeers, Warn); } var existingTypes = new HashSet ( diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index e63dcc63202..190fd095d8c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -343,13 +343,27 @@ static bool TryGetNamedArgument (CustomAttributeValue value, string a /// /// Scans assembly-level custom attributes for manifest-related data. /// + static readonly HashSet KnownAssemblyAttributes = new (StringComparer.Ordinal) { + "PermissionAttribute", + "PermissionGroupAttribute", + "PermissionTreeAttribute", + "UsesPermissionAttribute", + "UsesFeatureAttribute", + "UsesLibraryAttribute", + "UsesConfigurationAttribute", + "MetaDataAttribute", + "PropertyAttribute", + "SupportsGLTextureAttribute", + "ApplicationAttribute", + }; + internal void ScanAssemblyAttributes (AssemblyManifestInfo info) { var asmDef = Reader.GetAssemblyDefinition (); foreach (var caHandle in asmDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName is null) { + if (attrName is null || !KnownAssemblyAttributes.Contains (attrName)) { continue; } @@ -383,6 +397,11 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) case "PropertyAttribute": info.Properties.Add (CreatePropertyInfo (name, props)); break; + case "SupportsGLTextureAttribute": + if (name.Length > 0) { + info.SupportsGLTextures.Add (new SupportsGLTextureInfo { Name = name }); + } + break; case "ApplicationAttribute": info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal); foreach (var kvp in props) { @@ -421,7 +440,8 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) static UsesPermissionInfo CreateUsesPermissionInfo (string name, Dictionary props) { int? maxSdk = props.TryGetValue ("MaxSdkVersion", out var v) && v is int max ? max : null; - return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk }; + string? flags = props.TryGetValue ("UsesPermissionFlags", out var f) && f is string s ? s : null; + return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk, UsesPermissionFlags = flags }; } static UsesFeatureInfo CreateUsesFeatureInfo (string name, Dictionary props) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs index 068aa226709..ea917f72867 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs @@ -13,6 +13,7 @@ internal sealed class AssemblyManifestInfo public List UsesConfigurations { get; } = []; public List MetaData { get; } = []; public List Properties { get; } = []; + public List SupportsGLTextures { get; } = []; public Dictionary? ApplicationProperties { get; set; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 4b298352b75..99d3c000f99 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -246,7 +246,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, - ComponentAttribute = ToComponentInfo (attrInfo, typeDef, index), + ComponentAttribute = ToComponentInfo (attrInfo), }; results [fullName] = peer; @@ -1553,7 +1553,7 @@ static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, } } - static ComponentInfo? ToComponentInfo (TypeAttributeInfo? attrInfo, TypeDefinition typeDef, AssemblyIndex index) + static ComponentInfo? ToComponentInfo (TypeAttributeInfo? attrInfo) { if (attrInfo is null) { return null; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs new file mode 100644 index 00000000000..f1db452a2a8 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +internal sealed record SupportsGLTextureInfo +{ + public required string Name { get; init; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs index d11df2d8a3f..bf09a3dfee2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs @@ -4,4 +4,5 @@ internal sealed record UsesPermissionInfo { public required string Name { get; init; } public int? MaxSdkVersion { get; init; } + public string? UsesPermissionFlags { get; init; } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 5cf9e1cabe9..8d33d20deeb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -23,7 +23,7 @@ static string TestFixtureAssemblyPath { } static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { - var scanner = new JavaPeerScanner (); + using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 2653286fd20..72afe53a473 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -616,4 +616,103 @@ public void ConfigChanges_EnumConversion () Assert.True (parts.Contains ("screenSize"), "configChanges should contain 'screenSize'"); Assert.Equal (3, parts.Length); } + + [Fact] + public void AssemblyLevel_SupportsGLTexture () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.SupportsGLTextures.Add (new SupportsGLTextureInfo { Name = "GL_OES_compressed_ETC1_RGB8_texture" }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var element = doc.Root?.Elements ("supports-gl-texture") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "GL_OES_compressed_ETC1_RGB8_texture"); + Assert.NotNull (element); + } + + [Fact] + public void AssemblyLevel_UsesPermissionFlags () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.UsesPermissions.Add (new UsesPermissionInfo { + Name = "android.permission.POST_NOTIFICATIONS", + UsesPermissionFlags = "neverForLocation", + }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("uses-permission") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "android.permission.POST_NOTIFICATIONS"); + Assert.NotNull (perm); + Assert.Equal ("neverForLocation", (string?)perm?.Attribute (AndroidNs + "usesPermissionFlags")); + } + + [Fact] + public void AssemblyLevel_PermissionRoundIcon () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.Permissions.Add (new PermissionInfo { + Name = "com.example.MY_PERMISSION", + Properties = new Dictionary { + ["RoundIcon"] = "@mipmap/ic_launcher_round", + }, + }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("permission") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "com.example.MY_PERMISSION"); + Assert.NotNull (perm); + Assert.Equal ("@mipmap/ic_launcher_round", (string?)perm?.Attribute (AndroidNs + "roundIcon")); + } + + [Fact] + public void AssemblyLevel_ApplicationBackupAgent () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.ApplicationProperties = new Dictionary { + ["BackupAgent"] = "MyApp.MyBackupAgent", + }; + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/app/MyBackupAgent", + CompatJniName = "com/example/app/MyBackupAgent", + ManagedTypeName = "MyApp.MyBackupAgent", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyBackupAgent", + AssemblyName = "TestApp", + }, + }; + + var doc = GenerateAndLoad (gen, peers: peers, assemblyInfo: info); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("com.example.app.MyBackupAgent", (string?)app?.Attribute (AndroidNs + "backupAgent")); + } + + [Fact] + public void AssemblyLevel_ApplicationManageSpaceActivity () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.ApplicationProperties = new Dictionary { + ["ManageSpaceActivity"] = "MyApp.ManageActivity", + }; + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/app/ManageActivity", + CompatJniName = "com/example/app/ManageActivity", + ManagedTypeName = "MyApp.ManageActivity", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "ManageActivity", + AssemblyName = "TestApp", + }, + }; + + var doc = GenerateAndLoad (gen, peers: peers, assemblyInfo: info); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("com.example.app.ManageActivity", (string?)app?.Attribute (AndroidNs + "manageSpaceActivity")); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs index d1f9d04f7df..2910773d76a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs @@ -66,4 +66,21 @@ public void MetaData_ConstructorArgAndNamedArg () Assert.NotNull (meta); Assert.Equal ("test-value", meta.Value); } + + [Fact] + public void UsesPermission_Flags () + { + var info = ScanAssemblyManifestInfo (); + var perm = info.UsesPermissions.FirstOrDefault (p => p.Name == "android.permission.POST_NOTIFICATIONS"); + Assert.NotNull (perm); + Assert.Equal ("neverForLocation", perm.UsesPermissionFlags); + } + + [Fact] + public void SupportsGLTexture_ConstructorArg () + { + var info = ScanAssemblyManifestInfo (); + var gl = info.SupportsGLTextures.FirstOrDefault (g => g.Name == "GL_OES_compressed_ETC1_RGB8_texture"); + Assert.NotNull (gl); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs index f86a255829d..8d10610f08a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs @@ -4,6 +4,8 @@ [assembly: UsesFeature ("android.hardware.camera.autofocus", Required = false)] [assembly: UsesFeature (GLESVersion = 0x00020000)] [assembly: UsesPermission ("android.permission.INTERNET")] +[assembly: UsesPermission ("android.permission.POST_NOTIFICATIONS", UsesPermissionFlags = "neverForLocation")] [assembly: UsesLibrary ("org.apache.http.legacy")] [assembly: UsesLibrary ("com.example.optional", false)] [assembly: MetaData ("com.example.key", Value = "test-value")] +[assembly: SupportsGLTexture ("GL_OES_compressed_ETC1_RGB8_texture")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 1e19e314e32..332abe00a19 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -110,6 +110,7 @@ public UsesPermissionAttribute () { } public string? Name { get; set; } public int MaxSdkVersion { get; set; } + public string? UsesPermissionFlags { get; set; } } [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] @@ -145,6 +146,14 @@ public sealed class IntentFilterAttribute : Attribute public string [] Actions { get; } public string []? Categories { get; set; } } + + [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class SupportsGLTextureAttribute : Attribute + { + public SupportsGLTextureAttribute (string name) => Name = name; + + public string Name { get; private set; } + } } namespace Android.Content