diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
index 938c60fd10a..1bea7928e30 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -37,16 +35,16 @@ class ManifestGenerator
public string? ApplicationJavaClass { get; set; }
///
- /// Generates the merged manifest and writes it to .
+ /// Generates the merged manifest from an optional pre-loaded template and writes it to .
/// Returns the list of additional content provider names (for ApplicationRegistration.java).
///
public IList Generate (
- string? manifestTemplatePath,
+ XDocument? manifestTemplate,
IReadOnlyList allPeers,
AssemblyManifestInfo assemblyInfo,
string outputPath)
{
- var doc = LoadOrCreateManifest (manifestTemplatePath);
+ var doc = manifestTemplate ?? CreateDefaultManifest ();
var manifest = doc.Root;
if (manifest is null) {
throw new InvalidOperationException ("Manifest document has no root element.");
@@ -134,12 +132,8 @@ public IList Generate (
return providerNames;
}
- XDocument LoadOrCreateManifest (string? templatePath)
+ XDocument CreateDefaultManifest ()
{
- if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) {
- return XDocument.Load (templatePath);
- }
-
return new XDocument (
new XDeclaration ("1.0", "utf-8", null),
new XElement ("manifest",
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
deleted file mode 100644
index 166223cc44f..00000000000
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-#nullable enable
-
-using System.Collections.Generic;
-
-namespace Microsoft.Android.Sdk.TrimmableTypeMap;
-
-public enum ComponentKind
-{
- Activity,
- Service,
- BroadcastReceiver,
- ContentProvider,
- Application,
- Instrumentation,
-}
-
-public class ComponentInfo
-{
- public bool HasPublicDefaultConstructor { get; set; }
- public ComponentKind Kind { get; set; }
- public Dictionary Properties { get; set; } = new Dictionary ();
- public IReadOnlyList IntentFilters { get; set; } = [];
- public IReadOnlyList MetaData { get; set; } = [];
-}
-
-public class IntentFilterInfo
-{
- public IReadOnlyList Actions { get; set; } = [];
- public IReadOnlyList Categories { get; set; } = [];
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class MetaDataInfo
-{
- public string Name { get; set; } = "";
- public string? Value { get; set; }
- public string? Resource { get; set; }
-}
-
-public class PermissionInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class PermissionGroupInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class PermissionTreeInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class UsesPermissionInfo
-{
- public string Name { get; set; } = "";
- public int? MaxSdkVersion { get; set; }
-}
-
-public class UsesFeatureInfo
-{
- public string? Name { get; set; }
- public bool Required { get; set; }
- public int GLESVersion { get; set; }
-}
-
-public class UsesLibraryInfo
-{
- public string Name { get; set; } = "";
- public bool Required { get; set; }
-}
-
-public class UsesConfigurationInfo
-{
- public bool ReqFiveWayNav { get; set; }
- public bool ReqHardKeyboard { get; set; }
- public string? ReqKeyboardType { get; set; }
- public string? ReqNavigation { get; set; }
- public string? ReqTouchScreen { get; set; }
-}
-
-public class PropertyInfo
-{
- public string Name { get; set; } = "";
- public string? Value { get; set; }
- public string? Resource { get; set; }
-}
-
-public class AssemblyManifestInfo
-{
- public List Permissions { get; set; } = new List ();
- public List PermissionGroups { get; set; } = new List ();
- public List PermissionTrees { get; set; } = new List ();
- public List UsesPermissions { get; set; } = new List ();
- public List UsesFeatures { get; set; } = new List ();
- public List UsesLibraries { get; set; } = new List ();
- public List UsesConfigurations { get; set; } = new List ();
- public List MetaData { get; set; } = new List ();
- public List Properties { get; set; } = new List ();
- public Dictionary? ApplicationProperties { get; set; }
-}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
index d364200e460..2d4803c8983 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -83,6 +83,12 @@ void Build ()
RegisterInfo? registerInfo = null;
TypeAttributeInfo? attrInfo = null;
+ // Collect intent filters and metadata separately to avoid ordering issues:
+ // if [IntentFilter] appears before [Activity], we must not create attrInfo
+ // with the wrong AttributeName.
+ List? intentFilters = null;
+ List? metaData = null;
+
foreach (var caHandle in typeDef.GetCustomAttributes ()) {
var ca = Reader.GetCustomAttribute (caHandle);
var attrName = GetCustomAttributeName (ca, Reader);
@@ -98,14 +104,34 @@ void Build ()
// [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner
} else if (IsKnownComponentAttribute (attrName)) {
attrInfo ??= CreateTypeAttributeInfo (attrName);
- var name = TryGetNameProperty (ca);
+ var value = DecodeAttribute (ca);
+
+ // Capture all named properties
+ foreach (var named in value.NamedArguments) {
+ if (named.Name is not null) {
+ attrInfo.Properties [named.Name] = named.Value;
+ }
+ }
+
+ var name = TryGetNameFromDecodedAttribute (value);
if (name is not null) {
attrInfo.JniName = name.Replace ('.', '/');
}
if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
- applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent");
- applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity");
+ if (TryGetNamedArgument (value, "BackupAgent", out var backupAgent)) {
+ applicationAttributeInfo.BackupAgent = backupAgent;
+ }
+ if (TryGetNamedArgument (value, "ManageSpaceActivity", out var manageSpace)) {
+ applicationAttributeInfo.ManageSpaceActivity = manageSpace;
+ }
}
+ } else if (attrName == "IntentFilterAttribute") {
+ intentFilters ??= new List ();
+ intentFilters.Add (ParseIntentFilterAttribute (ca));
+ } else if (attrName == "MetaDataAttribute") {
+ metaData ??= new List ();
+ var (mdName, mdProps) = ParseNameAndProperties (ca);
+ metaData.Add (CreateMetaDataInfo (mdName, mdProps));
} else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) {
// Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName])
var name = TryGetNameProperty (ca);
@@ -116,6 +142,16 @@ void Build ()
}
}
+ // Attach collected intent filters and metadata to the component attribute
+ if (attrInfo is not null) {
+ if (intentFilters is not null) {
+ attrInfo.IntentFilters.AddRange (intentFilters);
+ }
+ if (metaData is not null) {
+ attrInfo.MetaData.AddRange (metaData);
+ }
+ }
+
return (registerInfo, attrInfo);
}
@@ -239,6 +275,14 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value)
}
var value = DecodeAttribute (ca);
+ return TryGetNameFromDecodedAttribute (value);
+ }
+
+ static string? TryGetNameFromDecodedAttribute (CustomAttributeValue value)
+ {
+ if (TryGetNamedArgument (value, "Name", out var name) && !string.IsNullOrEmpty (name)) {
+ return name;
+ }
// Fall back to first constructor argument (e.g., [CustomJniName("...")])
if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) {
@@ -248,6 +292,47 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value)
return null;
}
+ IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+
+ // First ctor argument is string[] actions
+ var actions = new List ();
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is IReadOnlyCollection> actionArgs) {
+ foreach (var arg in actionArgs) {
+ if (arg.Value is string action) {
+ actions.Add (action);
+ }
+ }
+ }
+
+ var categories = new List ();
+ // Categories is a string[] property — the SRM decoder sees it as
+ // IReadOnlyCollection>, not string.
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Categories" && named.Value is IReadOnlyCollection> catArgs) {
+ foreach (var arg in catArgs) {
+ if (arg.Value is string cat) {
+ categories.Add (cat);
+ }
+ }
+ }
+ }
+
+ var properties = new Dictionary (StringComparer.Ordinal);
+ foreach (var named in value.NamedArguments) {
+ if (named.Name is not null && named.Name != "Categories") {
+ properties [named.Name] = named.Value;
+ }
+ }
+
+ return new IntentFilterInfo {
+ Actions = actions,
+ Categories = categories,
+ Properties = properties,
+ };
+ }
+
static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull
{
foreach (var named in value.NamedArguments) {
@@ -260,6 +345,136 @@ static bool TryGetNamedArgument (CustomAttributeValue value, string a
return false;
}
+ ///
+ /// Scans assembly-level custom attributes for manifest-related data.
+ ///
+ 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) {
+ continue;
+ }
+
+ var (name, props) = ParseNameAndProperties (ca);
+
+ switch (attrName) {
+ case "PermissionAttribute":
+ info.Permissions.Add (new PermissionInfo { Name = name, Properties = props });
+ break;
+ case "PermissionGroupAttribute":
+ info.PermissionGroups.Add (new PermissionGroupInfo { Name = name, Properties = props });
+ break;
+ case "PermissionTreeAttribute":
+ info.PermissionTrees.Add (new PermissionTreeInfo { Name = name, Properties = props });
+ break;
+ case "UsesPermissionAttribute":
+ info.UsesPermissions.Add (CreateUsesPermissionInfo (name, props));
+ break;
+ case "UsesFeatureAttribute":
+ info.UsesFeatures.Add (CreateUsesFeatureInfo (name, props));
+ break;
+ case "UsesLibraryAttribute":
+ info.UsesLibraries.Add (CreateUsesLibraryInfo (name, props));
+ break;
+ case "UsesConfigurationAttribute":
+ info.UsesConfigurations.Add (CreateUsesConfigurationInfo (props));
+ break;
+ case "MetaDataAttribute":
+ info.MetaData.Add (CreateMetaDataInfo (name, props));
+ break;
+ case "PropertyAttribute":
+ info.Properties.Add (CreatePropertyInfo (name, props));
+ break;
+ case "ApplicationAttribute":
+ info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal);
+ foreach (var kvp in props) {
+ info.ApplicationProperties [kvp.Key] = kvp.Value;
+ }
+ break;
+ }
+ }
+ }
+
+ (string name, Dictionary props) ParseNameAndProperties (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string name = "";
+ var props = new Dictionary (StringComparer.Ordinal);
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName) {
+ name = ctorName;
+ }
+ // Handle 2-arg ctors like UsesLibrary(string, bool) — store extra ctor args in props
+ for (int i = 1; i < value.FixedArguments.Length; i++) {
+ if (value.FixedArguments [i].Value is bool boolVal) {
+ props ["Required"] = boolVal;
+ }
+ }
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string n) {
+ name = n;
+ }
+ if (named.Name is not null) {
+ props [named.Name] = named.Value;
+ }
+ }
+ return (name, props);
+ }
+
+ 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 };
+ }
+
+ static UsesFeatureInfo CreateUsesFeatureInfo (string name, Dictionary props)
+ {
+ var required = !props.TryGetValue ("Required", out var r) || r is not bool req || req;
+ var glesVersion = props.TryGetValue ("GLESVersion", out var g) && g is int gles ? gles : 0;
+ return new UsesFeatureInfo {
+ Name = name.Length > 0 ? name : null,
+ GLESVersion = glesVersion,
+ Required = required,
+ };
+ }
+
+ static UsesLibraryInfo CreateUsesLibraryInfo (string name, Dictionary props)
+ {
+ var required = !props.TryGetValue ("Required", out var r) || r is not bool req || req;
+ return new UsesLibraryInfo { Name = name, Required = required };
+ }
+
+ static UsesConfigurationInfo CreateUsesConfigurationInfo (Dictionary props)
+ {
+ return new UsesConfigurationInfo {
+ ReqFiveWayNav = props.TryGetValue ("ReqFiveWayNav", out var v1) && v1 is bool b1 && b1,
+ ReqHardKeyboard = props.TryGetValue ("ReqHardKeyboard", out var v2) && v2 is bool b2 && b2,
+ ReqKeyboardType = props.TryGetValue ("ReqKeyboardType", out var v3) && v3 is string s3 ? s3 : null,
+ ReqNavigation = props.TryGetValue ("ReqNavigation", out var v4) && v4 is string s4 ? s4 : null,
+ ReqTouchScreen = props.TryGetValue ("ReqTouchScreen", out var v5) && v5 is string s5 ? s5 : null,
+ };
+ }
+
+ static MetaDataInfo CreateMetaDataInfo (string name, Dictionary props)
+ {
+ return new MetaDataInfo {
+ Name = name,
+ Value = props.TryGetValue ("Value", out var v) && v is string val ? val : null,
+ Resource = props.TryGetValue ("Resource", out var r) && r is string res ? res : null,
+ };
+ }
+
+ static PropertyInfo CreatePropertyInfo (string name, Dictionary props)
+ {
+ return new PropertyInfo {
+ Name = name,
+ Value = props.TryGetValue ("Value", out var v) && v is string val ? val : null,
+ Resource = props.TryGetValue ("Resource", out var r) && r is string res ? res : null,
+ };
+ }
+
public void Dispose ()
{
peReader.Dispose ();
@@ -290,6 +505,21 @@ class TypeAttributeInfo (string attributeName)
{
public string AttributeName { get; } = attributeName;
public string? JniName { get; set; }
+
+ ///
+ /// All named property values from the component attribute.
+ ///
+ public Dictionary Properties { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Intent filters declared on this type via [IntentFilter] attributes.
+ ///
+ public List IntentFilters { get; } = [];
+
+ ///
+ /// Metadata entries declared on this type via [MetaData] attributes.
+ ///
+ public List MetaData { get; } = [];
}
sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs
new file mode 100644
index 00000000000..068aa226709
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed class AssemblyManifestInfo
+{
+ public List Permissions { get; } = [];
+ public List PermissionGroups { get; } = [];
+ public List PermissionTrees { get; } = [];
+ public List UsesPermissions { get; } = [];
+ public List UsesFeatures { get; } = [];
+ public List UsesLibraries { get; } = [];
+ public List UsesConfigurations { get; } = [];
+ public List MetaData { get; } = [];
+ public List Properties { get; } = [];
+
+ public Dictionary? ApplicationProperties { get; set; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs
new file mode 100644
index 00000000000..1995de0ecff
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public enum ComponentKind
+{
+ Activity,
+ Service,
+ BroadcastReceiver,
+ ContentProvider,
+ Application,
+ Instrumentation,
+}
+
+public sealed record ComponentInfo
+{
+ public required ComponentKind Kind { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+ public IReadOnlyList IntentFilters { get; init; } = [];
+ public IReadOnlyList MetaData { get; init; } = [];
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs
new file mode 100644
index 00000000000..0d8d328c973
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record IntentFilterInfo
+{
+ public IReadOnlyList Actions { get; init; } = [];
+ public IReadOnlyList Categories { get; init; } = [];
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index adb6a052f5e..aed62453042 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -119,9 +119,8 @@ public sealed record JavaPeerInfo
public bool IsGenericDefinition { get; init; }
///
- /// Component attribute information ([Activity], [Service], [BroadcastReceiver],
- /// [ContentProvider], [Application], [Instrumentation]).
- /// Null for types that are not Android components.
+ /// Android component attribute data ([Activity], [Service], [BroadcastReceiver], [ContentProvider],
+ /// [Application], [Instrumentation]) if present on this type. Used for manifest generation.
///
public ComponentInfo? ComponentAttribute { get; init; }
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index f81c0b0b87e..3b5ef62ae35 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -100,6 +100,19 @@ public List Scan (IReadOnlyList assemblyPaths)
return new List (resultsByManagedName.Values);
}
+ ///
+ /// Scans all loaded assemblies for assembly-level manifest attributes.
+ /// Must be called after .
+ ///
+ internal AssemblyManifestInfo ScanAssemblyManifestInfo ()
+ {
+ var info = new AssemblyManifestInfo ();
+ foreach (var index in assemblyCache.Values) {
+ index.ScanAssemblyAttributes (info);
+ }
+ return info;
+ }
+
///
/// Types referenced by [Application(BackupAgent = typeof(X))] or
/// [Application(ManageSpaceActivity = typeof(X))] must be unconditional,
@@ -238,6 +251,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
+ ComponentAttribute = ToComponentInfo (attrInfo, typeDef, index),
};
results [fullName] = peer;
@@ -1472,4 +1486,32 @@ static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index,
});
}
}
+
+ static ComponentInfo? ToComponentInfo (TypeAttributeInfo? attrInfo, TypeDefinition typeDef, AssemblyIndex index)
+ {
+ if (attrInfo is null) {
+ return null;
+ }
+
+ var kind = attrInfo.AttributeName switch {
+ "ActivityAttribute" => ComponentKind.Activity,
+ "ServiceAttribute" => ComponentKind.Service,
+ "BroadcastReceiverAttribute" => ComponentKind.BroadcastReceiver,
+ "ContentProviderAttribute" => ComponentKind.ContentProvider,
+ "ApplicationAttribute" => ComponentKind.Application,
+ "InstrumentationAttribute" => ComponentKind.Instrumentation,
+ _ => (ComponentKind?)null,
+ };
+
+ if (kind is null) {
+ return null;
+ }
+
+ return new ComponentInfo {
+ Kind = kind.Value,
+ Properties = attrInfo.Properties,
+ IntentFilters = attrInfo.IntentFilters,
+ MetaData = attrInfo.MetaData,
+ };
+ }
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs
new file mode 100644
index 00000000000..bf5ae6caa0c
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record MetaDataInfo
+{
+ public required string Name { get; init; }
+ public string? Value { get; init; }
+ public string? Resource { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs
new file mode 100644
index 00000000000..ad43ab52157
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record PermissionGroupInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs
new file mode 100644
index 00000000000..e966abc7016
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record PermissionInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs
new file mode 100644
index 00000000000..4b8fa9cce46
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record PermissionTreeInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs
new file mode 100644
index 00000000000..3912449ff8d
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record PropertyInfo
+{
+ public required string Name { get; init; }
+ public string? Value { get; init; }
+ public string? Resource { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs
new file mode 100644
index 00000000000..86edd938f84
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs
@@ -0,0 +1,10 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record UsesConfigurationInfo
+{
+ public bool ReqFiveWayNav { get; init; }
+ public bool ReqHardKeyboard { get; init; }
+ public string? ReqKeyboardType { get; init; }
+ public string? ReqNavigation { get; init; }
+ public string? ReqTouchScreen { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs
new file mode 100644
index 00000000000..09459197778
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs
@@ -0,0 +1,8 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record UsesFeatureInfo
+{
+ public string? Name { get; init; }
+ public int GLESVersion { get; init; }
+ public bool Required { get; init; } = true;
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs
new file mode 100644
index 00000000000..1e70b74232f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs
@@ -0,0 +1,7 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record UsesLibraryInfo
+{
+ public required string Name { get; init; }
+ public bool Required { get; init; } = true;
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs
new file mode 100644
index 00000000000..d11df2d8a3f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs
@@ -0,0 +1,7 @@
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+internal sealed record UsesPermissionInfo
+{
+ public required string Name { get; init; }
+ public int? MaxSdkVersion { 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 e73b18c460a..e04c700f845 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -21,12 +21,16 @@ static string TestFixtureAssemblyPath {
}
}
- static readonly Lazy> _cachedFixtures = new (() => {
- using var scanner = new JavaPeerScanner ();
- return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => {
+ var scanner = new JavaPeerScanner ();
+ var peers = scanner.Scan (new [] { TestFixtureAssemblyPath });
+ var manifestInfo = scanner.ScanAssemblyManifestInfo ();
+ return (peers, manifestInfo);
});
- private protected static List ScanFixtures () => _cachedFixtures.Value;
+ private protected static List ScanFixtures () => _cachedScanResult.Value.peers;
+
+ private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo;
private protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs
index b41fce30764..2653286fd20 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -76,7 +74,11 @@ XDocument GenerateAndLoad (
{
peers ??= [];
assemblyInfo ??= new AssemblyManifestInfo ();
- gen.Generate (templatePath, peers, assemblyInfo, OutputPath);
+ XDocument? template = null;
+ if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) {
+ template = XDocument.Load (templatePath);
+ }
+ gen.Generate (template, peers, assemblyInfo, OutputPath);
return XDocument.Load (OutputPath);
}
@@ -84,7 +86,7 @@ XDocument GenerateAndLoad (
public void Activity_MainLauncher ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MainActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MainActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
Properties = new Dictionary { ["MainLauncher"] = true },
});
@@ -108,7 +110,7 @@ public void Activity_MainLauncher ()
public void Activity_WithProperties ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MyActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MyActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
Properties = new Dictionary {
["Label"] = "My Activity",
@@ -131,7 +133,7 @@ public void Activity_WithProperties ()
public void Activity_IntentFilter ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/ShareActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/ShareActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
IntentFilters = [
new IntentFilterInfo {
@@ -161,7 +163,7 @@ public void Activity_IntentFilter ()
public void Activity_MetaData ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MetaActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MetaActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
MetaData = [
new MetaDataInfo { Name = "com.example.key", Value = "my_value" },
@@ -190,7 +192,7 @@ public void Activity_MetaData ()
public void Component_BasicProperties (ComponentKind kind, string elementName)
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MyComponent", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MyComponent", new ComponentInfo {
Kind = kind,
Properties = new Dictionary {
["Exported"] = true,
@@ -211,7 +213,7 @@ public void Component_BasicProperties (ComponentKind kind, string elementName)
public void ContentProvider_WithAuthorities ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MyProvider", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MyProvider", new ComponentInfo {
Kind = ComponentKind.ContentProvider,
Properties = new Dictionary {
["Authorities"] = "com.example.app.provider",
@@ -234,7 +236,7 @@ public void ContentProvider_WithAuthorities ()
public void Application_TypeLevel ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MyApp", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MyApp", new ComponentInfo {
Kind = ComponentKind.Application,
Properties = new Dictionary {
["Label"] = "Custom App",
@@ -256,7 +258,7 @@ public void Application_TypeLevel ()
public void Instrumentation_GoesToManifest ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo {
Kind = ComponentKind.Instrumentation,
Properties = new Dictionary {
["Label"] = "My Test",
@@ -418,7 +420,7 @@ public void ApplicationJavaClass_Set ()
public void AbstractTypes_Skipped ()
{
var gen = CreateDefaultGenerator ();
- var peer = CreatePeer ("com/example/app/AbstractActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/AbstractActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
Properties = new Dictionary { ["Label"] = "Abstract" },
}, isAbstract: true);
@@ -442,7 +444,7 @@ public void ExistingType_NotDuplicated ()
""");
- var peer = CreatePeer ("com/example/app/ExistingActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/ExistingActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
Properties = new Dictionary { ["Label"] = "New Label" },
});
@@ -595,7 +597,7 @@ public void ConfigChanges_EnumConversion ()
var gen = CreateDefaultGenerator ();
// orientation (0x0080) | keyboardHidden (0x0020) | screenSize (0x0400)
int configChanges = 0x0080 | 0x0020 | 0x0400;
- var peer = CreatePeer ("com/example/app/ConfigActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ var peer = CreatePeer ("com/example/app/ConfigActivity", new ComponentInfo {
Kind = ComponentKind.Activity,
Properties = new Dictionary {
["ConfigurationChanges"] = configChanges,
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs
new file mode 100644
index 00000000000..d1f9d04f7df
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs
@@ -0,0 +1,69 @@
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class AssemblyAttributeScanningTests : FixtureTestBase
+{
+ [Fact]
+ public void UsesFeature_ConstructorArg ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var feature = info.UsesFeatures.FirstOrDefault (f => f.Name == "android.hardware.camera");
+ Assert.NotNull (feature);
+ Assert.True (feature.Required);
+ }
+
+ [Fact]
+ public void UsesFeature_ConstructorArgWithNamedProperty ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var feature = info.UsesFeatures.FirstOrDefault (f => f.Name == "android.hardware.camera.autofocus");
+ Assert.NotNull (feature);
+ Assert.False (feature.Required);
+ }
+
+ [Fact]
+ public void UsesFeature_GLESVersion ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var feature = info.UsesFeatures.FirstOrDefault (f => f.GLESVersion == 0x00020000);
+ Assert.NotNull (feature);
+ Assert.Null (feature.Name);
+ }
+
+ [Fact]
+ public void UsesPermission_ConstructorArg ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var perm = info.UsesPermissions.FirstOrDefault (p => p.Name == "android.permission.INTERNET");
+ Assert.NotNull (perm);
+ }
+
+ [Fact]
+ public void UsesLibrary_ConstructorArg ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var lib = info.UsesLibraries.FirstOrDefault (l => l.Name == "org.apache.http.legacy");
+ Assert.NotNull (lib);
+ Assert.True (lib.Required);
+ }
+
+ [Fact]
+ public void UsesLibrary_TwoArgConstructor ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var lib = info.UsesLibraries.FirstOrDefault (l => l.Name == "com.example.optional");
+ Assert.NotNull (lib);
+ Assert.False (lib.Required);
+ }
+
+ [Fact]
+ public void MetaData_ConstructorArgAndNamedArg ()
+ {
+ var info = ScanAssemblyManifestInfo ();
+ var meta = info.MetaData.FirstOrDefault (m => m.Name == "com.example.key");
+ Assert.NotNull (meta);
+ Assert.Equal ("test-value", meta.Value);
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs
new file mode 100644
index 00000000000..f86a255829d
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs
@@ -0,0 +1,9 @@
+using Android.App;
+
+[assembly: UsesFeature ("android.hardware.camera")]
+[assembly: UsesFeature ("android.hardware.camera.autofocus", Required = false)]
+[assembly: UsesFeature (GLESVersion = 0x00020000)]
+[assembly: UsesPermission ("android.permission.INTERNET")]
+[assembly: UsesLibrary ("org.apache.http.legacy")]
+[assembly: UsesLibrary ("com.example.optional", false)]
+[assembly: MetaData ("com.example.key", Value = "test-value")]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
index e65ff2800bf..1e19e314e32 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
@@ -89,6 +89,62 @@ public sealed class ApplicationAttribute : Attribute, Java.Interop.IJniNameProvi
public string? Name { get; set; }
string Java.Interop.IJniNameProviderAttribute.Name => Name ?? "";
}
+
+ [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class UsesFeatureAttribute : Attribute
+ {
+ public UsesFeatureAttribute () { }
+ public UsesFeatureAttribute (string name) => Name = name;
+
+ // Name has a private setter — only settable via ctor (matches the real attribute)
+ public string? Name { get; private set; }
+ public int GLESVersion { get; set; }
+ public bool Required { get; set; } = true;
+ }
+
+ [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class UsesPermissionAttribute : Attribute
+ {
+ public UsesPermissionAttribute () { }
+ public UsesPermissionAttribute (string name) => Name = name;
+
+ public string? Name { get; set; }
+ public int MaxSdkVersion { get; set; }
+ }
+
+ [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+ public sealed class UsesLibraryAttribute : Attribute
+ {
+ public UsesLibraryAttribute () { }
+ public UsesLibraryAttribute (string name) => Name = name;
+ public UsesLibraryAttribute (string name, bool required)
+ {
+ Name = name;
+ Required = required;
+ }
+
+ public string? Name { get; set; }
+ public bool Required { get; set; } = true;
+ }
+
+ [AttributeUsage (AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = true)]
+ public sealed class MetaDataAttribute : Attribute
+ {
+ public MetaDataAttribute (string name) => Name = name;
+
+ public string Name { get; }
+ public string? Value { get; set; }
+ public string? Resource { get; set; }
+ }
+
+ [AttributeUsage (AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = true)]
+ public sealed class IntentFilterAttribute : Attribute
+ {
+ public IntentFilterAttribute (string [] actions) => Actions = actions;
+
+ public string [] Actions { get; }
+ public string []? Categories { get; set; }
+ }
}
namespace Android.Content