Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Globalization;
Expand All @@ -21,63 +19,73 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app,
{
var existingPermissions = new HashSet<string> (
manifest.Elements ("permission").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());
var existingPermissionGroups = new HashSet<string> (
manifest.Elements ("permission-group").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());
var existingPermissionTrees = new HashSet<string> (
manifest.Elements ("permission-tree").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());
var existingUsesPermissions = new HashSet<string> (
manifest.Elements ("uses-permission").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());

// <permission> 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);
}

// <permission-group> 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);
}

// <permission-tree> 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);
}

// <uses-permission> 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);
}

// <uses-feature> elements
var existingFeatures = new HashSet<string> (
manifest.Elements ("uses-feature").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());
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"));
Expand Down Expand Up @@ -153,11 +161,55 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app,
}
manifest.Add (element);
}

// <supports-gl-texture> elements
var existingGLTextures = new HashSet<string> (
manifest.Elements ("supports-gl-texture").Select (e => (string?)e.Attribute (AttName)).OfType<string> ());
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<string, object?> properties)
internal static void ApplyApplicationProperties (
XElement app,
Dictionary<string, object?> properties,
IReadOnlyList<JavaPeerInfo> allPeers)
{
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");
ApplyTypeProperty (app, properties, allPeers, "ManageSpaceActivity", "manageSpaceActivity");
}

static void ApplyTypeProperty (
XElement app,
Dictionary<string, object?> properties,
IReadOnlyList<JavaPeerInfo> allPeers,
string propertyName,
string xmlAttrName)
{
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;
}
}
}

internal static void AddInternetPermission (XElement manifest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public IList<string> Generate (

// Apply assembly-level [Application] properties
if (assemblyInfo.ApplicationProperties is not null) {
AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties);
AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties, allPeers);
}

var existingTypes = new HashSet<string> (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,27 @@ static bool TryGetNamedArgument<T> (CustomAttributeValue<string> value, string a
/// <summary>
/// Scans assembly-level custom attributes for manifest-related data.
/// </summary>
static readonly HashSet<string> 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;
}

Expand Down Expand Up @@ -388,6 +402,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<string, object?> (StringComparer.Ordinal);
foreach (var kvp in props) {
Expand Down Expand Up @@ -426,7 +445,8 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info)
static UsesPermissionInfo CreateUsesPermissionInfo (string name, Dictionary<string, object?> 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<string, object?> props)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal sealed class AssemblyManifestInfo
public List<UsesConfigurationInfo> UsesConfigurations { get; } = [];
public List<MetaDataInfo> MetaData { get; } = [];
public List<PropertyInfo> Properties { get; } = [];
public List<SupportsGLTextureInfo> SupportsGLTextures { get; } = [];

public Dictionary<string, object?>? ApplicationProperties { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<string, JavaPeerInfo> results
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
ComponentAttribute = ToComponentInfo (attrInfo, typeDef, index),
ComponentAttribute = ToComponentInfo (attrInfo),
};

results [fullName] = peer;
Expand Down Expand Up @@ -1487,7 +1487,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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Microsoft.Android.Sdk.TrimmableTypeMap;

internal sealed record SupportsGLTextureInfo
{
public required string Name { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ internal sealed record UsesPermissionInfo
{
public required string Name { get; init; }
public int? MaxSdkVersion { get; init; }
public string? UsesPermissionFlags { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ static string TestFixtureAssemblyPath {
}

static readonly Lazy<(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => {
var scanner = new JavaPeerScanner ();
using var scanner = new JavaPeerScanner ();
var peers = scanner.Scan (new [] { TestFixtureAssemblyPath });
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
return (peers, manifestInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object?> {
["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<string, object?> {
["BackupAgent"] = "MyApp.MyBackupAgent",
};
var peers = new List<JavaPeerInfo> {
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<string, object?> {
["ManageSpaceActivity"] = "MyApp.ManageActivity",
};
var peers = new List<JavaPeerInfo> {
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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Loading