From 0496fbc8870876fdaef2edc9e7740e9b58d846e9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 11:53:44 +0100 Subject: [PATCH 1/8] [TrimmableTypeMap] Add assembly-level manifest attribute scanning Add scanning for assembly-level attributes ([Application], [Activity], [Service], [BroadcastReceiver], [ContentProvider], permissions, etc.) and wire them into the manifest generator via Generate(XDocument?) overload. Replace ManifestModel.cs with individual Scanner/ record types for better composability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 25 +- .../Generator/ManifestModel.cs | 105 ------- .../Scanner/AssemblyIndex.cs | 297 +++++++++++++++++- .../Scanner/AssemblyManifestInfo.cs | 29 ++ .../Scanner/ComponentInfo.cs | 53 ++++ .../Scanner/IntentFilterInfo.cs | 26 ++ .../Scanner/JavaPeerInfo.cs | 5 +- .../Scanner/JavaPeerScanner.cs | 132 +++++++- .../Scanner/MetaDataInfo.cs | 24 ++ .../Scanner/PermissionGroupInfo.cs | 11 + .../Scanner/PermissionInfo.cs | 11 + .../Scanner/PermissionTreeInfo.cs | 11 + .../Scanner/PropertyInfo.cs | 10 + .../Scanner/UsesConfigurationInfo.cs | 12 + .../Scanner/UsesFeatureInfo.cs | 21 ++ .../Scanner/UsesLibraryInfo.cs | 9 + .../Scanner/UsesPermissionInfo.cs | 9 + .../Generator/JcwJavaSourceGeneratorTests.cs | 4 +- .../Scanner/OverrideDetectionTests.cs | 2 +- 19 files changed, 673 insertions(+), 123 deletions(-) delete mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 938c60fd10a..53b0976e528 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -46,7 +46,24 @@ public IList Generate ( AssemblyManifestInfo assemblyInfo, string outputPath) { - var doc = LoadOrCreateManifest (manifestTemplatePath); + XDocument? template = null; + if (!string.IsNullOrEmpty (manifestTemplatePath) && File.Exists (manifestTemplatePath)) { + template = XDocument.Load (manifestTemplatePath); + } + return Generate (template, allPeers, assemblyInfo, outputPath); + } + + /// + /// 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 ( + XDocument? manifestTemplate, + IReadOnlyList allPeers, + AssemblyManifestInfo assemblyInfo, + string outputPath) + { + var doc = manifestTemplate ?? CreateDefaultManifest (); var manifest = doc.Root; if (manifest is null) { throw new InvalidOperationException ("Manifest document has no root element."); @@ -134,12 +151,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..69611259087 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -98,14 +98,33 @@ 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") { + attrInfo ??= new TypeAttributeInfo ("IntentFilterAttribute"); + attrInfo.IntentFilters.Add (ParseIntentFilterAttribute (ca)); + } else if (attrName == "MetaDataAttribute") { + attrInfo ??= new TypeAttributeInfo ("MetaDataAttribute"); + attrInfo.MetaData.Add (ParseMetaDataAttribute (ca)); } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) var name = TryGetNameProperty (ca); @@ -239,6 +258,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 +275,68 @@ 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, + }; + } + + MetaDataInfo ParseMetaDataAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + + string name = ""; + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string nameArg) { + name = nameArg; + } + + string? metaValue = null; + string? resource = null; + TryGetNamedArgument (value, "Value", out metaValue); + TryGetNamedArgument (value, "Resource", out resource); + + return new MetaDataInfo { + Name = name, + Value = metaValue, + Resource = resource, + }; + } + static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull { foreach (var named in value.NamedArguments) { @@ -260,6 +349,193 @@ 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; + } + + switch (attrName) { + case "PermissionAttribute": + info.Permissions.Add (ParsePermissionAttribute (ca)); + break; + case "PermissionGroupAttribute": + info.PermissionGroups.Add (ParsePermissionGroupAttribute (ca)); + break; + case "PermissionTreeAttribute": + info.PermissionTrees.Add (ParsePermissionTreeAttribute (ca)); + break; + case "UsesPermissionAttribute": + info.UsesPermissions.Add (ParseUsesPermissionAttribute (ca)); + break; + case "UsesFeatureAttribute": + info.UsesFeatures.Add (ParseUsesFeatureAttribute (ca)); + break; + case "UsesLibraryAttribute": + info.UsesLibraries.Add (ParseUsesLibraryAttribute (ca)); + break; + case "UsesConfigurationAttribute": + info.UsesConfigurations.Add (ParseUsesConfigurationAttribute (ca)); + break; + case "MetaDataAttribute": + info.MetaData.Add (ParseMetaDataAttribute (ca)); + break; + case "PropertyAttribute": + info.Properties.Add (ParsePropertyAttribute (ca)); + break; + case "ApplicationAttribute": + info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal); + var appValue = DecodeAttribute (ca); + foreach (var named in appValue.NamedArguments) { + if (named.Name is not null) { + info.ApplicationProperties [named.Name] = named.Value; + } + } + break; + } + } + } + + (string name, Dictionary props) ParseNameAndProperties (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + string name = ""; + var props = new Dictionary (StringComparer.Ordinal); + 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); + } + + PermissionInfo ParsePermissionAttribute (CustomAttribute ca) + { + var (name, props) = ParseNameAndProperties (ca); + return new PermissionInfo { Name = name, Properties = props }; + } + + PermissionGroupInfo ParsePermissionGroupAttribute (CustomAttribute ca) + { + var (name, props) = ParseNameAndProperties (ca); + return new PermissionGroupInfo { Name = name, Properties = props }; + } + + PermissionTreeInfo ParsePermissionTreeAttribute (CustomAttribute ca) + { + var (name, props) = ParseNameAndProperties (ca); + return new PermissionTreeInfo { Name = name, Properties = props }; + } + + UsesPermissionInfo ParseUsesPermissionAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + string name = ""; + int? maxSdk = null; + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { + name = n; + } + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string nameVal) { + name = nameVal; + } else if (named.Name == "MaxSdkVersion" && named.Value is int max) { + maxSdk = max; + } + } + return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk }; + } + + UsesFeatureInfo ParseUsesFeatureAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + string? name = null; + int glesVersion = 0; + bool required = true; + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string n) { + name = n; + } else if (named.Name == "GLESVersion" && named.Value is int v) { + glesVersion = v; + } else if (named.Name == "Required" && named.Value is bool r) { + required = r; + } + } + return new UsesFeatureInfo { Name = name, GLESVersion = glesVersion, Required = required }; + } + + UsesLibraryInfo ParseUsesLibraryAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + string name = ""; + bool required = true; + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { + name = n; + } + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string nameVal) { + name = nameVal; + } else if (named.Name == "Required" && named.Value is bool r) { + required = r; + } + } + return new UsesLibraryInfo { Name = name, Required = required }; + } + + UsesConfigurationInfo ParseUsesConfigurationAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + bool reqFiveWayNav = false; + bool reqHardKeyboard = false; + string? reqKeyboardType = null; + string? reqNavigation = null; + string? reqTouchScreen = null; + foreach (var named in value.NamedArguments) { + switch (named.Name) { + case "ReqFiveWayNav" when named.Value is bool b: reqFiveWayNav = b; break; + case "ReqHardKeyboard" when named.Value is bool b: reqHardKeyboard = b; break; + case "ReqKeyboardType" when named.Value is string s: reqKeyboardType = s; break; + case "ReqNavigation" when named.Value is string s: reqNavigation = s; break; + case "ReqTouchScreen" when named.Value is string s: reqTouchScreen = s; break; + } + } + return new UsesConfigurationInfo { + ReqFiveWayNav = reqFiveWayNav, + ReqHardKeyboard = reqHardKeyboard, + ReqKeyboardType = reqKeyboardType, + ReqNavigation = reqNavigation, + ReqTouchScreen = reqTouchScreen, + }; + } + + PropertyInfo ParsePropertyAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + string name = ""; + string? propValue = null; + string? resource = null; + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { + name = n; + } + foreach (var named in value.NamedArguments) { + switch (named.Name) { + case "Name" when named.Value is string s: name = s; break; + case "Value" when named.Value is string s: propValue = s; break; + case "Resource" when named.Value is string s: resource = s; break; + } + } + return new PropertyInfo { Name = name, Value = propValue, Resource = resource }; + } + public void Dispose () { peReader.Dispose (); @@ -290,6 +566,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..e9434f49ca9 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Assembly-level manifest attributes collected from all scanned assemblies. +/// Aggregated across assemblies — used to generate top-level manifest elements +/// like ]]>, ]]>, etc. +/// +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; } = []; + + /// + /// Assembly-level [Application] attribute properties (merged from all assemblies). + /// Null if no assembly-level [Application] attribute was found. + /// + 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..79a9d07dab6 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// The kind of Android component (Activity, Service, etc.). +/// +public enum ComponentKind +{ + Activity, + Service, + BroadcastReceiver, + ContentProvider, + Application, + Instrumentation, +} + +/// +/// Describes an Android component attribute ([Activity], [Service], etc.) on a Java peer type. +/// All named property values from the attribute are stored in . +/// +public sealed record ComponentInfo +{ + /// + /// The kind of component. + /// + public required ComponentKind Kind { get; init; } + + /// + /// All named property values from the component attribute. + /// Keys are property names (e.g., "Label", "Exported", "MainLauncher"). + /// Values are the raw decoded values (string, bool, int for enums, etc.). + /// + public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); + + /// + /// Intent filters declared on this component via [IntentFilter] attributes. + /// + public IReadOnlyList IntentFilters { get; init; } = []; + + /// + /// Metadata entries declared on this component via [MetaData] attributes. + /// + public IReadOnlyList MetaData { get; init; } = []; + + /// + /// Whether the component type has a public parameterless constructor. + /// Required for manifest inclusion — XA4213 error if missing. + /// + public bool HasPublicDefaultConstructor { 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..4e685496097 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs @@ -0,0 +1,26 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Describes an [IntentFilter] attribute on a component type. +/// +public sealed record IntentFilterInfo +{ + /// + /// Action names from the first constructor argument (string[]). + /// + public IReadOnlyList Actions { get; init; } = []; + + /// + /// Category names. + /// + public IReadOnlyList Categories { get; init; } = []; + + /// + /// Named properties (DataScheme, DataHost, DataPath, Label, Icon, Priority, etc.). + /// + 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..e4a3c5128d4 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; @@ -773,7 +787,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = registerInfo.Signature, Connector = registerInfo.Connector, ManagedMethodName = methodName, - NativeCallbackName = isConstructor ? "n_ctor" : $"n_{methodName}", + NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor), IsConstructor = isConstructor, DeclaringTypeName = result.Value.DeclaringTypeName, DeclaringAssemblyName = result.Value.DeclaringAssemblyName, @@ -818,7 +832,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = propRegister.Signature, Connector = propRegister.Connector, ManagedMethodName = getterName, - NativeCallbackName = $"n_{getterName}", + NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false), IsConstructor = false, DeclaringTypeName = baseTypeName, DeclaringAssemblyName = baseAssemblyName, @@ -866,12 +880,18 @@ static void AddMarshalMethod (List methods, RegisterInfo regi string managedName = index.Reader.GetString (methodDef.Name); string jniSignature = registerInfo.Signature ?? "()V"; + string declaringTypeName = ""; + string declaringAssemblyName = ""; + ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName); + methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, JniSignature = jniSignature, Connector = registerInfo.Connector, ManagedMethodName = managedName, - NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}", + DeclaringTypeName = declaringTypeName, + DeclaringAssemblyName = declaringAssemblyName, + NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor), IsConstructor = isConstructor, IsExport = isExport, IsInterfaceImplementation = isInterfaceImplementation, @@ -1383,6 +1403,65 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (typeName, parentJniName, ns); } + /// + /// Derives the native callback method name from a [Register] attribute's Connector field. + /// The Connector may be a simple name like "GetOnCreate_Landroid_os_Bundle_Handler" + /// or a qualified name like "GetOnClick_Landroid_view_View_Handler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, …". + /// In both cases the result is e.g. "n_OnCreate_Landroid_os_Bundle_". + /// Falls back to "n_{managedName}" when the Connector doesn't follow the expected pattern. + /// + static string GetNativeCallbackName (string? connector, string managedName, bool isConstructor) + { + if (isConstructor) { + return "n_ctor"; + } + + if (connector is not null) { + // Strip the optional type qualifier after ':' + int colonIndex = connector.IndexOf (':'); + string handlerName = colonIndex >= 0 ? connector.Substring (0, colonIndex) : connector; + + if (handlerName.StartsWith ("Get", StringComparison.Ordinal) + && handlerName.EndsWith ("Handler", StringComparison.Ordinal)) { + return "n_" + handlerName.Substring (3, handlerName.Length - 3 - "Handler".Length); + } + } + + return $"n_{managedName}"; + } + + /// + /// Parses the type qualifier from a Connector string. + /// Connector format: "GetOnClickHandler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, Version=…". + /// Extracts the managed type name (converting /+ for nested types) and assembly name. + /// + static void ParseConnectorDeclaringType (string? connector, out string declaringTypeName, out string declaringAssemblyName) + { + declaringTypeName = ""; + declaringAssemblyName = ""; + + if (connector is null) { + return; + } + + int colonIndex = connector.IndexOf (':'); + if (colonIndex < 0) { + return; + } + + // After ':' is "TypeName, AssemblyName, Version=…" (assembly-qualified name) + string typeQualified = connector.Substring (colonIndex + 1); + int commaIndex = typeQualified.IndexOf (','); + if (commaIndex < 0) { + return; + } + + declaringTypeName = typeQualified.Substring (0, commaIndex).Trim ().Replace ('/', '+'); + string rest = typeQualified.Substring (commaIndex + 1).Trim (); + int nextComma = rest.IndexOf (','); + declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); + } + static string GetCrc64PackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly @@ -1472,4 +1551,51 @@ 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, + HasPublicDefaultConstructor = HasPublicParameterlessCtor (typeDef, index), + }; + } + + static bool HasPublicParameterlessCtor (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var method = index.Reader.GetMethodDefinition (methodHandle); + if (index.Reader.GetString (method.Name) != ".ctor") { + continue; + } + if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { + continue; + } + var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + if (sig.ParameterTypes.Length == 0) { + return true; + } + } + return false; + } } 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..f363e824cd8 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs @@ -0,0 +1,24 @@ +#nullable enable + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Describes a [MetaData] attribute on a component type. +/// +public sealed record MetaDataInfo +{ + /// + /// The metadata name (first constructor argument). + /// + public required string Name { get; init; } + + /// + /// The Value property, if set. + /// + public string? Value { get; init; } + + /// + /// The Resource property, if set. + /// + 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..f182058a10d --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs @@ -0,0 +1,11 @@ +#nullable enable + +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..537baeaa7f2 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs @@ -0,0 +1,11 @@ +#nullable enable + +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..3e6abb6f4b5 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs @@ -0,0 +1,11 @@ +#nullable enable + +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..4ce71a98835 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs @@ -0,0 +1,10 @@ +#nullable enable + +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..380d4091c51 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs @@ -0,0 +1,12 @@ +#nullable enable + +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..8499094c170 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs @@ -0,0 +1,21 @@ +#nullable enable + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Describes a [UsesFeature] attribute. +/// +internal sealed record UsesFeatureInfo +{ + /// + /// Feature name (e.g., "android.hardware.camera"). Null for GL ES version features. + /// + public string? Name { get; init; } + + /// + /// OpenGL ES version (e.g., 0x00020000 for 2.0). Zero for named features. + /// + 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..4ac0e86a66d --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs @@ -0,0 +1,9 @@ +#nullable enable + +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..287bcd04b7a --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs @@ -0,0 +1,9 @@ +#nullable enable + +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/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 18c6ff7d6b9..78fcb9159e6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -245,8 +245,8 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () var java = GenerateFixture ("my/app/MainActivity"); AssertContainsLine ("@Override\n", java); AssertContainsLine ("public void onCreate (android.os.Bundle p0)\n", java); - AssertContainsLine ("n_OnCreate (p0);\n", java); - AssertContainsLine ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + AssertContainsLine ("n_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 07e005e6482..54f7e0e133a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -15,7 +15,7 @@ public void Override_DetectedWithCorrectRegistration () var peer = FindFixtureByJavaName ("my/app/UserActivity"); var onCreate = peer.MarshalMethods.First (m => m.JniName == "onCreate"); Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); - Assert.Equal ("n_OnCreate", onCreate.NativeCallbackName); + Assert.Equal ("n_OnCreate_Landroid_os_Bundle_", onCreate.NativeCallbackName); Assert.False (onCreate.IsConstructor); Assert.Equal ("GetOnCreate_Landroid_os_Bundle_Handler", onCreate.Connector); Assert.NotNull (peer.ActivationCtor); From bde5a243281cd6ca21499f35592f232d5b041750 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 12:03:34 +0100 Subject: [PATCH 2/8] [TrimmableTypeMap] Assembly-level attribute scanning + manifest model types Add assembly-level manifest attribute scanning to the TrimmableTypeMap scanner: - AssemblyIndex.ScanAssemblyAttributes(): parses 9 assembly-level attribute types (UsesPermission, UsesLibrary, UsesFeature, UsesConfiguration, Permission, PermissionGroup, PermissionTree, MetaData, Property) - JavaPeerScanner.ScanAssemblyManifestInfo(): aggregates attributes across assemblies - JavaPeerScanner.ToComponentInfo()/HasPublicParameterlessCtor(): attaches component info to scanned peers - ManifestGenerator.Generate(XDocument?): new overload accepting pre-loaded template - ManifestGenerator.CreateDefaultManifest(): extracted from LoadOrCreateManifest - Replace ManifestModel.cs (105-line flat class) with 12 focused record types - Update JavaPeerInfo.ComponentAttribute doc comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 71 +------------------ .../Generator/JcwJavaSourceGeneratorTests.cs | 4 +- .../Scanner/OverrideDetectionTests.cs | 2 +- 3 files changed, 6 insertions(+), 71 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index e4a3c5128d4..e733df5604d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -787,7 +787,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = registerInfo.Signature, Connector = registerInfo.Connector, ManagedMethodName = methodName, - NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor), + NativeCallbackName = isConstructor ? "n_ctor" : $"n_{methodName}", IsConstructor = isConstructor, DeclaringTypeName = result.Value.DeclaringTypeName, DeclaringAssemblyName = result.Value.DeclaringAssemblyName, @@ -832,7 +832,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = propRegister.Signature, Connector = propRegister.Connector, ManagedMethodName = getterName, - NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false), + NativeCallbackName = $"n_{getterName}", IsConstructor = false, DeclaringTypeName = baseTypeName, DeclaringAssemblyName = baseAssemblyName, @@ -880,18 +880,12 @@ static void AddMarshalMethod (List methods, RegisterInfo regi string managedName = index.Reader.GetString (methodDef.Name); string jniSignature = registerInfo.Signature ?? "()V"; - string declaringTypeName = ""; - string declaringAssemblyName = ""; - ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName); - methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, JniSignature = jniSignature, Connector = registerInfo.Connector, ManagedMethodName = managedName, - DeclaringTypeName = declaringTypeName, - DeclaringAssemblyName = declaringAssemblyName, - NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor), + NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}", IsConstructor = isConstructor, IsExport = isExport, IsInterfaceImplementation = isInterfaceImplementation, @@ -1403,65 +1397,6 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (typeName, parentJniName, ns); } - /// - /// Derives the native callback method name from a [Register] attribute's Connector field. - /// The Connector may be a simple name like "GetOnCreate_Landroid_os_Bundle_Handler" - /// or a qualified name like "GetOnClick_Landroid_view_View_Handler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, …". - /// In both cases the result is e.g. "n_OnCreate_Landroid_os_Bundle_". - /// Falls back to "n_{managedName}" when the Connector doesn't follow the expected pattern. - /// - static string GetNativeCallbackName (string? connector, string managedName, bool isConstructor) - { - if (isConstructor) { - return "n_ctor"; - } - - if (connector is not null) { - // Strip the optional type qualifier after ':' - int colonIndex = connector.IndexOf (':'); - string handlerName = colonIndex >= 0 ? connector.Substring (0, colonIndex) : connector; - - if (handlerName.StartsWith ("Get", StringComparison.Ordinal) - && handlerName.EndsWith ("Handler", StringComparison.Ordinal)) { - return "n_" + handlerName.Substring (3, handlerName.Length - 3 - "Handler".Length); - } - } - - return $"n_{managedName}"; - } - - /// - /// Parses the type qualifier from a Connector string. - /// Connector format: "GetOnClickHandler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, Version=…". - /// Extracts the managed type name (converting /+ for nested types) and assembly name. - /// - static void ParseConnectorDeclaringType (string? connector, out string declaringTypeName, out string declaringAssemblyName) - { - declaringTypeName = ""; - declaringAssemblyName = ""; - - if (connector is null) { - return; - } - - int colonIndex = connector.IndexOf (':'); - if (colonIndex < 0) { - return; - } - - // After ':' is "TypeName, AssemblyName, Version=…" (assembly-qualified name) - string typeQualified = connector.Substring (colonIndex + 1); - int commaIndex = typeQualified.IndexOf (','); - if (commaIndex < 0) { - return; - } - - declaringTypeName = typeQualified.Substring (0, commaIndex).Trim ().Replace ('/', '+'); - string rest = typeQualified.Substring (commaIndex + 1).Trim (); - int nextComma = rest.IndexOf (','); - declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); - } - static string GetCrc64PackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 78fcb9159e6..18c6ff7d6b9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -245,8 +245,8 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () var java = GenerateFixture ("my/app/MainActivity"); AssertContainsLine ("@Override\n", java); AssertContainsLine ("public void onCreate (android.os.Bundle p0)\n", java); - AssertContainsLine ("n_OnCreate_Landroid_os_Bundle_ (p0);\n", java); - AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); + AssertContainsLine ("n_OnCreate (p0);\n", java); + AssertContainsLine ("public native void n_OnCreate (android.os.Bundle p0);\n", java); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 54f7e0e133a..07e005e6482 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -15,7 +15,7 @@ public void Override_DetectedWithCorrectRegistration () var peer = FindFixtureByJavaName ("my/app/UserActivity"); var onCreate = peer.MarshalMethods.First (m => m.JniName == "onCreate"); Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); - Assert.Equal ("n_OnCreate_Landroid_os_Bundle_", onCreate.NativeCallbackName); + Assert.Equal ("n_OnCreate", onCreate.NativeCallbackName); Assert.False (onCreate.IsConstructor); Assert.Equal ("GetOnCreate_Landroid_os_Bundle_Handler", onCreate.Connector); Assert.NotNull (peer.ActivationCtor); From 5072ced69cdd0a52cf4ab085a0f89921f7262e05 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:06:00 +0100 Subject: [PATCH 3/8] Address review: fix attribute ordering bug, remove redundant #nullable enable - Fix ParseAttributes: collect IntentFilter/MetaData in local lists and attach after the loop to avoid creating attrInfo with wrong AttributeName when [IntentFilter] appears before [Activity] - Remove redundant #nullable enable from 13 files (project has enable) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 1 - .../Scanner/AssemblyIndex.cs | 24 +++++++++++++++---- .../Scanner/AssemblyManifestInfo.cs | 1 - .../Scanner/ComponentInfo.cs | 1 - .../Scanner/IntentFilterInfo.cs | 1 - .../Scanner/MetaDataInfo.cs | 1 - .../Scanner/PermissionGroupInfo.cs | 1 - .../Scanner/PermissionInfo.cs | 1 - .../Scanner/PermissionTreeInfo.cs | 1 - .../Scanner/PropertyInfo.cs | 1 - .../Scanner/UsesConfigurationInfo.cs | 1 - .../Scanner/UsesFeatureInfo.cs | 1 - .../Scanner/UsesLibraryInfo.cs | 1 - .../Scanner/UsesPermissionInfo.cs | 1 - 14 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 53b0976e528..216d4f0ccc2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 69611259087..0686dab045e 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); @@ -120,11 +126,11 @@ void Build () } } } else if (attrName == "IntentFilterAttribute") { - attrInfo ??= new TypeAttributeInfo ("IntentFilterAttribute"); - attrInfo.IntentFilters.Add (ParseIntentFilterAttribute (ca)); + intentFilters ??= new List (); + intentFilters.Add (ParseIntentFilterAttribute (ca)); } else if (attrName == "MetaDataAttribute") { - attrInfo ??= new TypeAttributeInfo ("MetaDataAttribute"); - attrInfo.MetaData.Add (ParseMetaDataAttribute (ca)); + metaData ??= new List (); + metaData.Add (ParseMetaDataAttribute (ca)); } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) var name = TryGetNameProperty (ca); @@ -135,6 +141,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); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs index e9434f49ca9..856ec6ea267 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs index 79a9d07dab6..a16444e37da 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs index 4e685496097..f5ed20a901e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs index f363e824cd8..f6013c55707 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs index f182058a10d..71f676f805b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs index 537baeaa7f2..4425bfd5c09 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs index 3e6abb6f4b5..2e6f5d27c40 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs @@ -1,4 +1,3 @@ -#nullable enable using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs index 4ce71a98835..df1f92dab58 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs index 380d4091c51..7c4a6ac186e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs index 8499094c170..63e538efad0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs index 4ac0e86a66d..06d7567ffb5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs index 287bcd04b7a..35a4beff3a7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs @@ -1,4 +1,3 @@ -#nullable enable namespace Microsoft.Android.Sdk.TrimmableTypeMap; From 64fedb1d25b11e53bdb4da01137a49f0883e3e88 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:47:14 +0100 Subject: [PATCH 4/8] Fix ParseUsesFeatureAttribute to read constructor argument for feature name UsesFeatureAttribute.Name has a private setter and is set via the (string name) constructor. The parser only read NamedArguments, so the feature name was always null when set via the constructor, causing entries to be silently skipped. Read FixedArguments[0] first (matching ParseUsesPermissionAttribute and ParseUsesLibraryAttribute), then allow NamedArguments to override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 0686dab045e..a9bcfffc9c9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -477,6 +477,9 @@ UsesFeatureInfo ParseUsesFeatureAttribute (CustomAttribute ca) string? name = null; int glesVersion = 0; bool required = true; + if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string featureName) { + name = featureName; + } foreach (var named in value.NamedArguments) { if (named.Name == "Name" && named.Value is string n) { name = n; From 8c00ee8ea8edf5c9b67be5c254b3acd4e0f145e1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:56:35 +0100 Subject: [PATCH 5/8] Add scanner-level tests for assembly-level manifest attribute parsing Add stub attributes (UsesFeatureAttribute with private-setter Name, UsesPermissionAttribute, UsesLibraryAttribute, MetaDataAttribute, IntentFilterAttribute) and assembly-level usages in TestFixtures. Expose ScanAssemblyManifestInfo() via FixtureTestBase and add 6 tests in AssemblyAttributeScanningTests covering constructor-arg parsing for UsesFeature, UsesPermission, UsesLibrary, MetaData, and the GLESVersion named-arg-only variant. The UsesFeature_ConstructorArg and UsesFeature_ConstructorArgWithNamedProperty tests are regression tests for the ParseUsesFeatureAttribute fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 12 ++-- .../Scanner/AssemblyAttributeScanningTests.cs | 59 +++++++++++++++++++ .../TestFixtures/AssemblyAttributes.cs | 8 +++ .../TestFixtures/StubAttributes.cs | 56 ++++++++++++++++++ 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs 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/Scanner/AssemblyAttributeScanningTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs new file mode 100644 index 00000000000..732f53677d1 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs @@ -0,0 +1,59 @@ +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); + } + + [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..17493ed7ac3 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs @@ -0,0 +1,8 @@ +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: 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 From 67f69901b9a39e2f3f8512f0c70a21b06b1a74ab Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 14:47:19 +0100 Subject: [PATCH 6/8] Address review: cleanup and reduce Parse*Attribute duplication - Remove leading blank lines from 13 files (leftover from #nullable enable removal) - Remove unnecessary XML doc comments on internal record types - Remove unused HasPublicDefaultConstructor property and HasPublicParameterlessCtor method - Enhance ParseNameAndProperties to also read FixedArguments[0] for constructor-arg names - Refactor individual Parse*Attribute methods into static Create*Info methods that accept pre-parsed (name, props) from ParseNameAndProperties, eliminating duplicated DecodeAttribute calls and NamedArguments iteration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Generator/ManifestGenerator.cs | 1 - .../Scanner/AssemblyIndex.cs | 229 +++++++----------- .../Scanner/AssemblyManifestInfo.cs | 10 - .../Scanner/ComponentInfo.cs | 31 --- .../Scanner/IntentFilterInfo.cs | 15 -- .../Scanner/JavaPeerScanner.cs | 19 -- .../Scanner/MetaDataInfo.cs | 15 -- .../Scanner/PermissionGroupInfo.cs | 1 - .../Scanner/PermissionInfo.cs | 1 - .../Scanner/PermissionTreeInfo.cs | 1 - .../Scanner/PropertyInfo.cs | 1 - .../Scanner/UsesConfigurationInfo.cs | 1 - .../Scanner/UsesFeatureInfo.cs | 12 - .../Scanner/UsesLibraryInfo.cs | 1 - .../Scanner/UsesPermissionInfo.cs | 1 - .../Generator/ManifestGeneratorTests.cs | 23 +- 17 files changed, 97 insertions(+), 267 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index dc551974045..1d6cab64454 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit dc5519740458097ef2cac753b21bd2e1459e5908 +Subproject commit 1d6cab64454808bb8077e9e18207c9d7059ff43c diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 216d4f0ccc2..a32da8b0fcc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.IO; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index a9bcfffc9c9..d57087992b5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -130,7 +130,8 @@ void Build () intentFilters.Add (ParseIntentFilterAttribute (ca)); } else if (attrName == "MetaDataAttribute") { metaData ??= new List (); - metaData.Add (ParseMetaDataAttribute (ca)); + 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); @@ -332,27 +333,6 @@ IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca) }; } - MetaDataInfo ParseMetaDataAttribute (CustomAttribute ca) - { - var value = DecodeAttribute (ca); - - string name = ""; - if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string nameArg) { - name = nameArg; - } - - string? metaValue = null; - string? resource = null; - TryGetNamedArgument (value, "Value", out metaValue); - TryGetNamedArgument (value, "Resource", out resource); - - return new MetaDataInfo { - Name = name, - Value = metaValue, - Resource = resource, - }; - } - static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull { foreach (var named in value.NamedArguments) { @@ -379,43 +359,60 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) } switch (attrName) { - case "PermissionAttribute": - info.Permissions.Add (ParsePermissionAttribute (ca)); + case "PermissionAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.Permissions.Add (CreatePermissionInfo (name, props)); break; - case "PermissionGroupAttribute": - info.PermissionGroups.Add (ParsePermissionGroupAttribute (ca)); + } + case "PermissionGroupAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.PermissionGroups.Add (CreatePermissionGroupInfo (name, props)); break; - case "PermissionTreeAttribute": - info.PermissionTrees.Add (ParsePermissionTreeAttribute (ca)); + } + case "PermissionTreeAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.PermissionTrees.Add (CreatePermissionTreeInfo (name, props)); break; - case "UsesPermissionAttribute": - info.UsesPermissions.Add (ParseUsesPermissionAttribute (ca)); + } + case "UsesPermissionAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.UsesPermissions.Add (CreateUsesPermissionInfo (name, props)); break; - case "UsesFeatureAttribute": - info.UsesFeatures.Add (ParseUsesFeatureAttribute (ca)); + } + case "UsesFeatureAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.UsesFeatures.Add (CreateUsesFeatureInfo (name, props)); break; - case "UsesLibraryAttribute": - info.UsesLibraries.Add (ParseUsesLibraryAttribute (ca)); + } + case "UsesLibraryAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.UsesLibraries.Add (CreateUsesLibraryInfo (name, props)); break; - case "UsesConfigurationAttribute": - info.UsesConfigurations.Add (ParseUsesConfigurationAttribute (ca)); + } + case "UsesConfigurationAttribute": { + var (_, props) = ParseNameAndProperties (ca); + info.UsesConfigurations.Add (CreateUsesConfigurationInfo (props)); break; - case "MetaDataAttribute": - info.MetaData.Add (ParseMetaDataAttribute (ca)); + } + case "MetaDataAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.MetaData.Add (CreateMetaDataInfo (name, props)); break; - case "PropertyAttribute": - info.Properties.Add (ParsePropertyAttribute (ca)); + } + case "PropertyAttribute": { + var (name, props) = ParseNameAndProperties (ca); + info.Properties.Add (CreatePropertyInfo (name, props)); break; - case "ApplicationAttribute": + } + case "ApplicationAttribute": { + var (_, props) = ParseNameAndProperties (ca); info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal); - var appValue = DecodeAttribute (ca); - foreach (var named in appValue.NamedArguments) { - if (named.Name is not null) { - info.ApplicationProperties [named.Name] = named.Value; - } + foreach (var kvp in props) { + info.ApplicationProperties [kvp.Key] = kvp.Value; } break; } + } } } @@ -424,6 +421,9 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) 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; + } foreach (var named in value.NamedArguments) { if (named.Name == "Name" && named.Value is string n) { name = n; @@ -435,124 +435,65 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) return (name, props); } - PermissionInfo ParsePermissionAttribute (CustomAttribute ca) - { - var (name, props) = ParseNameAndProperties (ca); - return new PermissionInfo { Name = name, Properties = props }; - } + static PermissionInfo CreatePermissionInfo (string name, Dictionary props) + => new PermissionInfo { Name = name, Properties = props }; - PermissionGroupInfo ParsePermissionGroupAttribute (CustomAttribute ca) - { - var (name, props) = ParseNameAndProperties (ca); - return new PermissionGroupInfo { Name = name, Properties = props }; - } + static PermissionGroupInfo CreatePermissionGroupInfo (string name, Dictionary props) + => new PermissionGroupInfo { Name = name, Properties = props }; - PermissionTreeInfo ParsePermissionTreeAttribute (CustomAttribute ca) - { - var (name, props) = ParseNameAndProperties (ca); - return new PermissionTreeInfo { Name = name, Properties = props }; - } + static PermissionTreeInfo CreatePermissionTreeInfo (string name, Dictionary props) + => new PermissionTreeInfo { Name = name, Properties = props }; - UsesPermissionInfo ParseUsesPermissionAttribute (CustomAttribute ca) + static UsesPermissionInfo CreateUsesPermissionInfo (string name, Dictionary props) { - var value = DecodeAttribute (ca); - string name = ""; - int? maxSdk = null; - if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { - name = n; - } - foreach (var named in value.NamedArguments) { - if (named.Name == "Name" && named.Value is string nameVal) { - name = nameVal; - } else if (named.Name == "MaxSdkVersion" && named.Value is int max) { - maxSdk = max; - } - } + int? maxSdk = props.TryGetValue ("MaxSdkVersion", out var v) && v is int max ? max : null; return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk }; } - UsesFeatureInfo ParseUsesFeatureAttribute (CustomAttribute ca) + static UsesFeatureInfo CreateUsesFeatureInfo (string name, Dictionary props) { - var value = DecodeAttribute (ca); - string? name = null; - int glesVersion = 0; - bool required = true; - if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string featureName) { - name = featureName; - } - foreach (var named in value.NamedArguments) { - if (named.Name == "Name" && named.Value is string n) { - name = n; - } else if (named.Name == "GLESVersion" && named.Value is int v) { - glesVersion = v; - } else if (named.Name == "Required" && named.Value is bool r) { - required = r; - } - } - return new UsesFeatureInfo { Name = name, GLESVersion = glesVersion, Required = required }; + 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, + }; } - UsesLibraryInfo ParseUsesLibraryAttribute (CustomAttribute ca) + static UsesLibraryInfo CreateUsesLibraryInfo (string name, Dictionary props) { - var value = DecodeAttribute (ca); - string name = ""; - bool required = true; - if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { - name = n; - } - foreach (var named in value.NamedArguments) { - if (named.Name == "Name" && named.Value is string nameVal) { - name = nameVal; - } else if (named.Name == "Required" && named.Value is bool r) { - required = r; - } - } + var required = !props.TryGetValue ("Required", out var r) || r is not bool req || req; return new UsesLibraryInfo { Name = name, Required = required }; } - UsesConfigurationInfo ParseUsesConfigurationAttribute (CustomAttribute ca) + static UsesConfigurationInfo CreateUsesConfigurationInfo (Dictionary props) { - var value = DecodeAttribute (ca); - bool reqFiveWayNav = false; - bool reqHardKeyboard = false; - string? reqKeyboardType = null; - string? reqNavigation = null; - string? reqTouchScreen = null; - foreach (var named in value.NamedArguments) { - switch (named.Name) { - case "ReqFiveWayNav" when named.Value is bool b: reqFiveWayNav = b; break; - case "ReqHardKeyboard" when named.Value is bool b: reqHardKeyboard = b; break; - case "ReqKeyboardType" when named.Value is string s: reqKeyboardType = s; break; - case "ReqNavigation" when named.Value is string s: reqNavigation = s; break; - case "ReqTouchScreen" when named.Value is string s: reqTouchScreen = s; break; - } - } return new UsesConfigurationInfo { - ReqFiveWayNav = reqFiveWayNav, - ReqHardKeyboard = reqHardKeyboard, - ReqKeyboardType = reqKeyboardType, - ReqNavigation = reqNavigation, - ReqTouchScreen = reqTouchScreen, + 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, }; } - PropertyInfo ParsePropertyAttribute (CustomAttribute ca) + static MetaDataInfo CreateMetaDataInfo (string name, Dictionary props) { - var value = DecodeAttribute (ca); - string name = ""; - string? propValue = null; - string? resource = null; - if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) { - name = n; - } - foreach (var named in value.NamedArguments) { - switch (named.Name) { - case "Name" when named.Value is string s: name = s; break; - case "Value" when named.Value is string s: propValue = s; break; - case "Resource" when named.Value is string s: resource = s; break; - } - } - return new PropertyInfo { Name = name, Value = propValue, Resource = resource }; + 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 () diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs index 856ec6ea267..068aa226709 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs @@ -1,13 +1,7 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Assembly-level manifest attributes collected from all scanned assemblies. -/// Aggregated across assemblies — used to generate top-level manifest elements -/// like ]]>, ]]>, etc. -/// internal sealed class AssemblyManifestInfo { public List Permissions { get; } = []; @@ -20,9 +14,5 @@ internal sealed class AssemblyManifestInfo public List MetaData { get; } = []; public List Properties { get; } = []; - /// - /// Assembly-level [Application] attribute properties (merged from all assemblies). - /// Null if no assembly-level [Application] attribute was found. - /// 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 index a16444e37da..1995de0ecff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs @@ -1,11 +1,7 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// The kind of Android component (Activity, Service, etc.). -/// public enum ComponentKind { Activity, @@ -16,37 +12,10 @@ public enum ComponentKind Instrumentation, } -/// -/// Describes an Android component attribute ([Activity], [Service], etc.) on a Java peer type. -/// All named property values from the attribute are stored in . -/// public sealed record ComponentInfo { - /// - /// The kind of component. - /// public required ComponentKind Kind { get; init; } - - /// - /// All named property values from the component attribute. - /// Keys are property names (e.g., "Label", "Exported", "MainLauncher"). - /// Values are the raw decoded values (string, bool, int for enums, etc.). - /// public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); - - /// - /// Intent filters declared on this component via [IntentFilter] attributes. - /// public IReadOnlyList IntentFilters { get; init; } = []; - - /// - /// Metadata entries declared on this component via [MetaData] attributes. - /// public IReadOnlyList MetaData { get; init; } = []; - - /// - /// Whether the component type has a public parameterless constructor. - /// Required for manifest inclusion — XA4213 error if missing. - /// - public bool HasPublicDefaultConstructor { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs index f5ed20a901e..0d8d328c973 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs @@ -1,25 +1,10 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Describes an [IntentFilter] attribute on a component type. -/// public sealed record IntentFilterInfo { - /// - /// Action names from the first constructor argument (string[]). - /// public IReadOnlyList Actions { get; init; } = []; - - /// - /// Category names. - /// public IReadOnlyList Categories { get; init; } = []; - - /// - /// Named properties (DataScheme, DataHost, DataPath, Label, Icon, Priority, etc.). - /// public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index e733df5604d..3b5ef62ae35 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1512,25 +1512,6 @@ static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, Properties = attrInfo.Properties, IntentFilters = attrInfo.IntentFilters, MetaData = attrInfo.MetaData, - HasPublicDefaultConstructor = HasPublicParameterlessCtor (typeDef, index), }; } - - static bool HasPublicParameterlessCtor (TypeDefinition typeDef, AssemblyIndex index) - { - foreach (var methodHandle in typeDef.GetMethods ()) { - var method = index.Reader.GetMethodDefinition (methodHandle); - if (index.Reader.GetString (method.Name) != ".ctor") { - continue; - } - if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { - continue; - } - var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - if (sig.ParameterTypes.Length == 0) { - return true; - } - } - return false; - } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs index f6013c55707..bf5ae6caa0c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs @@ -1,23 +1,8 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Describes a [MetaData] attribute on a component type. -/// public sealed record MetaDataInfo { - /// - /// The metadata name (first constructor argument). - /// public required string Name { get; init; } - - /// - /// The Value property, if set. - /// public string? Value { get; init; } - - /// - /// The Resource property, if set. - /// 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 index 71f676f805b..ad43ab52157 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs @@ -1,4 +1,3 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs index 4425bfd5c09..e966abc7016 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs @@ -1,4 +1,3 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs index 2e6f5d27c40..4b8fa9cce46 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs @@ -1,4 +1,3 @@ - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs index df1f92dab58..3912449ff8d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs @@ -1,4 +1,3 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal sealed record PropertyInfo diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs index 7c4a6ac186e..86edd938f84 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs @@ -1,4 +1,3 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal sealed record UsesConfigurationInfo diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs index 63e538efad0..09459197778 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs @@ -1,20 +1,8 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// -/// Describes a [UsesFeature] attribute. -/// internal sealed record UsesFeatureInfo { - /// - /// Feature name (e.g., "android.hardware.camera"). Null for GL ES version features. - /// public string? Name { get; init; } - - /// - /// OpenGL ES version (e.g., 0x00020000 for 2.0). Zero for named features. - /// 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 index 06d7567ffb5..1e70b74232f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs @@ -1,4 +1,3 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal sealed record UsesLibraryInfo diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs index 35a4beff3a7..d11df2d8a3f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs @@ -1,4 +1,3 @@ - namespace Microsoft.Android.Sdk.TrimmableTypeMap; internal sealed record UsesPermissionInfo diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index b41fce30764..3b6f86aa98a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; @@ -84,7 +83,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 +107,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 +130,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 +160,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 +189,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 +210,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 +233,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 +255,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 +417,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 +441,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 +594,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, From 05818f89fae6464abf039f336094e41a7b2f256b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 15:07:42 +0100 Subject: [PATCH 7/8] Hoist ParseNameAndProperties, inline trivial Create methods, drop IO overload - Move ParseNameAndProperties(ca) call before the switch in ScanAssemblyAttributes so it runs once instead of being duplicated in every case - Inline trivial CreatePermissionInfo/CreatePermissionGroupInfo/CreatePermissionTreeInfo one-liners directly at the call site - Remove the file-path Generate() overload from ManifestGenerator; IO belongs in the caller - Fix leading blank line in ManifestGeneratorTests.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ManifestGenerator.cs | 17 ------ .../Scanner/AssemblyIndex.cs | 57 +++++-------------- .../Generator/ManifestGeneratorTests.cs | 7 ++- 3 files changed, 20 insertions(+), 61 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index a32da8b0fcc..1bea7928e30 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -34,23 +34,6 @@ class ManifestGenerator public string? ManifestPlaceholders { get; set; } public string? ApplicationJavaClass { get; set; } - /// - /// Generates the merged manifest and writes it to . - /// Returns the list of additional content provider names (for ApplicationRegistration.java). - /// - public IList Generate ( - string? manifestTemplatePath, - IReadOnlyList allPeers, - AssemblyManifestInfo assemblyInfo, - string outputPath) - { - XDocument? template = null; - if (!string.IsNullOrEmpty (manifestTemplatePath) && File.Exists (manifestTemplatePath)) { - template = XDocument.Load (manifestTemplatePath); - } - return Generate (template, allPeers, assemblyInfo, outputPath); - } - /// /// 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). diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index d57087992b5..641fb23b73d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -358,61 +358,43 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) continue; } + var (name, props) = ParseNameAndProperties (ca); + switch (attrName) { - case "PermissionAttribute": { - var (name, props) = ParseNameAndProperties (ca); - info.Permissions.Add (CreatePermissionInfo (name, props)); + case "PermissionAttribute": + info.Permissions.Add (new PermissionInfo { Name = name, Properties = props }); break; - } - case "PermissionGroupAttribute": { - var (name, props) = ParseNameAndProperties (ca); - info.PermissionGroups.Add (CreatePermissionGroupInfo (name, props)); + case "PermissionGroupAttribute": + info.PermissionGroups.Add (new PermissionGroupInfo { Name = name, Properties = props }); break; - } - case "PermissionTreeAttribute": { - var (name, props) = ParseNameAndProperties (ca); - info.PermissionTrees.Add (CreatePermissionTreeInfo (name, props)); + case "PermissionTreeAttribute": + info.PermissionTrees.Add (new PermissionTreeInfo { Name = name, Properties = props }); break; - } - case "UsesPermissionAttribute": { - var (name, props) = ParseNameAndProperties (ca); + case "UsesPermissionAttribute": info.UsesPermissions.Add (CreateUsesPermissionInfo (name, props)); break; - } - case "UsesFeatureAttribute": { - var (name, props) = ParseNameAndProperties (ca); + case "UsesFeatureAttribute": info.UsesFeatures.Add (CreateUsesFeatureInfo (name, props)); break; - } - case "UsesLibraryAttribute": { - var (name, props) = ParseNameAndProperties (ca); + case "UsesLibraryAttribute": info.UsesLibraries.Add (CreateUsesLibraryInfo (name, props)); break; - } - case "UsesConfigurationAttribute": { - var (_, props) = ParseNameAndProperties (ca); + case "UsesConfigurationAttribute": info.UsesConfigurations.Add (CreateUsesConfigurationInfo (props)); break; - } - case "MetaDataAttribute": { - var (name, props) = ParseNameAndProperties (ca); + case "MetaDataAttribute": info.MetaData.Add (CreateMetaDataInfo (name, props)); break; - } - case "PropertyAttribute": { - var (name, props) = ParseNameAndProperties (ca); + case "PropertyAttribute": info.Properties.Add (CreatePropertyInfo (name, props)); break; - } - case "ApplicationAttribute": { - var (_, props) = ParseNameAndProperties (ca); + case "ApplicationAttribute": info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal); foreach (var kvp in props) { info.ApplicationProperties [kvp.Key] = kvp.Value; } break; } - } } } @@ -435,15 +417,6 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) return (name, props); } - static PermissionInfo CreatePermissionInfo (string name, Dictionary props) - => new PermissionInfo { Name = name, Properties = props }; - - static PermissionGroupInfo CreatePermissionGroupInfo (string name, Dictionary props) - => new PermissionGroupInfo { Name = name, Properties = props }; - - static PermissionTreeInfo CreatePermissionTreeInfo (string name, Dictionary props) - => new PermissionTreeInfo { Name = name, Properties = props }; - static UsesPermissionInfo CreateUsesPermissionInfo (string name, Dictionary props) { int? maxSdk = props.TryGetValue ("MaxSdkVersion", out var v) && v is int max ? max : null; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 3b6f86aa98a..2653286fd20 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.IO; @@ -75,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); } From fd71439b8ae96223ea7ae85acea6cca9ed66a39e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 30 Mar 2026 14:08:34 +0200 Subject: [PATCH 8/8] Fix UsesLibrary 2-arg ctor losing Required flag UsesLibraryAttribute(string, bool) puts Required in FixedArguments[1]. ParseNameAndProperties now stores additional bool ctor args in props so CreateUsesLibraryInfo can read them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Scanner/AssemblyIndex.cs | 6 ++++++ .../Scanner/AssemblyAttributeScanningTests.cs | 10 ++++++++++ .../TestFixtures/AssemblyAttributes.cs | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/external/Java.Interop b/external/Java.Interop index 1d6cab64454..dc551974045 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 1d6cab64454808bb8077e9e18207c9d7059ff43c +Subproject commit dc5519740458097ef2cac753b21bd2e1459e5908 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 641fb23b73d..2d4803c8983 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -406,6 +406,12 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) 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; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs index 732f53677d1..d1f9d04f7df 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs @@ -46,6 +46,16 @@ 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] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs index 17493ed7ac3..f86a255829d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs @@ -5,4 +5,5 @@ [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")]