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