diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index ec5040ad515..d679f2becba 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit ec5040ad5158f240a67d887133dd56cfa5eb74ba +Subproject commit d679f2becbac319c1ef35934d40866d87996f7b0 diff --git a/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj b/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj index 996e0ddb2da..ff0ad2f58b4 100644 --- a/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj +++ b/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj @@ -3,6 +3,8 @@ $(DotNetAndroidTargetFramework) Exe HelloWorld + <_AndroidTypeMapImplementation>trimmable + false diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs new file mode 100644 index 00000000000..b25a1407a4f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs @@ -0,0 +1,130 @@ +#nullable enable + +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Converts Android enum integer values to their XML attribute string representations. +/// Ported from ManifestDocumentElement.cs. +/// +static class AndroidEnumConverter +{ + public static string? LaunchModeToString (int value) => value switch { + 1 => "singleTop", + 2 => "singleTask", + 3 => "singleInstance", + 4 => "singleInstancePerTask", + _ => null, + }; + + public static string? ScreenOrientationToString (int value) => value switch { + 0 => "landscape", + 1 => "portrait", + 3 => "sensor", + 4 => "nosensor", + 5 => "user", + 6 => "behind", + 7 => "reverseLandscape", + 8 => "reversePortrait", + 9 => "sensorLandscape", + 10 => "sensorPortrait", + 11 => "fullSensor", + 12 => "userLandscape", + 13 => "userPortrait", + 14 => "fullUser", + 15 => "locked", + -1 => "unspecified", + _ => null, + }; + + public static string? ConfigChangesToString (int value) + { + var parts = new List (); + if ((value & 0x0001) != 0) parts.Add ("mcc"); + if ((value & 0x0002) != 0) parts.Add ("mnc"); + if ((value & 0x0004) != 0) parts.Add ("locale"); + if ((value & 0x0008) != 0) parts.Add ("touchscreen"); + if ((value & 0x0010) != 0) parts.Add ("keyboard"); + if ((value & 0x0020) != 0) parts.Add ("keyboardHidden"); + if ((value & 0x0040) != 0) parts.Add ("navigation"); + if ((value & 0x0080) != 0) parts.Add ("orientation"); + if ((value & 0x0100) != 0) parts.Add ("screenLayout"); + if ((value & 0x0200) != 0) parts.Add ("uiMode"); + if ((value & 0x0400) != 0) parts.Add ("screenSize"); + if ((value & 0x0800) != 0) parts.Add ("smallestScreenSize"); + if ((value & 0x1000) != 0) parts.Add ("density"); + if ((value & 0x2000) != 0) parts.Add ("layoutDirection"); + if ((value & 0x4000) != 0) parts.Add ("colorMode"); + if ((value & 0x8000) != 0) parts.Add ("grammaticalGender"); + if ((value & 0x10000000) != 0) parts.Add ("fontWeightAdjustment"); + if ((value & 0x40000000) != 0) parts.Add ("fontScale"); + return parts.Count > 0 ? string.Join ("|", parts) : null; + } + + public static string? SoftInputToString (int value) + { + var parts = new List (); + int state = value & 0x0f; + int adjust = value & 0xf0; + if (state == 1) parts.Add ("stateUnchanged"); + else if (state == 2) parts.Add ("stateHidden"); + else if (state == 3) parts.Add ("stateAlwaysHidden"); + else if (state == 4) parts.Add ("stateVisible"); + else if (state == 5) parts.Add ("stateAlwaysVisible"); + if (adjust == 0x10) parts.Add ("adjustResize"); + else if (adjust == 0x20) parts.Add ("adjustPan"); + else if (adjust == 0x30) parts.Add ("adjustNothing"); + return parts.Count > 0 ? string.Join ("|", parts) : null; + } + + public static string? DocumentLaunchModeToString (int value) => value switch { + 1 => "intoExisting", + 2 => "always", + 3 => "never", + _ => null, + }; + + public static string? UiOptionsToString (int value) => value switch { + 1 => "splitActionBarWhenNarrow", + _ => null, + }; + + public static string? ForegroundServiceTypeToString (int value) + { + var parts = new List (); + if ((value & 0x00000001) != 0) parts.Add ("dataSync"); + if ((value & 0x00000002) != 0) parts.Add ("mediaPlayback"); + if ((value & 0x00000004) != 0) parts.Add ("phoneCall"); + if ((value & 0x00000008) != 0) parts.Add ("location"); + if ((value & 0x00000010) != 0) parts.Add ("connectedDevice"); + if ((value & 0x00000020) != 0) parts.Add ("mediaProjection"); + if ((value & 0x00000040) != 0) parts.Add ("camera"); + if ((value & 0x00000080) != 0) parts.Add ("microphone"); + if ((value & 0x00000100) != 0) parts.Add ("health"); + if ((value & 0x00000200) != 0) parts.Add ("remoteMessaging"); + if ((value & 0x00000400) != 0) parts.Add ("systemExempted"); + if ((value & 0x00000800) != 0) parts.Add ("shortService"); + if ((value & 0x40000000) != 0) parts.Add ("specialUse"); + return parts.Count > 0 ? string.Join ("|", parts) : null; + } + + public static string? ProtectionToString (int value) + { + int baseValue = value & 0x0f; + return baseValue switch { + 0 => "normal", + 1 => "dangerous", + 2 => "signature", + 3 => "signatureOrSystem", + _ => null, + }; + } + + public static string? ActivityPersistableModeToString (int value) => value switch { + 0 => "persistRootOnly", + 1 => "persistAcrossReboots", + 2 => "persistNever", + _ => null, + }; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs new file mode 100644 index 00000000000..8ba00724f87 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -0,0 +1,171 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Adds assembly-level manifest elements (permissions, uses-permissions, uses-features, +/// uses-library, uses-configuration, meta-data, property). +/// +static class AssemblyLevelElementBuilder +{ + static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs; + static readonly XName AttName = ManifestConstants.AttName; + + internal static void AddAssemblyLevelElements (XElement manifest, XElement app, AssemblyManifestInfo info) + { + var existingPermissions = new HashSet ( + manifest.Elements ("permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); + var existingUsesPermissions = new HashSet ( + manifest.Elements ("uses-permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); + + // elements + foreach (var perm in info.Permissions) { + if (string.IsNullOrEmpty (perm.Name) || existingPermissions.Contains (perm.Name)) { + continue; + } + var element = new XElement ("permission", new XAttribute (AttName, perm.Name)); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Label", "label"); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Description", "description"); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "PermissionGroup", "permissionGroup"); + PropertyMapper.MapDictionaryEnumProperty (element, perm.Properties, "ProtectionLevel", "protectionLevel", AndroidEnumConverter.ProtectionToString); + manifest.Add (element); + } + + // elements + foreach (var pg in info.PermissionGroups) { + if (string.IsNullOrEmpty (pg.Name)) { + continue; + } + var element = new XElement ("permission-group", new XAttribute (AttName, pg.Name)); + PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Label", "label"); + PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Description", "description"); + PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Icon", "icon"); + manifest.Add (element); + } + + // elements + foreach (var pt in info.PermissionTrees) { + if (string.IsNullOrEmpty (pt.Name)) { + continue; + } + var element = new XElement ("permission-tree", new XAttribute (AttName, pt.Name)); + PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Label", "label"); + PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Icon", "icon"); + manifest.Add (element); + } + + // elements + foreach (var up in info.UsesPermissions) { + if (string.IsNullOrEmpty (up.Name) || existingUsesPermissions.Contains (up.Name)) { + continue; + } + var element = new XElement ("uses-permission", new XAttribute (AttName, up.Name)); + if (up.MaxSdkVersion.HasValue) { + element.SetAttributeValue (AndroidNs + "maxSdkVersion", up.MaxSdkVersion.Value.ToString (CultureInfo.InvariantCulture)); + } + manifest.Add (element); + } + + // elements + var existingFeatures = new HashSet ( + manifest.Elements ("uses-feature").Select (e => (string?)e.Attribute (AttName)).OfType ()); + foreach (var uf in info.UsesFeatures) { + if (uf.Name is not null && !existingFeatures.Contains (uf.Name)) { + var element = new XElement ("uses-feature", + new XAttribute (AttName, uf.Name), + new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false")); + manifest.Add (element); + } else if (uf.GLESVersion != 0) { + var versionStr = $"0x{uf.GLESVersion:X8}"; + if (!manifest.Elements ("uses-feature").Any (e => (string?)e.Attribute (AndroidNs + "glEsVersion") == versionStr)) { + var element = new XElement ("uses-feature", + new XAttribute (AndroidNs + "glEsVersion", versionStr), + new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false")); + manifest.Add (element); + } + } + } + + // elements inside + foreach (var ul in info.UsesLibraries) { + if (string.IsNullOrEmpty (ul.Name)) { + continue; + } + if (!app.Elements ("uses-library").Any (e => (string?)e.Attribute (AttName) == ul.Name)) { + app.Add (new XElement ("uses-library", + new XAttribute (AttName, ul.Name), + new XAttribute (AndroidNs + "required", ul.Required ? "true" : "false"))); + } + } + + // Assembly-level inside + foreach (var md in info.MetaData) { + if (string.IsNullOrEmpty (md.Name)) { + continue; + } + if (!app.Elements ("meta-data").Any (e => (string?)e.Attribute (AndroidNs + "name") == md.Name)) { + app.Add (ComponentElementBuilder.CreateMetaDataElement (md)); + } + } + + // Assembly-level inside + foreach (var prop in info.Properties) { + if (string.IsNullOrEmpty (prop.Name)) { + continue; + } + if (!app.Elements ("property").Any (e => (string?)e.Attribute (AndroidNs + "name") == prop.Name)) { + var element = new XElement ("property", + new XAttribute (AndroidNs + "name", prop.Name)); + if (prop.Value is not null) { + element.SetAttributeValue (AndroidNs + "value", prop.Value); + } + if (prop.Resource is not null) { + element.SetAttributeValue (AndroidNs + "resource", prop.Resource); + } + app.Add (element); + } + } + + // elements + foreach (var uc in info.UsesConfigurations) { + var element = new XElement ("uses-configuration"); + if (uc.ReqFiveWayNav) { + element.SetAttributeValue (AndroidNs + "reqFiveWayNav", "true"); + } + if (uc.ReqHardKeyboard) { + element.SetAttributeValue (AndroidNs + "reqHardKeyboard", "true"); + } + if (uc.ReqKeyboardType is not null) { + element.SetAttributeValue (AndroidNs + "reqKeyboardType", uc.ReqKeyboardType); + } + if (uc.ReqNavigation is not null) { + element.SetAttributeValue (AndroidNs + "reqNavigation", uc.ReqNavigation); + } + if (uc.ReqTouchScreen is not null) { + element.SetAttributeValue (AndroidNs + "reqTouchScreen", uc.ReqTouchScreen); + } + manifest.Add (element); + } + } + + internal static void ApplyApplicationProperties (XElement app, Dictionary properties) + { + PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true); + } + + internal static void AddInternetPermission (XElement manifest) + { + if (!manifest.Elements ("uses-permission").Any (p => + (string?)p.Attribute (AndroidNs + "name") == "android.permission.INTERNET")) { + manifest.Add (new XElement ("uses-permission", + new XAttribute (AndroidNs + "name", "android.permission.INTERNET"))); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs new file mode 100644 index 00000000000..9f6f8e7f516 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -0,0 +1,184 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Builds XML elements for individual Android components (Activity, Service, BroadcastReceiver, ContentProvider). +/// +static class ComponentElementBuilder +{ + static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs; + static readonly XName AttName = ManifestConstants.AttName; + + internal static XElement? CreateComponentElement (JavaPeerInfo peer, string jniName) + { + var component = peer.ComponentAttribute; + if (component is null) { + return null; + } + + string elementName = component.Kind switch { + ComponentKind.Activity => "activity", + ComponentKind.Service => "service", + ComponentKind.BroadcastReceiver => "receiver", + ComponentKind.ContentProvider => "provider", + _ => throw new NotSupportedException ($"Unsupported component kind: {component.Kind}"), + }; + + var element = new XElement (elementName, new XAttribute (AttName, jniName)); + + // Map known properties to android: attributes + PropertyMapper.MapComponentProperties (element, component); + + // Add intent filters + foreach (var intentFilter in component.IntentFilters) { + element.Add (CreateIntentFilterElement (intentFilter)); + } + + // Handle MainLauncher for activities + if (component.Kind == ComponentKind.Activity && component.Properties.TryGetValue ("MainLauncher", out var ml) && ml is bool b && b) { + AddLauncherIntentFilter (element); + } + + // Add metadata + foreach (var meta in component.MetaData) { + element.Add (CreateMetaDataElement (meta)); + } + + return element; + } + + internal static void AddLauncherIntentFilter (XElement activity) + { + // Check if there's already a launcher intent filter + if (activity.Elements ("intent-filter").Any (f => + f.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.MAIN") && + f.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.LAUNCHER"))) { + return; + } + + // Add android:exported="true" if not already present + if (activity.Attribute (AndroidNs + "exported") is null) { + activity.Add (new XAttribute (AndroidNs + "exported", "true")); + } + + var filter = new XElement ("intent-filter", + new XElement ("action", new XAttribute (AttName, "android.intent.action.MAIN")), + new XElement ("category", new XAttribute (AttName, "android.intent.category.LAUNCHER"))); + activity.AddFirst (filter); + } + + internal static XElement CreateIntentFilterElement (IntentFilterInfo intentFilter) + { + var filter = new XElement ("intent-filter"); + + foreach (var action in intentFilter.Actions) { + filter.Add (new XElement ("action", new XAttribute (AttName, action))); + } + + foreach (var category in intentFilter.Categories) { + filter.Add (new XElement ("category", new XAttribute (AttName, category))); + } + + // Map IntentFilter properties to XML attributes + if (intentFilter.Properties.TryGetValue ("Label", out var label) && label is string labelStr) { + filter.SetAttributeValue (AndroidNs + "label", labelStr); + } + if (intentFilter.Properties.TryGetValue ("Icon", out var icon) && icon is string iconStr) { + filter.SetAttributeValue (AndroidNs + "icon", iconStr); + } + if (intentFilter.Properties.TryGetValue ("Priority", out var priority) && priority is int priorityInt) { + filter.SetAttributeValue (AndroidNs + "priority", priorityInt.ToString (CultureInfo.InvariantCulture)); + } + + // Data elements + AddIntentFilterDataElement (filter, intentFilter); + + return filter; + } + + internal static void AddIntentFilterDataElement (XElement filter, IntentFilterInfo intentFilter) + { + var dataElement = new XElement ("data"); + bool hasData = false; + + if (intentFilter.Properties.TryGetValue ("DataScheme", out var scheme) && scheme is string schemeStr) { + dataElement.SetAttributeValue (AndroidNs + "scheme", schemeStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataHost", out var host) && host is string hostStr) { + dataElement.SetAttributeValue (AndroidNs + "host", hostStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataPath", out var path) && path is string pathStr) { + dataElement.SetAttributeValue (AndroidNs + "path", pathStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataPathPattern", out var pattern) && pattern is string patternStr) { + dataElement.SetAttributeValue (AndroidNs + "pathPattern", patternStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataPathPrefix", out var prefix) && prefix is string prefixStr) { + dataElement.SetAttributeValue (AndroidNs + "pathPrefix", prefixStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataMimeType", out var mime) && mime is string mimeStr) { + dataElement.SetAttributeValue (AndroidNs + "mimeType", mimeStr); + hasData = true; + } + if (intentFilter.Properties.TryGetValue ("DataPort", out var port) && port is string portStr) { + dataElement.SetAttributeValue (AndroidNs + "port", portStr); + hasData = true; + } + + if (hasData) { + filter.Add (dataElement); + } + } + + internal static XElement CreateMetaDataElement (MetaDataInfo meta) + { + var element = new XElement ("meta-data", + new XAttribute (AndroidNs + "name", meta.Name)); + + if (meta.Value is not null) { + element.SetAttributeValue (AndroidNs + "value", meta.Value); + } + if (meta.Resource is not null) { + element.SetAttributeValue (AndroidNs + "resource", meta.Resource); + } + return element; + } + + internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) + { + string jniName = peer.JavaName.Replace ('/', '.'); + app.SetAttributeValue (AttName, jniName); + + var component = peer.ComponentAttribute; + if (component is null) { + return; + } + PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings); + } + + internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) + { + string jniName = peer.JavaName.Replace ('/', '.'); + var element = new XElement ("instrumentation", + new XAttribute (AttName, jniName)); + + var component = peer.ComponentAttribute; + if (component is null) { + return; + } + PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings); + + manifest.Add (element); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index abf66a4ffad..91670834b31 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -163,11 +163,14 @@ internal static void ValidateJniName (string jniName) /// /// Converts a JNI type name to a Java source type name. - /// e.g., "android/app/Activity" \u2192 "android.app.Activity" + /// JNI uses '/' for packages and '$' for inner classes. + /// Java source uses '.' for both. + /// e.g., "android/app/Activity" → "android.app.Activity" + /// e.g., "android/drm/DrmManagerClient$OnEventListener" → "android.drm.DrmManagerClient.OnEventListener" /// internal static string JniNameToJavaName (string jniName) { - return jniName.Replace ('/', '.'); + return jniName.Replace ('/', '.').Replace ('$', '.'); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs new file mode 100644 index 00000000000..4ed62e9baa6 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -0,0 +1,11 @@ +#nullable enable + +using System.Xml.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class ManifestConstants +{ + public static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android"; + public static readonly XName AttName = AndroidNs + "name"; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs new file mode 100644 index 00000000000..d3cdd147df4 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -0,0 +1,288 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Generates AndroidManifest.xml from component attributes captured by the JavaPeerScanner. +/// This is the trimmable-path equivalent of ManifestDocument — it works from ComponentInfo +/// records instead of Cecil TypeDefinitions. +/// +public class ManifestGenerator +{ + static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs; + static readonly XName AttName = ManifestConstants.AttName; + static readonly char [] PlaceholderSeparators = [';']; + + int appInitOrder = 2000000000; + + public string PackageName { get; set; } = ""; + public string ApplicationLabel { get; set; } = ""; + public string VersionCode { get; set; } = ""; + public string VersionName { get; set; } = ""; + public string MinSdkVersion { get; set; } = "21"; + public string TargetSdkVersion { get; set; } = "36"; + public string AndroidRuntime { get; set; } = "coreclr"; + public bool Debug { get; set; } + public bool NeedsInternet { get; set; } + public bool EmbedAssemblies { get; set; } + public bool ForceDebuggable { get; set; } + public bool ForceExtractNativeLibs { get; set; } + 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) + { + var doc = LoadOrCreateManifest (manifestTemplatePath); + var manifest = doc.Root; + if (manifest is null) { + throw new InvalidOperationException ("Manifest document has no root element."); + } + + EnsureManifestAttributes (manifest); + var app = EnsureApplicationElement (manifest); + + // Apply assembly-level [Application] properties + if (assemblyInfo.ApplicationProperties is not null) { + AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties); + } + + var existingTypes = new HashSet ( + app.Descendants ().Select (a => (string?)a.Attribute (AttName)).OfType ()); + + // Add components from scanned types + foreach (var peer in allPeers) { + if (peer.IsAbstract || peer.ComponentAttribute is null) { + continue; + } + + // Skip Application types (handled separately via assembly-level attribute) + if (peer.ComponentAttribute.Kind == ComponentKind.Application) { + ComponentElementBuilder.UpdateApplicationElement (app, peer); + continue; + } + + if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) { + ComponentElementBuilder.AddInstrumentation (manifest, peer); + continue; + } + + string jniName = peer.JavaName.Replace ('/', '.'); + if (existingTypes.Contains (jniName)) { + continue; + } + + var element = ComponentElementBuilder.CreateComponentElement (peer, jniName); + if (element is not null) { + app.Add (element); + } + } + + // Add assembly-level manifest elements + AssemblyLevelElementBuilder.AddAssemblyLevelElements (manifest, app, assemblyInfo); + + // Add runtime provider + var providerNames = AddRuntimeProviders (app); + + // Set ApplicationJavaClass + if (!string.IsNullOrEmpty (ApplicationJavaClass) && app.Attribute (AttName) is null) { + app.SetAttributeValue (AttName, ApplicationJavaClass); + } + + // Handle debuggable + bool needDebuggable = Debug && app.Attribute (AndroidNs + "debuggable") is null; + if (ForceDebuggable || needDebuggable) { + app.SetAttributeValue (AndroidNs + "debuggable", "true"); + } + + // Handle extractNativeLibs + if (ForceExtractNativeLibs) { + app.SetAttributeValue (AndroidNs + "extractNativeLibs", "true"); + } + + // Add internet permission for debug + if (Debug || NeedsInternet) { + AssemblyLevelElementBuilder.AddInternetPermission (manifest); + } + + // Apply manifest placeholders + string? placeholders = ManifestPlaceholders; + if (placeholders is not null && placeholders.Length > 0) { + ApplyPlaceholders (doc, placeholders); + } + + // Write output + var outputDir = Path.GetDirectoryName (outputPath); + if (outputDir is not null) { + Directory.CreateDirectory (outputDir); + } + doc.Save (outputPath); + + return providerNames; + } + + XDocument LoadOrCreateManifest (string? templatePath) + { + if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) { + return XDocument.Load (templatePath); + } + + return new XDocument ( + new XDeclaration ("1.0", "utf-8", null), + new XElement ("manifest", + new XAttribute (XNamespace.Xmlns + "android", AndroidNs.NamespaceName), + new XAttribute ("package", PackageName))); + } + + void EnsureManifestAttributes (XElement manifest) + { + manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName); + + if (string.IsNullOrEmpty ((string?)manifest.Attribute ("package"))) { + manifest.SetAttributeValue ("package", PackageName); + } + + if (manifest.Attribute (AndroidNs + "versionCode") is null) { + manifest.SetAttributeValue (AndroidNs + "versionCode", + string.IsNullOrEmpty (VersionCode) ? "1" : VersionCode); + } + + if (manifest.Attribute (AndroidNs + "versionName") is null) { + manifest.SetAttributeValue (AndroidNs + "versionName", + string.IsNullOrEmpty (VersionName) ? "1.0" : VersionName); + } + + // Add + if (!manifest.Elements ("uses-sdk").Any ()) { + manifest.AddFirst (new XElement ("uses-sdk", + new XAttribute (AndroidNs + "minSdkVersion", MinSdkVersion), + new XAttribute (AndroidNs + "targetSdkVersion", TargetSdkVersion))); + } + } + + XElement EnsureApplicationElement (XElement manifest) + { + var app = manifest.Element ("application"); + if (app is null) { + app = new XElement ("application"); + manifest.Add (app); + } + + if (app.Attribute (AndroidNs + "label") is null && !string.IsNullOrEmpty (ApplicationLabel)) { + app.SetAttributeValue (AndroidNs + "label", ApplicationLabel); + } + + return app; + } + + IList AddRuntimeProviders (XElement app) + { + string packageName = "mono"; + string className = "MonoRuntimeProvider"; + + if (string.Equals (AndroidRuntime, "nativeaot", StringComparison.OrdinalIgnoreCase)) { + packageName = "net.dot.jni.nativeaot"; + className = "NativeAotRuntimeProvider"; + } + + // Check if runtime provider already exists in template + string runtimeProviderName = $"{packageName}.{className}"; + if (!app.Elements ("provider").Any (p => { + var name = (string?)p.Attribute (ManifestConstants.AttName); + return name == runtimeProviderName || + ((string?)p.Attribute (AndroidNs.GetName ("authorities")))?.EndsWith (".__mono_init__", StringComparison.Ordinal) == true; + })) { + app.Add (CreateRuntimeProvider (runtimeProviderName, null, --appInitOrder)); + } + + var providerNames = new List (); + var processAttrName = AndroidNs.GetName ("process"); + var procs = new List (); + + foreach (var el in app.Elements ()) { + var proc = el.Attribute (processAttrName); + if (proc is null || procs.Contains (proc.Value)) { + continue; + } + if (el.Name.NamespaceName != "") { + continue; + } + switch (el.Name.LocalName) { + case "provider": + var autho = el.Attribute (AndroidNs.GetName ("authorities")); + if (autho is not null && autho.Value.EndsWith (".__mono_init__", StringComparison.Ordinal)) { + continue; + } + goto case "activity"; + case "activity": + case "receiver": + case "service": + procs.Add (proc.Value); + string providerName = $"{className}_{procs.Count}"; + providerNames.Add (providerName); + app.Add (CreateRuntimeProvider ($"{packageName}.{providerName}", proc.Value, --appInitOrder)); + break; + } + } + + return providerNames; + } + + XElement CreateRuntimeProvider (string name, string? processName, int initOrder) + { + return new XElement ("provider", + new XAttribute (AndroidNs + "name", name), + new XAttribute (AndroidNs + "exported", "false"), + new XAttribute (AndroidNs + "initOrder", initOrder), + processName is not null ? new XAttribute (AndroidNs + "process", processName) : null, + new XAttribute (AndroidNs + "authorities", PackageName + "." + name + ".__mono_init__")); + } + + /// + /// Replaces ${key} placeholders in all attribute values throughout the document. + /// Placeholder format: "key1=value1;key2=value2" + /// + static void ApplyPlaceholders (XDocument doc, string placeholders) + { + var replacements = new Dictionary (StringComparer.Ordinal); + foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { + var eqIndex = entry.IndexOf ('='); + if (eqIndex > 0) { + var key = entry.Substring (0, eqIndex).Trim (); + var value = entry.Substring (eqIndex + 1).Trim (); + replacements ["${" + key + "}"] = value; + } + } + + if (replacements.Count == 0) { + return; + } + + foreach (var element in doc.Descendants ()) { + foreach (var attr in element.Attributes ()) { + var val = attr.Value; + foreach (var kvp in replacements) { + if (val.Contains (kvp.Key)) { + val = val.Replace (kvp.Key, kvp.Value); + } + } + if (val != attr.Value) { + attr.Value = val; + } + } + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs new file mode 100644 index 00000000000..32b07671600 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs @@ -0,0 +1,200 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Xml.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Defines the property mapping infrastructure for converting +/// properties to Android manifest XML attributes. +/// +static class PropertyMapper +{ + static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs; + + internal enum MappingKind { String, Bool, Enum } + + internal readonly struct PropertyMapping + { + public string PropertyName { get; } + public string XmlAttributeName { get; } + public MappingKind Kind { get; } + public Func? EnumConverter { get; } + + public PropertyMapping (string propertyName, string xmlAttributeName, MappingKind kind = MappingKind.String, Func? enumConverter = null) + { + PropertyName = propertyName; + XmlAttributeName = xmlAttributeName; + Kind = kind; + EnumConverter = enumConverter; + } + } + + internal static readonly PropertyMapping[] CommonMappings = [ + new ("Label", "label"), + new ("Description", "description"), + new ("Icon", "icon"), + new ("RoundIcon", "roundIcon"), + new ("Permission", "permission"), + new ("Process", "process"), + new ("Enabled", "enabled", MappingKind.Bool), + new ("DirectBootAware", "directBootAware", MappingKind.Bool), + new ("Exported", "exported", MappingKind.Bool), + ]; + + internal static readonly PropertyMapping[] ActivityMappings = [ + new ("Theme", "theme"), + new ("ParentActivity", "parentActivityName"), + new ("TaskAffinity", "taskAffinity"), + new ("AllowTaskReparenting", "allowTaskReparenting", MappingKind.Bool), + new ("AlwaysRetainTaskState", "alwaysRetainTaskState", MappingKind.Bool), + new ("ClearTaskOnLaunch", "clearTaskOnLaunch", MappingKind.Bool), + new ("ExcludeFromRecents", "excludeFromRecents", MappingKind.Bool), + new ("FinishOnCloseSystemDialogs", "finishOnCloseSystemDialogs", MappingKind.Bool), + new ("FinishOnTaskLaunch", "finishOnTaskLaunch", MappingKind.Bool), + new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool), + new ("NoHistory", "noHistory", MappingKind.Bool), + new ("MultiProcess", "multiprocess", MappingKind.Bool), + new ("StateNotNeeded", "stateNotNeeded", MappingKind.Bool), + new ("Immersive", "immersive", MappingKind.Bool), + new ("ResizeableActivity", "resizeableActivity", MappingKind.Bool), + new ("SupportsPictureInPicture", "supportsPictureInPicture", MappingKind.Bool), + new ("ShowForAllUsers", "showForAllUsers", MappingKind.Bool), + new ("TurnScreenOn", "turnScreenOn", MappingKind.Bool), + new ("LaunchMode", "launchMode", MappingKind.Enum, AndroidEnumConverter.LaunchModeToString), + new ("ScreenOrientation", "screenOrientation", MappingKind.Enum, AndroidEnumConverter.ScreenOrientationToString), + new ("ConfigurationChanges", "configChanges", MappingKind.Enum, AndroidEnumConverter.ConfigChangesToString), + new ("WindowSoftInputMode", "windowSoftInputMode", MappingKind.Enum, AndroidEnumConverter.SoftInputToString), + new ("DocumentLaunchMode", "documentLaunchMode", MappingKind.Enum, AndroidEnumConverter.DocumentLaunchModeToString), + new ("UiOptions", "uiOptions", MappingKind.Enum, AndroidEnumConverter.UiOptionsToString), + new ("PersistableMode", "persistableMode", MappingKind.Enum, AndroidEnumConverter.ActivityPersistableModeToString), + ]; + + internal static readonly PropertyMapping[] ServiceMappings = [ + new ("IsolatedProcess", "isolatedProcess", MappingKind.Bool), + new ("ForegroundServiceType", "foregroundServiceType", MappingKind.Enum, AndroidEnumConverter.ForegroundServiceTypeToString), + ]; + + internal static readonly PropertyMapping[] ContentProviderMappings = [ + new ("Authorities", "authorities"), + new ("GrantUriPermissions", "grantUriPermissions", MappingKind.Bool), + new ("Syncable", "syncable", MappingKind.Bool), + new ("MultiProcess", "multiprocess", MappingKind.Bool), + ]; + + internal static readonly PropertyMapping[] ApplicationElementMappings = [ + new ("Label", "label"), + new ("Icon", "icon"), + new ("RoundIcon", "roundIcon"), + new ("Theme", "theme"), + new ("AllowBackup", "allowBackup", MappingKind.Bool), + new ("SupportsRtl", "supportsRtl", MappingKind.Bool), + new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool), + new ("LargeHeap", "largeHeap", MappingKind.Bool), + new ("Debuggable", "debuggable", MappingKind.Bool), + new ("UsesCleartextTraffic", "usesCleartextTraffic", MappingKind.Bool), + ]; + + internal static readonly PropertyMapping[] InstrumentationMappings = [ + new ("Label", "label"), + new ("Icon", "icon"), + new ("TargetPackage", "targetPackage"), + new ("FunctionalTest", "functionalTest", MappingKind.Bool), + new ("HandleProfiling", "handleProfiling", MappingKind.Bool), + ]; + + internal static readonly PropertyMapping[] ApplicationPropertyMappings = [ + new ("Label", "label"), + new ("Icon", "icon"), + new ("RoundIcon", "roundIcon"), + new ("Theme", "theme"), + new ("NetworkSecurityConfig", "networkSecurityConfig"), + new ("Description", "description"), + new ("Logo", "logo"), + new ("Permission", "permission"), + new ("Process", "process"), + new ("TaskAffinity", "taskAffinity"), + new ("AllowBackup", "allowBackup", MappingKind.Bool), + new ("SupportsRtl", "supportsRtl", MappingKind.Bool), + new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool), + new ("LargeHeap", "largeHeap", MappingKind.Bool), + new ("Debuggable", "debuggable", MappingKind.Bool), + new ("UsesCleartextTraffic", "usesCleartextTraffic", MappingKind.Bool), + new ("RestoreAnyVersion", "restoreAnyVersion", MappingKind.Bool), + ]; + + internal static void ApplyMappings (XElement element, IReadOnlyDictionary properties, PropertyMapping[] mappings, bool skipExisting = false) + { + foreach (var m in mappings) { + if (!properties.TryGetValue (m.PropertyName, out var value) || value is null) { + continue; + } + if (skipExisting && element.Attribute (AndroidNs + m.XmlAttributeName) is not null) { + continue; + } + switch (m.Kind) { + case MappingKind.String when value is string s && !string.IsNullOrEmpty (s): + element.SetAttributeValue (AndroidNs + m.XmlAttributeName, s); + break; + case MappingKind.Bool when value is bool b: + element.SetAttributeValue (AndroidNs + m.XmlAttributeName, b ? "true" : "false"); + break; + case MappingKind.Enum when m.EnumConverter is not null: + int intValue = value switch { int i => i, long l => (int)l, short s => s, byte b => b, _ => 0 }; + var strValue = m.EnumConverter (intValue); + if (strValue is not null) { + element.SetAttributeValue (AndroidNs + m.XmlAttributeName, strValue); + } + break; + } + } + } + + internal static void MapComponentProperties (XElement element, ComponentInfo component) + { + ApplyMappings (element, component.Properties, CommonMappings); + + var extra = component.Kind switch { + ComponentKind.Activity => ActivityMappings, + ComponentKind.Service => ServiceMappings, + ComponentKind.ContentProvider => ContentProviderMappings, + _ => null, + }; + if (extra is not null) { + ApplyMappings (element, component.Properties, extra); + } + + // Handle InitOrder for ContentProvider (int, not a standard mapping) + if (component.Kind == ComponentKind.ContentProvider && component.Properties.TryGetValue ("InitOrder", out var initOrder) && initOrder is int order) { + element.SetAttributeValue (AndroidNs + "initOrder", order.ToString (CultureInfo.InvariantCulture)); + } + } + + internal static void MapDictionaryProperties (XElement element, IReadOnlyDictionary props, string propertyName, string xmlAttrName) + { + if (props.TryGetValue (propertyName, out var value) && value is string s && !string.IsNullOrEmpty (s)) { + element.SetAttributeValue (AndroidNs + xmlAttrName, s); + } + } + + internal static void MapDictionaryEnumProperty (XElement element, IReadOnlyDictionary props, string propertyName, string xmlAttrName, Func converter) + { + if (!props.TryGetValue (propertyName, out var value)) { + return; + } + int intValue = value switch { + int i => i, + long l => (int)l, + short s => s, + byte b => b, + _ => 0, + }; + var strValue = converter (intValue); + if (strValue is not null) { + element.SetAttributeValue (AndroidNs + xmlAttrName, strValue); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 775f48af607..3c696c33eda 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -20,7 +20,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias /// /// // One proxy type per Java peer that needs activation or UCO wrappers: -/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only +/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only /// { /// public Activity_Proxy() : base() { } /// @@ -33,7 +33,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// // or: null; // no activation /// // or: throw new NotSupportedException(...); // open generic /// -/// public override Type TargetType => typeof(Activity); +/// // TargetType and GetContainerFactory() are inherited from JavaPeerProxy /// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only /// /// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only): @@ -43,13 +43,13 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) -/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// => TrimmableTypeMap.ActivateInstance(self, typeof(Activity)); /// /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) /// { -/// TrimmableNativeRegistration.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); -/// TrimmableNativeRegistration.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); +/// TrimmableTypeMap.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); +/// TrimmableTypeMap.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); /// } /// } /// @@ -75,11 +75,10 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _trimmableNativeRegistrationRef; + TypeReferenceHandle _trimmableTypeMapRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; - MemberReferenceHandle _baseCtorRef; MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; @@ -169,7 +168,7 @@ void EmitTypeReferences () { var metadata = _pe.Metadata; _javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy`1")); _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, @@ -188,8 +187,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); + _trimmableTypeMapRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -198,9 +197,6 @@ void EmitTypeReferences () void EmitMemberReferences () { - _baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().Type (_systemTypeRef, false), @@ -232,7 +228,7 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", + _activateInstanceRef = _pe.AddMemberRef (_trimmableTypeMapRef, "ActivateInstance", sig => sig.MethodSignature ().Parameters (2, rt => rt.Void (), p => { @@ -240,7 +236,7 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - _registerMethodRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "RegisterMethod", + _registerMethodRef = _pe.AddMemberRef (_trimmableTypeMapRef, "RegisterMethod", sig => sig.MethodSignature ().Parameters (4, rt => rt.Void (), p => { @@ -315,11 +311,16 @@ void EmitTypeMapAssociationAttributeCtorRef () void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { var metadata = _pe.Metadata; + + // Create JavaPeerProxy as the base class + var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); + var genericBaseSpec = _pe.MakeGenericTypeSpec (_javaPeerProxyRef, targetTypeRef); + var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, metadata.GetOrAddString (proxy.Namespace), metadata.GetOrAddString (proxy.TypeName), - _javaPeerProxyRef, + genericBaseSpec, MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); @@ -327,24 +328,27 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary..ctor() + var genericBaseCtorRef = _pe.AddMemberRef (genericBaseSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + var ctorDefHandle = _pe.EmitBody (".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { encoder.OpCode (ILOpCode.Ldarg_0); - encoder.Call (_baseCtorRef); + encoder.Call (genericBaseCtorRef); encoder.OpCode (ILOpCode.Ret); }); + // Self-application: [ProxyType] on ProxyType — lets GetCustomAttribute() instantiate it + metadata.AddCustomAttribute (typeDefHandle, ctorDefHandle, _pe.BuildAttributeBlob (_ => { })); + // CreateInstance EmitCreateInstance (proxy); - // get_TargetType - EmitTypeGetter ("get_TargetType", proxy.TargetType, - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + // get_TargetType and GetContainerFactory() are inherited from JavaPeerProxy - // get_InvokerType + // get_InvokerType — only for interfaces/abstract types if (proxy.InvokerType != null) { EmitTypeGetter ("get_InvokerType", proxy.InvokerType, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); 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/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 9fceaeaa3ac..a393240474e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -117,6 +117,12 @@ public sealed record JavaPeerInfo /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// public bool IsGenericDefinition { get; init; } + + /// + /// Android component attribute data ([Activity], [Service], [BroadcastReceiver], [ContentProvider], + /// [Application], [Instrumentation]) if present on this type. Used for manifest generation. + /// + public ComponentInfo? ComponentAttribute { get; init; } } /// @@ -309,3 +315,178 @@ public enum ActivationCtorStyle /// JavaInterop, } + +/// +/// 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; } +} + +/// +/// 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 (); +} + +/// +/// 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; } +} + +/// +/// Assembly-level manifest attributes collected from all scanned assemblies. +/// Aggregated across assemblies — used to generate top-level manifest elements +/// like ]]>, ]]>, etc. +/// +public 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; } +} + +public sealed record PermissionInfo +{ + public required string Name { get; init; } + public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); +} + +public sealed record PermissionGroupInfo +{ + public required string Name { get; init; } + public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); +} + +public sealed record PermissionTreeInfo +{ + public required string Name { get; init; } + public IReadOnlyDictionary Properties { get; init; } = new Dictionary (); +} + +public sealed record UsesPermissionInfo +{ + public required string Name { get; init; } + public int? MaxSdkVersion { get; init; } +} + +public 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; +} + +public sealed record UsesLibraryInfo +{ + public required string Name { get; init; } + public bool Required { get; init; } = true; +} + +public 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; } +} + +public 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/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f81c0b0b87e..c394fc7288b 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 . + /// + public 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/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 8a2e443883a..3a68feb8bbc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -44,19 +44,21 @@ internal static void Initialize () var previous = Interlocked.CompareExchange (ref s_instance, instance, null); Debug.Assert (previous is null, "TrimmableTypeMap must only be created once."); - instance.RegisterBootstrapNativeMethod (); + instance.RegisterNatives (); } /// /// Registers the mono.android.Runtime.registerNatives JNI native method. - /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. /// - void RegisterBootstrapNativeMethod () + unsafe void RegisterNatives () { using var runtimeClass = new JniType ("mono/android/Runtime"); + var registration = new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", + Marshal.GetDelegateForFunctionPointer> ( + (IntPtr)(delegate* unmanaged)&OnRegisterNatives)); JniEnvironment.Types.RegisterNatives ( runtimeClass.PeerReference, - [new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", s_onRegisterNatives)], + [registration], 1); } @@ -197,10 +199,7 @@ public static void RegisterMethod (JniType nativeClass, string name, string sign 1); } - static readonly RegisterNativesHandler s_onRegisterNatives = OnRegisterNatives; - - delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); - + [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { string? className = null; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml new file mode 100644 index 00000000000..8c62546764a --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets index 8b49df77580..9a5e479837c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.MonoVM.targets @@ -17,6 +17,10 @@ This file contains the MonoVM-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index b4ecd0f5fe6..7119b475f1d 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -44,6 +44,10 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index a06b771e122..8897c697ac1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -1,15 +1,67 @@ - + - - + + - <_ResolvedAssemblies Include="@(_GeneratedTypeMapAssemblies)" /> + + %(Filename)%(Extension) + + + + + + + + <_ExtraTrimmerArgs>--typemap-entry-assembly $(_TypeMapAssemblyName) $(_ExtraTrimmerArgs) + + + + + + + <_CurrentAbi>%(_BuildTargetAbis.Identity) + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'arm64-v8a' ">android-arm64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'armeabi-v7a' ">android-arm + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86_64' ">android-x64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86' ">android-x86 + + + + <_CurrentLinkedTypeMapDlls Include="$(IntermediateOutputPath)$(_CurrentRid)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)$(_CurrentRid)/linked/_Microsoft.Android.TypeMap*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentLinkedTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentLinkedTypeMapDlls.Filename)%(_CurrentLinkedTypeMapDlls.Extension) + $(_CurrentAbi)/ + + + + + <_CurrentTypeMapDlls Include="$(_TypeMapOutputDirectory)*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentTypeMapDlls.Filename)%(_CurrentTypeMapDlls.Extension) + $(_CurrentAbi)/ + + + + <_CurrentLinkedTypeMapDlls Remove="@(_CurrentLinkedTypeMapDlls)" /> + <_CurrentTypeMapDlls Remove="@(_CurrentTypeMapDlls)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index b056316d7db..a1010df0e93 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -1,9 +1,8 @@ - + + @@ -12,94 +11,170 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps - <_TypeMapOutputDirectory>$(IntermediateOutputPath)typemap\ - <_TypeMapJavaOutputDirectory>$(IntermediateOutputPath)typemap\java - <_PerAssemblyAcwMapDirectory>$(IntermediateOutputPath)acw-maps\ + <_TypeMapBaseOutputDir>$(IntermediateOutputPath) + <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) + <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ + <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - + + + - + Condition=" '$(_AndroidRuntime)' != 'CoreCLR' And '$(_AndroidRuntime)' != 'NativeAOT' " + BeforeTargets="Build"> + - - + + + + + <_TypeMapInputAssemblies Include="@(ReferencePath)" /> + <_TypeMapInputAssemblies Include="@(ResolvedAssemblies)" /> + <_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" /> + <_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)" + Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" /> + + AcwMapDirectory="$(_TypeMapBaseOutputDir)acw-maps/" + TargetFrameworkVersion="$(TargetFrameworkVersion)" + ManifestTemplate="$(_AndroidManifestAbs)" + MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml" + PackageName="$(_AndroidPackage)" + ApplicationLabel="$(_ApplicationLabel)" + VersionCode="$(_AndroidVersionCode)" + VersionName="$(_AndroidVersionName)" + AndroidApiLevel="$(_AndroidApiLevel)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" + AndroidRuntime="$(_AndroidRuntime)" + Debug="$(AndroidIncludeDebugSymbols)" + NeedsInternet="$(AndroidNeedsInternetPermission)" + EmbedAssemblies="$(EmbedAssembliesIntoApk)" + ManifestPlaceholders="$(AndroidManifestPlaceholders)" + CheckedBuild="$(_AndroidCheckedBuild)" + ApplicationJavaClass="$(AndroidApplicationJavaClass)"> - + - + - - - - + + - <_PerAssemblyAcwMapFiles Remove="@(_PerAssemblyAcwMapFiles)" /> - <_PerAssemblyAcwMapFiles Include="$(_PerAssemblyAcwMapDirectory)*.txt" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)\**\*.java" /> - + - - + + + <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) + <_TypeMapFirstAbi Condition=" '$(_TypeMapFirstAbi)' == '' ">arm64-v8a + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'arm64-v8a' ">android-arm64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'armeabi-v7a' ">android-arm + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86_64' ">android-x64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 + + + + + <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + + + + + - - - + + + + + Lines="package net.dot.android%3b;public class ApplicationRegistration { public static android.content.Context Context%3b public static void registerApplications () { } }" /> + + + + + + + + + + <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> + + + + + - + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs new file mode 100644 index 00000000000..9c0df0c8ed9 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs @@ -0,0 +1,97 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// Generates empty native typemap LLVM IR stub files (typemap.{abi}.ll) for the trimmable typemap path. +/// These are compiled by the native toolchain to provide the type_map and related symbols that libmonodroid.so expects. +/// +public class GenerateEmptyTypemapStub : AndroidTask +{ + public override string TaskPrefix => "GETS"; + + [Required] + public string OutputDirectory { get; set; } = ""; + + [Required] + public ITaskItem [] Abis { get; set; } = []; + + public bool Debug { get; set; } + + [Output] + public ITaskItem []? Sources { get; set; } + + public override bool RunTask () + { + Directory.CreateDirectory (OutputDirectory); + var sources = new List (); + + foreach (var abi in Abis) { + string abiName = abi.ItemSpec; + string stubPath = Path.Combine (OutputDirectory, $"typemap.{abiName}.ll"); + Files.CopyIfStringChanged (GenerateStubLlvmIr (abiName), stubPath); + var item = new TaskItem (stubPath); + item.SetMetadata ("abi", abiName); + sources.Add (item); + } + + Sources = sources.ToArray (); + return !Log.HasLoggedErrors; + } + + string GenerateStubLlvmIr (string abi) + { + var (triple, datalayout) = abi switch { + "arm64-v8a" => ("aarch64-unknown-linux-android21", "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128"), + "x86_64" => ("x86_64-unknown-linux-android21", "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"), + "armeabi-v7a" => ("armv7-unknown-linux-androideabi21", "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64"), + "x86" => ("i686-unknown-linux-android21", "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128"), + _ => throw new NotSupportedException ($"Unsupported ABI: {abi}"), + }; + + string header = $""" + ; ModuleID = 'typemap.{abi}.ll' + source_filename = "typemap.{abi}.ll" + target datalayout = "{datalayout}" + target triple = "{triple}" + + """; + + if (Debug) { + return header + """ + %struct.TypeMap = type { i32, i32, ptr, ptr } + %struct.TypeMapManagedTypeInfo = type { i64, i32, i32 } + %struct.TypeMapAssembly = type { i64 } + + @type_map = dso_local constant %struct.TypeMap zeroinitializer, align 8 + @typemap_use_hashes = dso_local constant i8 1, align 1 + @type_map_managed_type_info = dso_local constant [0 x %struct.TypeMapManagedTypeInfo] zeroinitializer, align 8 + @type_map_unique_assemblies = dso_local constant [0 x %struct.TypeMapAssembly] zeroinitializer, align 8 + @type_map_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 + @type_map_managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 + @type_map_java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 + """; + } + + return header + """ + @managed_to_java_map_module_count = dso_local constant i32 0, align 4 + @managed_to_java_map = dso_local constant [0 x i8] zeroinitializer, align 8 + @java_to_managed_map = dso_local constant [0 x i8] zeroinitializer, align 8 + @java_to_managed_hashes = dso_local constant [0 x i64] zeroinitializer, align 8 + @modules_map_data = dso_local constant [0 x i8] zeroinitializer, align 8 + @modules_duplicates_data = dso_local constant [0 x i8] zeroinitializer, align 8 + @java_type_count = dso_local constant i32 0, align 4 + @java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 + @java_type_names_size = dso_local constant i64 0, align 8 + @managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 + @managed_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 + """; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index a17945d8cee..7b01f6f08e5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -334,7 +334,21 @@ void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_ } if (android_runtime_jnienv_class_token == -1 || jnienv_initialize_method_token == -1 || jnienv_registerjninatives_method_token == -1) { - throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens for {assemblyFilePath}"); + if (!TargetsCLR) { + throw new InvalidOperationException ($"Required JNIEnvInit tokens not found in '{assemblyFilePath}' (class={android_runtime_jnienv_class_token}, init={jnienv_initialize_method_token}, register={jnienv_registerjninatives_method_token})."); + } + + // In the trimmable typemap path (CoreCLR), some JNIEnvInit methods may be trimmed. + // Use token 0 for missing tokens — native code will skip them. + if (jnienv_registerjninatives_method_token == -1) { + jnienv_registerjninatives_method_token = 0; + } + if (jnienv_initialize_method_token == -1) { + jnienv_initialize_method_token = 0; + } + if (android_runtime_jnienv_class_token == -1) { + android_runtime_jnienv_class_token = 0; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index ecb6529357f..290017c7334 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -12,9 +12,9 @@ namespace Xamarin.Android.Tasks; /// -/// Generates trimmable TypeMap assemblies, JCW Java source files, and per-assembly -/// acw-map files from resolved assemblies. The acw-map files are later merged into -/// a single acw-map.txt consumed by _ConvertCustomView for layout XML fixups. +/// Generates trimmable TypeMap assemblies and JCW Java source files from resolved assemblies. +/// Runs before the trimmer to produce per-assembly typemap .dll files and a root +/// _Microsoft.Android.TypeMaps.dll, plus .java files for ACW types with registerNatives. /// public class GenerateTrimmableTypeMap : AndroidTask { @@ -29,9 +29,6 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string JavaSourceOutputDirectory { get; set; } = ""; - /// - /// Directory for per-assembly acw-map.{AssemblyName}.txt files. - /// [Required] public string AcwMapDirectory { get; set; } = ""; @@ -42,6 +39,66 @@ public class GenerateTrimmableTypeMap : AndroidTask [Required] public string TargetFrameworkVersion { get; set; } = ""; + /// + /// User's AndroidManifest.xml template. May be null if no template exists. + /// + public string? ManifestTemplate { get; set; } + + /// + /// Output path for the merged AndroidManifest.xml. + /// + public string? MergedAndroidManifestOutput { get; set; } + + /// + /// Android package name (e.g., "com.example.myapp"). + /// + public string? PackageName { get; set; } + + /// + /// Application label for the manifest. + /// + public string? ApplicationLabel { get; set; } + + public string? VersionCode { get; set; } + + public string? VersionName { get; set; } + + /// + /// Target Android API level (e.g., "36"). + /// + public string? AndroidApiLevel { get; set; } + + /// + /// Supported OS platform version (e.g., "21.0"). + /// + public string? SupportedOSPlatformVersion { get; set; } + + /// + /// Android runtime type ("mono", "coreclr", "nativeaot"). + /// + public string? AndroidRuntime { get; set; } + + public bool Debug { get; set; } + + public bool NeedsInternet { get; set; } + + public bool EmbedAssemblies { get; set; } + + /// + /// Manifest placeholder values (e.g., "applicationId=com.example.app;key=value"). + /// + public string? ManifestPlaceholders { get; set; } + + /// + /// When set, forces android:debuggable="true" and android:extractNativeLibs="true". + /// + public string? CheckedBuild { get; set; } + + /// + /// Optional custom Application Java class name. + /// + public string? ApplicationJavaClass { get; set; } + [Output] public ITaskItem []? GeneratedAssemblies { get; set; } @@ -49,31 +106,58 @@ public class GenerateTrimmableTypeMap : AndroidTask public ITaskItem []? GeneratedJavaFiles { get; set; } /// - /// Per-assembly acw-map files produced during scanning. Each file contains - /// three lines per type: PartialAssemblyQualifiedName;JavaKey, - /// ManagedKey;JavaKey, and CompatJniName;JavaKey. + /// Content provider names for ApplicationRegistration.java. /// [Output] - public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } + public string []? AdditionalProviderSources { get; set; } public override bool RunTask () { var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); - var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies); + // Don't filter by HasMonoAndroidReference — ReferencePath items from the compiler + // don't carry this metadata. The scanner handles non-Java assemblies gracefully. + var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); + + // Framework binding types (Activity, View, etc.) already exist in java_runtime.dex and don't + // need JCW .java files. Framework Implementor types (mono/ prefix, e.g. OnClickListenerImplementor) + // DO need JCWs — they're included via the mono/ filter below. + // User NuGet libraries also need JCWs, so we only filter by FrameworkReferenceName. + // Note: Pre-generating SDK-compatible JCWs (mono.android-trimmable.jar) is tracked by #10792. + var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + foreach (var item in ResolvedAssemblies) { + if (!item.GetMetadata ("FrameworkReferenceName").IsNullOrEmpty ()) { + frameworkAssemblyNames.Add (Path.GetFileNameWithoutExtension (item.ItemSpec)); + } + } Directory.CreateDirectory (OutputDirectory); Directory.CreateDirectory (JavaSourceOutputDirectory); Directory.CreateDirectory (AcwMapDirectory); - var allPeers = ScanAssemblies (assemblyPaths); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblyPaths); if (allPeers.Count == 0) { Log.LogDebugMessage ("No Java peer types found, skipping typemap generation."); return !Log.HasLoggedErrors; } GeneratedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths); - GeneratedJavaFiles = GenerateJcwJavaSources (allPeers); - PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (allPeers); + + // Generate JCW .java files for user assemblies + framework Implementor types. + // Framework binding types already have compiled JCWs in the SDK but their constructors + // use the legacy TypeManager.Activate() JNI native which isn't available in the + // trimmable runtime. Implementor types (View_OnClickListenerImplementor, etc.) are + // in the mono.* Java package so we use the mono/ prefix to identify them. + // We generate fresh JCWs that use Runtime.registerNatives() for activation. + var jcwPeers = allPeers.Where (p => + !frameworkAssemblyNames.Contains (p.AssemblyName) + || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); + Log.LogDebugMessage ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); + GeneratedJavaFiles = GenerateJcwJavaSources (jcwPeers); + + // Generate manifest if output path is configured + if (!MergedAndroidManifestOutput.IsNullOrEmpty () && !PackageName.IsNullOrEmpty ()) { + GenerateManifest (allPeers, assemblyManifestInfo); + } return !Log.HasLoggedErrors; } @@ -85,12 +169,13 @@ public override bool RunTask () // resolution (base types, interfaces, activation ctors). // 2. Cache scan results per assembly to skip PE I/O entirely for unchanged assemblies. // Both require profiling to determine if they meaningfully improve build times. - List ScanAssemblies (IReadOnlyList assemblyPaths) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList assemblyPaths) { using var scanner = new JavaPeerScanner (); var peers = scanner.Scan (assemblyPaths); + var manifestInfo = scanner.ScanAssemblyManifestInfo (); Log.LogDebugMessage ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types."); - return peers; + return (peers, manifestInfo); } ITaskItem [] GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, @@ -169,36 +254,49 @@ ITaskItem [] GenerateJcwJavaSources (List allPeers) return items; } - ITaskItem [] GeneratePerAssemblyAcwMaps (List allPeers) + void GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo) { - var peersByAssembly = allPeers - .GroupBy (p => p.AssemblyName, StringComparer.Ordinal) - .OrderBy (g => g.Key, StringComparer.Ordinal); - - var outputFiles = new List (); + if (PackageName is null || MergedAndroidManifestOutput is null) { + return; + } - foreach (var group in peersByAssembly) { - var peers = group.ToList (); - string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt"); - - bool written; - using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { - AcwMapWriter.Write (sw, peers); - sw.Flush (); - written = Files.CopyIfStreamChanged (sw.BaseStream, outputFile); - } + // Validate components + ValidateComponents (allPeers, assemblyManifestInfo); + if (Log.HasLoggedErrors) { + return; + } - Log.LogDebugMessage (written - ? $" acw-map.{group.Key}.txt: {peers.Count} types" - : $" acw-map.{group.Key}.txt: unchanged"); + string minSdk = "21"; + if (!SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (SupportedOSPlatformVersion, out var sopv)) { + minSdk = sopv.Major.ToString (); + } - var item = new TaskItem (outputFile); - item.SetMetadata ("AssemblyName", group.Key); - outputFiles.Add (item); + string targetSdk = AndroidApiLevel ?? "36"; + if (Version.TryParse (targetSdk, out var apiVersion)) { + targetSdk = apiVersion.Major.ToString (); } - Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files."); - return outputFiles.ToArray (); + bool forceDebuggable = !CheckedBuild.IsNullOrEmpty (); + + var generator = new ManifestGenerator { + PackageName = PackageName, + ApplicationLabel = ApplicationLabel ?? PackageName, + VersionCode = VersionCode ?? "", + VersionName = VersionName ?? "", + MinSdkVersion = minSdk, + TargetSdkVersion = targetSdk, + AndroidRuntime = AndroidRuntime ?? "coreclr", + Debug = Debug, + NeedsInternet = NeedsInternet, + EmbedAssemblies = EmbedAssemblies, + ForceDebuggable = forceDebuggable, + ForceExtractNativeLibs = forceDebuggable, + ManifestPlaceholders = ManifestPlaceholders, + ApplicationJavaClass = ApplicationJavaClass, + }; + + var providerNames = generator.Generate (ManifestTemplate, allPeers, assemblyManifestInfo, MergedAndroidManifestOutput); + AdditionalProviderSources = providerNames.ToArray (); } static Version ParseTargetFrameworkVersion (string tfv) @@ -212,6 +310,37 @@ static Version ParseTargetFrameworkVersion (string tfv) throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); } + void ValidateComponents (List allPeers, AssemblyManifestInfo assemblyManifestInfo) + { + // XA4213: component types must have a public parameterless constructor + foreach (var peer in allPeers) { + if (peer.ComponentAttribute is null || peer.IsAbstract) { + continue; + } + if (!peer.ComponentAttribute.HasPublicDefaultConstructor) { + Log.LogCodedError ("XA4213", Properties.Resources.XA4213, peer.ManagedTypeName); + } + } + + // Validate only one Application type + var applicationTypes = new List (); + foreach (var peer in allPeers) { + if (peer.ComponentAttribute?.Kind == ComponentKind.Application && !peer.IsAbstract) { + applicationTypes.Add (peer.ManagedTypeName); + } + } + + bool hasAssemblyLevelApplication = assemblyManifestInfo.ApplicationProperties is not null; + // These match the legacy ManifestDocument behavior (InvalidOperationException with same messages). + // No XA error code — legacy doesn't have one either. + if (applicationTypes.Count > 1) { + Log.LogError ("There can be only one type with an [Application] attribute; found: " + + string.Join (", ", applicationTypes)); + } else if (applicationTypes.Count > 0 && hasAssemblyLevelApplication) { + Log.LogError ("Application cannot have both a type with an [Application] attribute and an [assembly:Application] attribute."); + } + } + /// /// Filters resolved assemblies to only those that reference Mono.Android or Java.Interop /// (i.e., assemblies that could contain [Register] types). Skips BCL assemblies. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs index d94e932b8ce..406087e8564 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ComparisonDiffHelper.cs @@ -170,4 +170,204 @@ public static (List interfaceMismatches, List abstractMismatches return (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches); } + + public static (List missingComponents, List extraComponents, List kindMismatches, List nameMismatches, List propertyMismatches) CompareComponentAttributes ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.UnionWith (newData.Keys); + + var missingComponents = new List (); + var extraComponents = new List (); + var kindMismatches = new List (); + var nameMismatches = new List (); + var propertyMismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var inLegacy = legacyData.TryGetValue (managedName, out var legacy); + var inNew = newData.TryGetValue (managedName, out var newInfo); + + if (inLegacy && !inNew) { + missingComponents.Add ($"{managedName}: {(legacy?.ComponentKind ?? "(null)")}"); + continue; + } + + if (!inLegacy && inNew) { + extraComponents.Add ($"{managedName}: {(newInfo?.ComponentKind ?? "(null)")}"); + continue; + } + + if (legacy == null || newInfo == null) { + continue; + } + + if (legacy.ComponentKind != newInfo.ComponentKind) { + kindMismatches.Add ($"{managedName}: legacy='{legacy.ComponentKind ?? "(null)"}' new='{newInfo.ComponentKind ?? "(null)"}'"); + } + + if (legacy.ComponentName != newInfo.ComponentName) { + nameMismatches.Add ($"{managedName}: legacy='{legacy.ComponentName ?? "(null)"}' new='{newInfo.ComponentName ?? "(null)"}'"); + } + + // Compare component properties + var legacyPropSet = new HashSet (legacy.ComponentProperties, System.StringComparer.Ordinal); + var newPropSet = new HashSet (newInfo.ComponentProperties, System.StringComparer.Ordinal); + + foreach (var prop in legacyPropSet.Except (newPropSet)) { + propertyMismatches.Add ($"{managedName}: missing property '{prop}'"); + } + + foreach (var prop in newPropSet.Except (legacyPropSet)) { + propertyMismatches.Add ($"{managedName}: extra property '{prop}'"); + } + } + + return (missingComponents, extraComponents, kindMismatches, nameMismatches, propertyMismatches); + } + + public static (List missingPermissions, List extraPermissions, List missingFeatures, List extraFeatures) CompareAssemblyManifestAttributes ( + ManifestAttributeComparisonData legacy, + ManifestAttributeComparisonData newData) + { + var legacyPermSet = new HashSet (legacy.UsesPermissions, System.StringComparer.Ordinal); + var newPermSet = new HashSet (newData.UsesPermissions, System.StringComparer.Ordinal); + + var missingPermissions = legacyPermSet.Except (newPermSet) + .Select (p => $"uses-permission: {p}") + .OrderBy (s => s, System.StringComparer.Ordinal) + .ToList (); + + var extraPermissions = newPermSet.Except (legacyPermSet) + .Select (p => $"uses-permission: {p}") + .OrderBy (s => s, System.StringComparer.Ordinal) + .ToList (); + + var legacyFeatSet = new HashSet (legacy.UsesFeatures, System.StringComparer.Ordinal); + var newFeatSet = new HashSet (newData.UsesFeatures, System.StringComparer.Ordinal); + + var missingFeatures = legacyFeatSet.Except (newFeatSet) + .Select (f => $"uses-feature: {f}") + .OrderBy (s => s, System.StringComparer.Ordinal) + .ToList (); + + var extraFeatures = newFeatSet.Except (legacyFeatSet) + .Select (f => $"uses-feature: {f}") + .OrderBy (s => s, System.StringComparer.Ordinal) + .ToList (); + + return (missingPermissions, extraPermissions, missingFeatures, extraFeatures); + } + + public static List CompareCompatJniNames ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (!string.Equals (legacy.CompatJniName, newInfo.CompatJniName, System.StringComparison.Ordinal)) { + mismatches.Add ($"{managedName}: legacy='{legacy.CompatJniName}' new='{newInfo.CompatJniName}'"); + } + } + + return mismatches; + } + + public static List CompareCannotRegisterInStaticCtor ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + if (legacy.CannotRegisterInStaticConstructor != newInfo.CannotRegisterInStaticConstructor) { + mismatches.Add ($"{managedName}: legacy={legacy.CannotRegisterInStaticConstructor} new={newInfo.CannotRegisterInStaticConstructor}"); + } + } + + return mismatches; + } + + public static List CompareInvokerTypes ( + Dictionary legacyData, + Dictionary newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacy = legacyData [managedName]; + var newInfo = newData [managedName]; + + // The legacy [Register] attribute only stores InvokerTypeName for + // interface types (as the third ctor arg). Abstract class invokers + // are resolved differently and not available from Cecil attributes. + // Only compare when legacy has a value. + if (legacy.InvokerTypeName == null && newInfo.InvokerTypeName != null) { + continue; + } + + if (!string.Equals (legacy.InvokerTypeName, newInfo.InvokerTypeName, System.StringComparison.Ordinal)) { + mismatches.Add ($"{managedName}: legacy='{legacy.InvokerTypeName ?? "(null)"}' new='{newInfo.InvokerTypeName ?? "(null)"}'"); + } + } + + return mismatches; + } + + public static List CompareConstructorSuperArgs ( + Dictionary> legacyData, + Dictionary> newData) + { + var allManagedNames = new HashSet (legacyData.Keys); + allManagedNames.IntersectWith (newData.Keys); + + var mismatches = new List (); + + foreach (var managedName in allManagedNames.OrderBy (n => n, System.StringComparer.Ordinal)) { + var legacyCtors = legacyData [managedName]; + var newCtors = newData [managedName]; + + var legacyBySig = new Dictionary (System.StringComparer.Ordinal); + foreach (var c in legacyCtors) { + legacyBySig [c.JniSignature] = c.SuperArgs; + } + + var newBySig = new Dictionary (System.StringComparer.Ordinal); + foreach (var c in newCtors) { + newBySig [c.JniSignature] = c.SuperArgs; + } + + foreach (var sig in legacyBySig.Keys.Intersect (newBySig.Keys)) { + var legacyArgs = legacyBySig [sig]; + var newArgs = newBySig [sig]; + + // Only compare when the new scanner reports non-null SuperArgs ([Export] constructors) + if (newArgs == null) { + continue; + } + + if (!string.Equals (legacyArgs, newArgs, System.StringComparison.Ordinal)) { + mismatches.Add ($"{managedName} {sig}: legacy='{legacyArgs ?? "(null)"}' new='{newArgs}'"); + } + } + } + + return mismatches; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs index cea308ee3bc..3ba81eec021 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MarshalMethodDiffHelper.cs @@ -162,4 +162,157 @@ static void CompareUserTypeMethodGroup ( } } } + + public static List CompareExportFlags ( + Dictionary> legacyMethods, + Dictionary> newMethods) + { + var mismatches = new List (); + + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.IntersectWith (newMethods.Keys); + + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacyGroups = legacyMethods [javaName]; + var newGroups = newMethods [javaName]; + + var legacyByManaged = legacyGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + var legacyMethodList = legacyByManaged [managedName]; + var newMethodList = newByManaged [managedName]; + + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lm = legacyByKey [key]; + var nm = newByKey [key]; + + if (lm.IsExport != nm.IsExport) { + mismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy.IsExport={lm.IsExport} new.IsExport={nm.IsExport}"); + } + } + } + } + + return mismatches; + } + + public static List CompareNativeCallbackNames ( + Dictionary> legacyMethods, + Dictionary> newMethods) + { + var mismatches = new List (); + + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.IntersectWith (newMethods.Keys); + + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacyGroups = legacyMethods [javaName]; + var newGroups = newMethods [javaName]; + + var legacyByManaged = legacyGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + var legacyMethodList = legacyByManaged [managedName]; + var newMethodList = newByManaged [managedName]; + + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lm = legacyByKey [key]; + var nm = newByKey [key]; + + // Skip constructors — legacy uses "n_ClassName" format while + // new scanner uses "n_ctor", which are different by design + if (key.JniName == ".ctor") { + continue; + } + + var lc = lm.NativeCallbackName ?? ""; + var nc = nm.NativeCallbackName ?? ""; + + // DoNotGenerateAcw types and interfaces use direct [Register] + // attribute extraction which doesn't produce NativeCallbackName. + // Only compare when legacy has a value. + if (lc == "") { + continue; + } + + // Legacy format: "n_jniMethodName" + // New format: "n_ManagedMethodName_EncodedParams" + // The naming conventions differ by design — legacy uses JNI names + // while new scanner uses managed names. Just verify both have a + // callback and start with "n_". + if (nc == "") { + mismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new=(missing)"); + } else if (!nc.StartsWith ("n_", StringComparison.Ordinal)) { + mismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} new callback '{nc}' does not start with 'n_'"); + } + } + } + } + + return mismatches; + } + + public static List CompareDeclaringTypes ( + Dictionary> legacyMethods, + Dictionary> newMethods) + { + var mismatches = new List (); + + var allJavaNames = new HashSet (legacyMethods.Keys); + allJavaNames.IntersectWith (newMethods.Keys); + + foreach (var javaName in allJavaNames.OrderBy (n => n, StringComparer.Ordinal)) { + var legacyGroups = legacyMethods [javaName]; + var newGroups = newMethods [javaName]; + + var legacyByManaged = legacyGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + var newByManaged = newGroups.ToDictionary (g => g.ManagedName, g => g.Methods); + + foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) { + var legacyMethodList = legacyByManaged [managedName]; + var newMethodList = newByManaged [managedName]; + + var legacyByKey = legacyMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + var newByKey = newMethodList + .GroupBy (m => (m.JniName, m.JniSignature)) + .ToDictionary (g => g.Key, g => g.First ()); + + foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) { + var lm = legacyByKey [key]; + var nm = newByKey [key]; + + // The legacy pipeline doesn't track declaring types for + // interface method implementations. Only compare when + // legacy has a non-empty value. + if (lm.DeclaringTypeName == "" && nm.DeclaringTypeName != "") { + continue; + } + + if (lm.DeclaringTypeName != nm.DeclaringTypeName) { + mismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lm.DeclaringTypeName}' new='{nm.DeclaringTypeName}'"); + } + } + } + } + + return mismatches; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs index 5014fe28a4c..a97ec4c662b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs @@ -137,4 +137,225 @@ static void AssertNoDiffs (string label, List items) var details = string.Join (Environment.NewLine, items.Take (20).Select (item => $" {item}")); Assert.Fail ($"{label} ({items.Count}){Environment.NewLine}{details}"); } + + [Fact] + public void Scanner_NonPeerAssembly_ProducesEmptyResults () + { + // Scan an assembly with no Java peers (the test assembly itself has no [Register] types) + var testAssemblyPath = typeof (ScannerComparisonTests).Assembly.Location; + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { testAssemblyPath }); + + // The test assembly has no Java peer types — scan should succeed with empty results + Assert.Empty (peers); + } + + [Fact] + public void ImplementorTypes_HaveCorrectMonoPrefix () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (AllAssemblyPaths); + + // Find implementor types — these follow the pattern *_*Implementor + var implementors = peers.Where (p => + p.ManagedTypeName.Contains ("Implementor", StringComparison.Ordinal) && + !p.IsInterface).ToList (); + + Assert.True (implementors.Count > 10, + $"Expected >10 implementor types in Mono.Android, got {implementors.Count}"); + + // Verify all implementors have the mono/ package prefix in their JNI name + var missingPrefix = implementors + .Where (p => !p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)) + .Select (p => $"{p.ManagedTypeName} → {p.JavaName}") + .ToList (); + + AssertNoDiffs ("IMPLEMENTORS MISSING mono/ PREFIX", missingPrefix); + } + + [Fact] + public void ExactComponentAttributes_MonoAndroid () + { + var legacyData = TypeDataBuilder.BuildLegacyComponentData (MonoAndroidAssemblyPath); + var newData = TypeDataBuilder.BuildNewComponentData (AllAssemblyPaths); + + // Mono.Android is a binding assembly — most types have DoNotGenerateAcw=true + // and no component attributes, so counts may be low. Just verify parity. + var (missing, extra, kindMismatches, nameMismatches, propertyMismatches) = ComparisonDiffHelper.CompareComponentAttributes (legacyData, newData); + + AssertNoDiffs ("COMPONENTS MISSING from new scanner", missing); + AssertNoDiffs ("COMPONENTS EXTRA in new scanner", extra); + AssertNoDiffs ("COMPONENT KIND MISMATCHES", kindMismatches); + AssertNoDiffs ("COMPONENT NAME MISMATCHES", nameMismatches); + AssertNoDiffs ("COMPONENT PROPERTY MISMATCHES", propertyMismatches); + } + + [Fact] + public void ExactComponentAttributes_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var legacyData = TypeDataBuilder.BuildLegacyComponentData (paths! [0]); + var newData = TypeDataBuilder.BuildNewComponentData (paths); + var (missing, extra, kindMismatches, nameMismatches, propertyMismatches) = ComparisonDiffHelper.CompareComponentAttributes (legacyData, newData); + + AssertNoDiffs ("COMPONENTS MISSING from new scanner", missing); + AssertNoDiffs ("COMPONENTS EXTRA in new scanner", extra); + AssertNoDiffs ("COMPONENT KIND MISMATCHES", kindMismatches); + AssertNoDiffs ("COMPONENT NAME MISMATCHES", nameMismatches); + AssertNoDiffs ("COMPONENT PROPERTY MISMATCHES", propertyMismatches); + } + + [Fact] + public void ExactAssemblyManifestAttributes_MonoAndroid () + { + var legacy = TypeDataBuilder.BuildLegacyManifestData (MonoAndroidAssemblyPath); + var newData = TypeDataBuilder.BuildNewManifestData (AllAssemblyPaths); + + var (missingPerms, extraPerms, missingFeats, extraFeats) = + ComparisonDiffHelper.CompareAssemblyManifestAttributes (legacy, newData); + + AssertNoDiffs ("USES-PERMISSION MISSING from new scanner", missingPerms); + AssertNoDiffs ("USES-PERMISSION EXTRA in new scanner", extraPerms); + AssertNoDiffs ("USES-FEATURE MISSING from new scanner", missingFeats); + AssertNoDiffs ("USES-FEATURE EXTRA in new scanner", extraFeats); + } + + [Fact] + public void ExactJavaFields_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var primaryAssemblyName = Path.GetFileNameWithoutExtension (paths! [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (paths); + + var peersWithFields = peers + .Where (p => p.AssemblyName == primaryAssemblyName && p.JavaFields.Count > 0) + .ToList (); + + // The FieldExporter fixture type should have at least one field + Assert.True (peersWithFields.Count > 0, + "Expected at least one user type with JavaFields (FieldExporter)"); + + foreach (var peer in peersWithFields) { + foreach (var field in peer.JavaFields) { + // Every field should have required properties populated + Assert.False (string.IsNullOrEmpty (field.FieldName), + $"{peer.ManagedTypeName}: JavaField has empty FieldName"); + Assert.False (string.IsNullOrEmpty (field.JavaTypeName), + $"{peer.ManagedTypeName}: JavaField '{field.FieldName}' has empty JavaTypeName"); + Assert.False (string.IsNullOrEmpty (field.InitializerMethodName), + $"{peer.ManagedTypeName}: JavaField '{field.FieldName}' has empty InitializerMethodName"); + Assert.False (string.IsNullOrEmpty (field.Visibility), + $"{peer.ManagedTypeName}: JavaField '{field.FieldName}' has empty Visibility"); + } + } + } + + [Fact] + public void ExportMethod_UserTypesFixture_IsDiscovered () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var primaryAssemblyName = Path.GetFileNameWithoutExtension (paths! [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (paths); + + var exportPeer = peers.FirstOrDefault (p => + p.AssemblyName == primaryAssemblyName && + p.ManagedTypeName.Contains ("ExportWithThrows", StringComparison.Ordinal)); + Assert.NotNull (exportPeer); + + var exportMethod = exportPeer.MarshalMethods.FirstOrDefault (m => m.IsExport && m.JniName == "riskyOperation"); + Assert.NotNull (exportMethod); + Assert.Equal ("()V", exportMethod.JniSignature); + } + + [Fact] + public void ExactCompatJniNames_MonoAndroid () + { + var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); + var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + + Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); + Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + + var mismatches = ComparisonDiffHelper.CompareCompatJniNames (legacyData, newData); + + AssertNoDiffs ("COMPAT JNI NAME MISMATCHES", mismatches); + } + + [Fact] + public void ExactNativeCallbackNames_MonoAndroid () + { + var (_, legacyMethods) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); + var (_, newMethods) = ScannerRunner.RunNew (AllAssemblyPaths); + + Assert.True (legacyMethods.Count > 500, $"Expected >500 legacy method groups, got {legacyMethods.Count}"); + Assert.True (newMethods.Count > 500, $"Expected >500 new method groups, got {newMethods.Count}"); + + var mismatches = MarshalMethodDiffHelper.CompareNativeCallbackNames (legacyMethods, newMethods); + + AssertNoDiffs ("NATIVE CALLBACK NAME MISMATCHES", mismatches); + } + + [Fact] + public void ExactDeclaringTypes_MonoAndroid () + { + var (_, legacyMethods) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); + var (_, newMethods) = ScannerRunner.RunNew (AllAssemblyPaths); + + Assert.True (legacyMethods.Count > 500, $"Expected >500 legacy method groups, got {legacyMethods.Count}"); + Assert.True (newMethods.Count > 500, $"Expected >500 new method groups, got {newMethods.Count}"); + + var mismatches = MarshalMethodDiffHelper.CompareDeclaringTypes (legacyMethods, newMethods); + + AssertNoDiffs ("DECLARING TYPE MISMATCHES", mismatches); + } + + [Fact] + public void ExactConstructorSuperArgs_UserTypesFixture () + { + var paths = AllUserTypesAssemblyPaths; + Assert.NotNull (paths); + + var legacyData = TypeDataBuilder.BuildLegacyConstructorSuperArgs (paths! [0]); + var newData = TypeDataBuilder.BuildNewConstructorSuperArgs (paths); + + var mismatches = ComparisonDiffHelper.CompareConstructorSuperArgs (legacyData, newData); + + AssertNoDiffs ("CONSTRUCTOR SUPER ARGS MISMATCHES", mismatches); + } + + [Fact] + public void ExactInvokerTypes_MonoAndroid () + { + var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); + var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + + Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); + Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + + var mismatches = ComparisonDiffHelper.CompareInvokerTypes (legacyData, newData); + + AssertNoDiffs ("INVOKER TYPE MISMATCHES", mismatches); + } + + [Fact] + public void ExactCannotRegisterInStaticCtor_MonoAndroid () + { + var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); + var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + + Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); + Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + + var mismatches = ComparisonDiffHelper.CompareCannotRegisterInStaticCtor (legacyData, newData); + + AssertNoDiffs ("CANNOT REGISTER IN STATIC CTOR MISMATCHES", mismatches); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs index 3b0d79e43d8..319c4872c3c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs @@ -11,6 +11,10 @@ public void ExactTypeMap_MonoAndroid () { var (legacy, _) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); var (newEntries, _) = ScannerRunner.RunNew (AllAssemblyPaths); + +Assert.True (legacy.Count > 8500, $"Expected >8500 legacy type map entries, got {legacy.Count}"); +Assert.True (newEntries.Count > 8500, $"Expected >8500 new type map entries, got {newEntries.Count}"); + AssertTypeMapMatch (legacy, newEntries); } @@ -19,6 +23,10 @@ public void ExactMarshalMethods_MonoAndroid () { var (_, legacyMethods) = ScannerRunner.RunLegacy (MonoAndroidAssemblyPath); var (_, newMethods) = ScannerRunner.RunNew (AllAssemblyPaths); + +Assert.True (legacyMethods.Count > 500, $"Expected >500 legacy method groups, got {legacyMethods.Count}"); +Assert.True (newMethods.Count > 500, $"Expected >500 new method groups, got {newMethods.Count}"); + var result = MarshalMethodDiffHelper.CompareMarshalMethods (legacyMethods, newMethods); AssertNoDiffs ("MANAGED TYPES MISSING from new scanner", result.MissingTypes); @@ -26,6 +34,9 @@ public void ExactMarshalMethods_MonoAndroid () AssertNoDiffs ("METHODS MISSING from new scanner", result.MissingMethods); AssertNoDiffs ("METHODS EXTRA in new scanner", result.ExtraMethods); AssertNoDiffs ("CONNECTOR MISMATCHES", result.ConnectorMismatches); + +var exportMismatches = MarshalMethodDiffHelper.CompareExportFlags (legacyMethods, newMethods); +AssertNoDiffs ("EXPORT FLAG MISMATCHES", exportMismatches); } [Fact] @@ -36,7 +47,7 @@ public void ScannerDiagnostics_MonoAndroid () var interfaces = peers.Count (p => p.IsInterface); var totalMethods = peers.Sum (p => p.MarshalMethods.Count); -Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}"); +Assert.True (peers.Count > 8500, $"Expected >8500 types, got {peers.Count}"); Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}"); Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}"); } @@ -46,6 +57,10 @@ public void ExactBaseJavaNames_MonoAndroid () { var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + +Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); +Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + var mismatches = ComparisonDiffHelper.CompareBaseJavaNames (legacyData, newData); AssertNoDiffs ("BASE JAVA NAME MISMATCHES", mismatches); @@ -56,6 +71,10 @@ public void ExactImplementedInterfaces_MonoAndroid () { var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + +Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); +Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + var (missingInterfaces, extraInterfaces) = ComparisonDiffHelper.CompareImplementedInterfaces (legacyData, newData); AssertNoDiffs ("INTERFACES MISSING from new scanner", missingInterfaces); @@ -67,6 +86,10 @@ public void ExactActivationCtors_MonoAndroid () { var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + +Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); +Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + var (presenceMismatches, declaringTypeMismatches, styleMismatches) = ComparisonDiffHelper.CompareActivationCtors (legacyData, newData); AssertNoDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches); @@ -79,6 +102,10 @@ public void ExactJavaConstructors_MonoAndroid () { var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + +Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); +Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + var (missingCtors, extraCtors) = ComparisonDiffHelper.CompareJavaConstructors (legacyData, newData); AssertNoDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors); @@ -90,6 +117,10 @@ public void ExactTypeFlags_MonoAndroid () { var (legacyData, _) = TypeDataBuilder.BuildLegacy (MonoAndroidAssemblyPath); var newData = TypeDataBuilder.BuildNew (AllAssemblyPaths); + +Assert.True (legacyData.Count > 8500, $"Expected >8500 legacy type data entries, got {legacyData.Count}"); +Assert.True (newData.Count > 8500, $"Expected >8500 new type data entries, got {newData.Count}"); + var (interfaceMismatches, abstractMismatches, genericMismatches, acwMismatches) = ComparisonDiffHelper.CompareTypeFlags (legacyData, newData); AssertNoDiffs ("IsInterface MISMATCHES", interfaceMismatches); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index 10d4cea2f57..e55f67f78c1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -12,7 +12,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged); -record MethodEntry (string JniName, string JniSignature, string? Connector); +record MethodEntry (string JniName, string JniSignature, string? Connector, bool IsExport = false, string? JavaAccess = null, string? NativeCallbackName = null, string DeclaringTypeName = ""); record TypeMethodGroup (string ManagedName, List Methods); @@ -104,7 +104,7 @@ public static (List entries, Dictionary new MethodEntry (m.JniName, m.JniSignature, m.Connector)) + .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector, m.IsExport, m.JavaAccess, m.NativeCallbackName, m.DeclaringTypeName)) .OrderBy (m => m.JniName, StringComparer.Ordinal) .ThenBy (m => m.JniSignature, StringComparer.Ordinal) .ToList () @@ -161,14 +161,33 @@ static List ExtractMethodRegistrations (TypeDefinition typeDef, Typ var wrapper = CecilImporter.CreateType (typeDef, cache); var methods = new List (); + // Build a set of Java method names that come from [Export] attributes + var exportedJavaNames = new HashSet (StringComparer.Ordinal); + foreach (var method in typeDef.Methods) { + if (!method.HasCustomAttributes) { + continue; + } + foreach (var attr in method.CustomAttributes) { + if (attr.AttributeType.FullName == "Java.Interop.ExportAttribute") { + string? exportName = attr.ConstructorArguments.Count > 0 + ? attr.ConstructorArguments [0].Value as string + : null; + exportedJavaNames.Add (exportName ?? method.Name); + } + } + } + foreach (var m in wrapper.Methods) { // Extract connector from Method string "n_name:sig:connector" string? connector = ParseConnectorFromMethodString (m.Method); - methods.Add (new MethodEntry (m.JavaName, m.JniSignature, connector)); + string? nativeCallback = ParseNativeCallbackFromMethodString (m.Method); + bool isExport = exportedJavaNames.Contains (m.JavaName); + methods.Add (new MethodEntry (m.JavaName, m.JniSignature, connector, isExport, NativeCallbackName: nativeCallback)); } foreach (var c in wrapper.Constructors) { - methods.Add (new MethodEntry (".ctor", c.JniSignature, null)); + string? nativeCallback = ParseNativeCallbackFromMethodString (c.Method); + methods.Add (new MethodEntry (".ctor", c.JniSignature, null, NativeCallbackName: nativeCallback)); } return methods; @@ -251,4 +270,20 @@ static List ExtractDirectRegisterAttributes (TypeDefinition typeDef var connector = methodStr.Substring (secondColon + 1); return connector.Replace ('+', '/'); } + + /// + /// Parses the native callback name from a CallableWrapperMethod.Method string. + /// Format: "n_{name}:{signature}:{connector}" — returns "n_{name}". + /// + static string? ParseNativeCallbackFromMethodString (string? methodStr) + { + if (methodStr is null) { + return null; + } + int firstColon = methodStr.IndexOf (':'); + if (firstColon < 0) { + return methodStr; + } + return methodStr.Substring (0, firstColon); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs index d003eb73e07..41bd03bd0b6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -11,6 +11,18 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; +record ComponentComparisonData ( + string ManagedName, + string? ComponentKind, + string? ComponentName, + IReadOnlyList ComponentProperties +); + +record ManifestAttributeComparisonData ( + IReadOnlyList UsesPermissions, + IReadOnlyList UsesFeatures +); + record TypeComparisonData ( string ManagedName, string JavaName, @@ -23,11 +35,34 @@ record TypeComparisonData ( bool IsInterface, bool IsAbstract, bool IsGenericDefinition, - bool DoNotGenerateAcw + bool DoNotGenerateAcw, + string CompatJniName, + bool CannotRegisterInStaticConstructor, + string? InvokerTypeName +); + +record ConstructorSuperArgData ( + string JniSignature, + string? SuperArgs ); static class TypeDataBuilder { + /// + /// Encodes a uses-permission as "name" or "name;maxSdkVersion=N" for richer comparison. + /// + static string EncodePermission (string name, int? maxSdkVersion) + => maxSdkVersion.HasValue ? $"{name};maxSdkVersion={maxSdkVersion.Value}" : name; + + /// + /// Encodes a uses-feature as "name;required=true/false" or "glEsVersion=0xNNNN;required=true/false". + /// + static string EncodeFeature (string? name, int glesVersion, bool required) + { + var key = name ?? $"glEsVersion=0x{glesVersion:X8}"; + return $"{key};required={required}"; + } + public static (Dictionary perType, List entries) BuildLegacy (string assemblyPath) { var cache = new TypeDefinitionCache (); @@ -97,13 +132,29 @@ public static (Dictionary perType, List= 3) { + var invoker = attr.ConstructorArguments [2].Value as string; + if (!string.IsNullOrEmpty (invoker)) { + invokerTypeName = invoker; + } + } + } + } + // Use the real legacy JCW pipeline (CecilImporter.CreateType) to extract // Java constructors, including the base ctor chain and parameterless fallback. // This matches what the actual build does, unlike the previous manual [Register] // attribute scanning which only found directly-attributed ctors. + bool cannotRegisterInStaticCtor = false; var javaCtorSignatures = new List (); if (!typeDef.IsInterface && !ScannerRunner.HasDoNotGenerateAcw (typeDef)) { var wrapper = CecilImporter.CreateType (typeDef, cache); + cannotRegisterInStaticCtor = wrapper.CannotRegisterInStaticConstructor; foreach (var ctor in wrapper.Constructors) { if (!string.IsNullOrEmpty (ctor.JniSignature)) { javaCtorSignatures.Add (ctor.JniSignature); @@ -126,7 +177,10 @@ public static (Dictionary perType, List BuildNew (string[] assembly peer.IsInterface, peer.IsAbstract && !peer.IsInterface, peer.IsGenericDefinition, - peer.DoNotGenerateAcw + peer.DoNotGenerateAcw, + peer.CompatJniName, + peer.CannotRegisterInStaticConstructor, + peer.InvokerTypeName ); } return perType; } + public static Dictionary> BuildLegacyConstructorSuperArgs (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var result = new Dictionary> (StringComparer.Ordinal); + + foreach (var typeDef in javaTypes) { + if (typeDef.IsInterface || ScannerRunner.HasDoNotGenerateAcw (typeDef)) { + continue; + } + + var managedName = ScannerRunner.GetManagedName (typeDef); + var wrapper = CecilImporter.CreateType (typeDef, cache); + var ctors = new List (); + + foreach (var ctor in wrapper.Constructors) { + if (!string.IsNullOrEmpty (ctor.JniSignature)) { + ctors.Add (new ConstructorSuperArgData (ctor.JniSignature, ctor.SuperCall)); + } + } + + if (ctors.Count > 0) { + result [managedName] = ctors; + } + } + + return result; + } + + public static Dictionary> BuildNewConstructorSuperArgs (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (assemblyPaths); + + var result = new Dictionary> (StringComparer.Ordinal); + + foreach (var peer in peers) { + if (peer.AssemblyName != primaryAssemblyName) { + continue; + } + + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + var ctors = new List (); + + foreach (var ctor in peer.JavaConstructors) { + ctors.Add (new ConstructorSuperArgData (ctor.JniSignature, ctor.SuperArgumentsString)); + } + + if (ctors.Count > 0) { + result [managedName] = ctors; + } + } + + return result; + } + static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache, out bool found, out string? declaringType, out string? style) { @@ -264,4 +395,213 @@ static void ExtractDirectRegisterCtors (TypeDefinition typeDef, List jav } } } + + static readonly HashSet ComponentAttributeNames = new (StringComparer.Ordinal) { + "Android.App.ActivityAttribute", + "Android.App.ServiceAttribute", + "Android.Content.BroadcastReceiverAttribute", + "Android.Content.ContentProviderAttribute", + "Android.App.ApplicationAttribute", + "Android.App.InstrumentationAttribute", + }; + + static string GetComponentKindFromAttributeName (string attributeFullName) + { + return attributeFullName switch { + "Android.App.ActivityAttribute" => "Activity", + "Android.App.ServiceAttribute" => "Service", + "Android.Content.BroadcastReceiverAttribute" => "BroadcastReceiver", + "Android.Content.ContentProviderAttribute" => "ContentProvider", + "Android.App.ApplicationAttribute" => "Application", + "Android.App.InstrumentationAttribute" => "Instrumentation", + _ => attributeFullName, + }; + } + + public static Dictionary BuildLegacyComponentData (string assemblyPath) + { + var cache = new TypeDefinitionCache (); + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var scanner = new XAJavaTypeScanner ( + Xamarin.Android.Tools.AndroidTargetArch.Arm64, + new TaskLoggingHelper (new MockBuildEngine (), "test"), + cache + ); + + var javaTypes = scanner.GetJavaTypes (assembly); + var result = new Dictionary (StringComparer.Ordinal); + + foreach (var typeDef in javaTypes) { + if (!typeDef.HasCustomAttributes) { + continue; + } + + foreach (var attr in typeDef.CustomAttributes) { + if (!ComponentAttributeNames.Contains (attr.AttributeType.FullName)) { + continue; + } + + var managedName = ScannerRunner.GetManagedName (typeDef); + var kind = GetComponentKindFromAttributeName (attr.AttributeType.FullName); + + string? componentName = null; + var properties = new List (); + + if (attr.HasProperties) { + foreach (var prop in attr.Properties.OrderBy (p => p.Name, StringComparer.Ordinal)) { + if (prop.Name == "Name") { + componentName = prop.Argument.Value as string; + } else { + var valueStr = prop.Argument.Value?.ToString () ?? "(null)"; + properties.Add ($"{prop.Name}={valueStr}"); + } + } + } + + properties.Sort (StringComparer.Ordinal); + + result [managedName] = new ComponentComparisonData ( + managedName, + kind, + componentName, + properties + ); + break; + } + } + + return result; + } + + public static Dictionary BuildNewComponentData (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (assemblyPaths); + + var result = new Dictionary (StringComparer.Ordinal); + + foreach (var peer in peers) { + if (peer.AssemblyName != primaryAssemblyName) { + continue; + } + + if (peer.ComponentAttribute == null) { + continue; + } + + var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + var component = peer.ComponentAttribute; + var kind = component.Kind.ToString (); + + string? componentName = null; + var properties = new List (); + + foreach (var kvp in component.Properties.OrderBy (p => p.Key, StringComparer.Ordinal)) { + if (kvp.Key == "Name") { + componentName = kvp.Value as string; + } else { + var valueStr = kvp.Value?.ToString () ?? "(null)"; + properties.Add ($"{kvp.Key}={valueStr}"); + } + } + + properties.Sort (StringComparer.Ordinal); + + result [managedName] = new ComponentComparisonData ( + managedName, + kind, + componentName, + properties + ); + } + + return result; + } + + public static ManifestAttributeComparisonData BuildLegacyManifestData (string assemblyPath) + { + var resolver = new DefaultAssemblyResolver (); + resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!); + + var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location); + if (runtimeDir != null) { + resolver.AddSearchDirectory (runtimeDir); + } + + var readerParams = new ReaderParameters { AssemblyResolver = resolver }; + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams); + + var permissions = new List (); + var features = new List (); + + foreach (var attr in assembly.CustomAttributes) { + var attrName = attr.AttributeType.Name; + switch (attrName) { + case "UsesPermissionAttribute": + if (attr.ConstructorArguments.Count > 0 && attr.ConstructorArguments [0].Value is string permName) { + int? maxSdk = null; + foreach (var prop in attr.Properties) { + if (prop.Name == "MaxSdkVersion" && prop.Argument.Value is int sdk) { + maxSdk = sdk; + } + } + permissions.Add (EncodePermission (permName, maxSdk)); + } + break; + case "UsesFeatureAttribute": + string? featName = null; + int glesVersion = 0; + bool required = true; + if (attr.ConstructorArguments.Count > 0 && attr.ConstructorArguments [0].Value is string fn) { + featName = fn; + } + foreach (var prop in attr.Properties) { + if (prop.Name == "GLESVersion" && prop.Argument.Value is int gles) { + glesVersion = gles; + } else if (prop.Name == "Required" && prop.Argument.Value is bool req) { + required = req; + } + } + if (featName != null || glesVersion != 0) { + features.Add (EncodeFeature (featName, glesVersion, required)); + } + break; + } + } + + permissions.Sort (StringComparer.Ordinal); + features.Sort (StringComparer.Ordinal); + + return new ManifestAttributeComparisonData (permissions, features); + } + + public static ManifestAttributeComparisonData BuildNewManifestData (string[] assemblyPaths) + { + using var scanner = new JavaPeerScanner (); + scanner.Scan (assemblyPaths); + var manifestInfo = scanner.ScanAssemblyManifestInfo (); + + var permissions = manifestInfo.UsesPermissions + .Select (p => EncodePermission (p.Name, p.MaxSdkVersion)) + .OrderBy (n => n, StringComparer.Ordinal) + .ToList (); + + var features = manifestInfo.UsesFeatures + .Select (f => EncodeFeature (f.Name, f.GLESVersion, f.Required)) + .OrderBy (n => n, StringComparer.Ordinal) + .ToList (); + + return new ManifestAttributeComparisonData (permissions, features); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 75586236a8e..f37ba98fc27 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -155,4 +155,36 @@ public void DoWork () { } } + + // Export with Throws for export metadata comparison + public class ExportWithThrows : Java.Lang.Object + { + [Export ("riskyOperation", Throws = new [] { typeof (Java.IO.IOException) })] + public void RiskyOperation () + { + } + } + + // ExportField for Java field generation comparison + public class FieldExporter : Java.Lang.Object + { + [ExportField ("MY_CONSTANT")] + public static string GetMyConstant () => "hello"; + } + + // Activity with intent filters and categories + [Activity (Name = "com.example.userapp.ShareActivity", Label = "Share")] + [IntentFilter ( + new [] { "android.intent.action.SEND" }, + Categories = new [] { "android.intent.category.DEFAULT" }, + DataMimeType = "text/plain")] + public class ShareActivity : Activity + { + } + + // Instrumentation component + [Instrumentation (Name = "com.example.userapp.TestRunner")] + public class TestRunner : Android.App.Instrumentation + { + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 18c6ff7d6b9..15a88254a3d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -37,7 +37,7 @@ public class JniNameConversion [Theory] [InlineData ("android/app/Activity", "android.app.Activity")] [InlineData ("java/lang/Object", "java.lang.Object")] - [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")] + [InlineData ("android/view/View$OnClickListener", "android.view.View.OnClickListener")] public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected) { Assert.Equal (expected, JniSignatureHelper.JniNameToJavaName (jniName)); @@ -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/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs new file mode 100644 index 00000000000..b41fce30764 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -0,0 +1,617 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Android.Sdk.TrimmableTypeMap; + +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ManifestGeneratorTests : IDisposable +{ + static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android"; + static readonly XName AttName = AndroidNs + "name"; + + string tempDir; + + public ManifestGeneratorTests () + { + tempDir = Path.Combine (Path.GetTempPath (), "ManifestGeneratorTests_" + Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (tempDir); + } + + public void Dispose () + { + if (Directory.Exists (tempDir)) { + Directory.Delete (tempDir, recursive: true); + } + } + + ManifestGenerator CreateDefaultGenerator () => new ManifestGenerator { + PackageName = "com.example.app", + ApplicationLabel = "My App", + VersionCode = "1", + VersionName = "1.0", + MinSdkVersion = "21", + TargetSdkVersion = "36", + AndroidRuntime = "coreclr", + }; + + string OutputPath => Path.Combine (tempDir, "AndroidManifest.xml"); + + string WriteTemplate (string xml) + { + var path = Path.Combine (tempDir, "template.xml"); + File.WriteAllText (path, xml); + return path; + } + + static JavaPeerInfo CreatePeer ( + string javaName, + ComponentInfo? component = null, + bool isAbstract = false, + string assemblyName = "TestApp") + { + return new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = javaName.Replace ('/', '.'), + ManagedTypeNamespace = javaName.Contains ('/') ? javaName.Substring (0, javaName.LastIndexOf ('/')).Replace ('/', '.') : "", + ManagedTypeShortName = javaName.Contains ('/') ? javaName.Substring (javaName.LastIndexOf ('/') + 1) : javaName, + AssemblyName = assemblyName, + IsAbstract = isAbstract, + ComponentAttribute = component, + }; + } + + XDocument GenerateAndLoad ( + ManifestGenerator gen, + IReadOnlyList? peers = null, + AssemblyManifestInfo? assemblyInfo = null, + string? templatePath = null) + { + peers ??= []; + assemblyInfo ??= new AssemblyManifestInfo (); + gen.Generate (templatePath, peers, assemblyInfo, OutputPath); + return XDocument.Load (OutputPath); + } + + [Fact] + public void Activity_MainLauncher () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MainActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + Properties = new Dictionary { ["MainLauncher"] = true }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + + Assert.Equal ("com.example.app.MainActivity", (string?)activity?.Attribute (AttName)); + Assert.Equal ("true", (string?)activity?.Attribute (AndroidNs + "exported")); + + var filter = activity?.Element ("intent-filter"); + Assert.NotNull (filter); + Assert.True (filter?.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.MAIN")); + Assert.True (filter?.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.LAUNCHER")); + } + + [Fact] + public void Activity_WithProperties () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + Properties = new Dictionary { + ["Label"] = "My Activity", + ["Icon"] = "@drawable/icon", + ["Theme"] = "@style/MyTheme", + ["LaunchMode"] = 2, // singleTask + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var activity = doc.Root?.Element ("application")?.Element ("activity"); + + Assert.Equal ("My Activity", (string?)activity?.Attribute (AndroidNs + "label")); + Assert.Equal ("@drawable/icon", (string?)activity?.Attribute (AndroidNs + "icon")); + Assert.Equal ("@style/MyTheme", (string?)activity?.Attribute (AndroidNs + "theme")); + Assert.Equal ("singleTask", (string?)activity?.Attribute (AndroidNs + "launchMode")); + } + + [Fact] + public void Activity_IntentFilter () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/ShareActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + IntentFilters = [ + new IntentFilterInfo { + Actions = ["android.intent.action.SEND"], + Categories = ["android.intent.category.DEFAULT"], + Properties = new Dictionary { + ["DataMimeType"] = "text/plain", + }, + }, + ], + }); + + var doc = GenerateAndLoad (gen, [peer]); + var activity = doc.Root?.Element ("application")?.Element ("activity"); + + var filter = activity?.Element ("intent-filter"); + Assert.NotNull (filter); + Assert.True (filter?.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.SEND")); + Assert.True (filter?.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.DEFAULT")); + + var data = filter?.Element ("data"); + Assert.NotNull (data); + Assert.Equal ("text/plain", (string?)data?.Attribute (AndroidNs + "mimeType")); + } + + [Fact] + public void Activity_MetaData () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MetaActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + MetaData = [ + new MetaDataInfo { Name = "com.example.key", Value = "my_value" }, + new MetaDataInfo { Name = "com.example.res", Resource = "@xml/config" }, + ], + }); + + var doc = GenerateAndLoad (gen, [peer]); + var activity = doc.Root?.Element ("application")?.Element ("activity"); + + var metaElements = activity?.Elements ("meta-data").ToList (); + Assert.Equal (2, metaElements?.Count); + + var meta1 = metaElements?.FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.example.key"); + Assert.NotNull (meta1); + Assert.Equal ("my_value", (string?)meta1?.Attribute (AndroidNs + "value")); + + var meta2 = metaElements?.FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.example.res"); + Assert.NotNull (meta2); + Assert.Equal ("@xml/config", (string?)meta2?.Attribute (AndroidNs + "resource")); + } + + [Theory] + [InlineData (ComponentKind.Service, "service")] + [InlineData (ComponentKind.BroadcastReceiver, "receiver")] + public void Component_BasicProperties (ComponentKind kind, string elementName) + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyComponent", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = kind, + Properties = new Dictionary { + ["Exported"] = true, + ["Label"] = "My Component", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var element = doc.Root?.Element ("application")?.Element (elementName); + Assert.NotNull (element); + + Assert.Equal ("com.example.app.MyComponent", (string?)element?.Attribute (AttName)); + Assert.Equal ("true", (string?)element?.Attribute (AndroidNs + "exported")); + Assert.Equal ("My Component", (string?)element?.Attribute (AndroidNs + "label")); + } + + [Fact] + public void ContentProvider_WithAuthorities () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyProvider", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.ContentProvider, + Properties = new Dictionary { + ["Authorities"] = "com.example.app.provider", + ["Exported"] = false, + ["GrantUriPermissions"] = true, + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var provider = doc.Root?.Element ("application")?.Element ("provider"); + Assert.NotNull (provider); + + Assert.Equal ("com.example.app.MyProvider", (string?)provider?.Attribute (AttName)); + Assert.Equal ("com.example.app.provider", (string?)provider?.Attribute (AndroidNs + "authorities")); + Assert.Equal ("false", (string?)provider?.Attribute (AndroidNs + "exported")); + Assert.Equal ("true", (string?)provider?.Attribute (AndroidNs + "grantUriPermissions")); + } + + [Fact] + public void Application_TypeLevel () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyApp", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Application, + Properties = new Dictionary { + ["Label"] = "Custom App", + ["AllowBackup"] = false, + ["LargeHeap"] = true, + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + + Assert.Equal ("com.example.app.MyApp", (string?)app?.Attribute (AttName)); + Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup")); + Assert.Equal ("true", (string?)app?.Attribute (AndroidNs + "largeHeap")); + } + + [Fact] + public void Instrumentation_GoesToManifest () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Instrumentation, + Properties = new Dictionary { + ["Label"] = "My Test", + ["TargetPackage"] = "com.example.target", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + + // Instrumentation should be under , not + var instrumentation = doc.Root?.Element ("instrumentation"); + Assert.NotNull (instrumentation); + + Assert.Equal ("com.example.app.MyInstrumentation", (string?)instrumentation?.Attribute (AttName)); + Assert.Equal ("My Test", (string?)instrumentation?.Attribute (AndroidNs + "label")); + Assert.Equal ("com.example.target", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); + + // Should NOT be inside + var appInstrumentation = doc.Root?.Element ("application")?.Element ("instrumentation"); + Assert.Null (appInstrumentation); + } + + [Fact] + public void RuntimeProvider_Added () + { + var gen = CreateDefaultGenerator (); + var doc = GenerateAndLoad (gen); + var app = doc.Root?.Element ("application"); + + var providers = app?.Elements ("provider").ToList (); + Assert.True (providers?.Count > 0); + + var runtimeProvider = providers?.FirstOrDefault (p => + ((string?)p.Attribute (AndroidNs + "name"))?.Contains ("MonoRuntimeProvider") == true); + Assert.NotNull (runtimeProvider); + + var authorities = (string?)runtimeProvider?.Attribute (AndroidNs + "authorities"); + Assert.True (authorities?.Contains ("com.example.app") == true, "authorities should contain package name"); + Assert.True (authorities?.Contains ("__mono_init__") == true, "authorities should contain __mono_init__"); + Assert.Equal ("false", (string?)runtimeProvider?.Attribute (AndroidNs + "exported")); + } + + [Fact] + public void TemplateManifest_Preserved () + { + var gen = CreateDefaultGenerator (); + var template = WriteTemplate ( + """ + + + + + + """); + + var doc = GenerateAndLoad (gen, templatePath: template); + var app = doc.Root?.Element ("application"); + + Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup")); + Assert.Equal ("@mipmap/ic_launcher", (string?)app?.Attribute (AndroidNs + "icon")); + } + + [Theory] + [InlineData ("", "", "1", "1.0")] + [InlineData ("42", "2.5", "42", "2.5")] + public void VersionDefaults (string versionCode, string versionName, string expectedCode, string expectedName) + { + var gen = CreateDefaultGenerator (); + gen.VersionCode = versionCode; + gen.VersionName = versionName; + + var doc = GenerateAndLoad (gen); + Assert.Equal (expectedCode, (string?)doc.Root?.Attribute (AndroidNs + "versionCode")); + Assert.Equal (expectedName, (string?)doc.Root?.Attribute (AndroidNs + "versionName")); + } + + [Fact] + public void UsesSdk_Added () + { + var gen = CreateDefaultGenerator (); + gen.MinSdkVersion = "24"; + gen.TargetSdkVersion = "34"; + + var doc = GenerateAndLoad (gen); + var usesSdk = doc.Root?.Element ("uses-sdk"); + Assert.NotNull (usesSdk); + + Assert.Equal ("24", (string?)usesSdk?.Attribute (AndroidNs + "minSdkVersion")); + Assert.Equal ("34", (string?)usesSdk?.Attribute (AndroidNs + "targetSdkVersion")); + } + + [Theory] + [InlineData (true, false, false, "debuggable", "true")] + [InlineData (false, true, false, "debuggable", "true")] + [InlineData (false, false, true, "extractNativeLibs", "true")] + public void ApplicationFlags (bool debug, bool forceDebuggable, bool forceExtractNativeLibs, string attrName, string expected) + { + var gen = CreateDefaultGenerator (); + gen.Debug = debug; + gen.ForceDebuggable = forceDebuggable; + gen.ForceExtractNativeLibs = forceExtractNativeLibs; + + var doc = GenerateAndLoad (gen); + var app = doc.Root?.Element ("application"); + Assert.Equal (expected, (string?)app?.Attribute (AndroidNs + attrName)); + } + + [Fact] + public void InternetPermission_WhenDebug () + { + var gen = CreateDefaultGenerator (); + gen.Debug = true; + + var doc = GenerateAndLoad (gen); + var internetPerm = doc.Root?.Elements ("uses-permission") + .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "android.permission.INTERNET"); + Assert.NotNull (internetPerm); + } + + [Fact] + public void ManifestPlaceholders_Replaced () + { + var gen = CreateDefaultGenerator (); + gen.ManifestPlaceholders = "myAuthority=com.example.auth;myKey=12345"; + + var template = WriteTemplate ( + """ + + + + + + + + """); + + var doc = GenerateAndLoad (gen, templatePath: template); + var provider = doc.Root?.Element ("application")?.Elements ("provider") + .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "com.example.MyProvider"); + Assert.Equal ("com.example.auth", (string?)provider?.Attribute (AndroidNs + "authorities")); + + var meta = doc.Root?.Element ("application")?.Elements ("meta-data") + .FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "api_key"); + Assert.Equal ("12345", (string?)meta?.Attribute (AndroidNs + "value")); + } + + [Fact] + public void ApplicationJavaClass_Set () + { + var gen = CreateDefaultGenerator (); + gen.ApplicationJavaClass = "com.example.app.CustomApplication"; + + var doc = GenerateAndLoad (gen); + var app = doc.Root?.Element ("application"); + Assert.Equal ("com.example.app.CustomApplication", (string?)app?.Attribute (AttName)); + } + + [Fact] + public void AbstractTypes_Skipped () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/AbstractActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + Properties = new Dictionary { ["Label"] = "Abstract" }, + }, isAbstract: true); + + var doc = GenerateAndLoad (gen, [peer]); + var activity = doc.Root?.Element ("application")?.Element ("activity"); + Assert.Null (activity); + } + + [Fact] + public void ExistingType_NotDuplicated () + { + var gen = CreateDefaultGenerator (); + var template = WriteTemplate ( + """ + + + + + + + """); + + var peer = CreatePeer ("com/example/app/ExistingActivity", new ComponentInfo { HasPublicDefaultConstructor = true, + Kind = ComponentKind.Activity, + Properties = new Dictionary { ["Label"] = "New Label" }, + }); + + var doc = GenerateAndLoad (gen, [peer], templatePath: template); + var activities = doc.Root?.Element ("application")?.Elements ("activity") + .Where (a => (string?)a.Attribute (AttName) == "com.example.app.ExistingActivity") + .ToList (); + + Assert.Equal (1, activities?.Count); + // Original label preserved + Assert.Equal ("Existing", (string?)activities? [0].Attribute (AndroidNs + "label")); + } + + [Fact] + public void AssemblyLevel_UsesPermission () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.UsesPermissions.Add (new UsesPermissionInfo { Name = "android.permission.CAMERA" }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("uses-permission") + .FirstOrDefault (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA"); + Assert.NotNull (perm); + } + + [Fact] + public void AssemblyLevel_UsesFeature () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.UsesFeatures.Add (new UsesFeatureInfo { Name = "android.hardware.camera", Required = false }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var feature = doc.Root?.Elements ("uses-feature") + .FirstOrDefault (f => (string?)f.Attribute (AttName) == "android.hardware.camera"); + Assert.NotNull (feature); + Assert.Equal ("false", (string?)feature?.Attribute (AndroidNs + "required")); + } + + [Fact] + public void AssemblyLevel_UsesLibrary () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy", Required = false }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var lib = doc.Root?.Element ("application")?.Elements ("uses-library") + .FirstOrDefault (l => (string?)l.Attribute (AttName) == "org.apache.http.legacy"); + Assert.NotNull (lib); + Assert.Equal ("false", (string?)lib?.Attribute (AndroidNs + "required")); + } + + [Fact] + public void AssemblyLevel_Permission () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.Permissions.Add (new PermissionInfo { + Name = "com.example.MY_PERMISSION", + Properties = new Dictionary { + ["Label"] = "My Permission", + ["Description"] = "A custom permission", + }, + }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("permission") + .FirstOrDefault (p => (string?)p.Attribute (AttName) == "com.example.MY_PERMISSION"); + Assert.NotNull (perm); + Assert.Equal ("My Permission", (string?)perm?.Attribute (AndroidNs + "label")); + Assert.Equal ("A custom permission", (string?)perm?.Attribute (AndroidNs + "description")); + } + + [Fact] + public void AssemblyLevel_MetaData () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.MetaData.Add (new MetaDataInfo { Name = "com.google.android.gms.version", Value = "12345" }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var meta = doc.Root?.Element ("application")?.Elements ("meta-data") + .FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.google.android.gms.version"); + Assert.NotNull (meta); + Assert.Equal ("12345", (string?)meta?.Attribute (AndroidNs + "value")); + } + + [Fact] + public void AssemblyLevel_Application () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo { + ApplicationProperties = new Dictionary { + ["Theme"] = "@style/AppTheme", + ["SupportsRtl"] = true, + }, + }; + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var app = doc.Root?.Element ("application"); + Assert.Equal ("@style/AppTheme", (string?)app?.Attribute (AndroidNs + "theme")); + Assert.Equal ("true", (string?)app?.Attribute (AndroidNs + "supportsRtl")); + } + + [Fact] + public void AssemblyLevel_Deduplication () + { + var gen = CreateDefaultGenerator (); + var template = WriteTemplate ( + """ + + + + + + + + + """); + + var info = new AssemblyManifestInfo (); + info.UsesPermissions.Add (new UsesPermissionInfo { Name = "android.permission.CAMERA" }); + info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy" }); + info.MetaData.Add (new MetaDataInfo { Name = "existing.key", Value = "new_value" }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info, templatePath: template); + + var cameraPerms = doc.Root?.Elements ("uses-permission") + .Where (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA") + .ToList (); + Assert.Equal (1, cameraPerms?.Count); + + var libs = doc.Root?.Element ("application")?.Elements ("uses-library") + .Where (l => (string?)l.Attribute (AttName) == "org.apache.http.legacy") + .ToList (); + Assert.Equal (1, libs?.Count); + + var metas = doc.Root?.Element ("application")?.Elements ("meta-data") + .Where (m => (string?)m.Attribute (AndroidNs + "name") == "existing.key") + .ToList (); + Assert.Equal (1, metas?.Count); + } + + [Fact] + 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, + Kind = ComponentKind.Activity, + Properties = new Dictionary { + ["ConfigurationChanges"] = configChanges, + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var activity = doc.Root?.Element ("application")?.Element ("activity"); + + var configValue = (string?)activity?.Attribute (AndroidNs + "configChanges"); + + // The value should be pipe-separated and contain all three flags + var parts = configValue?.Split ('|') ?? []; + Assert.True (parts.Contains ("orientation"), "configChanges should contain 'orientation'"); + Assert.True (parts.Contains ("keyboardHidden"), "configChanges should contain 'keyboardHidden'"); + Assert.True (parts.Contains ("screenSize"), "configChanges should contain 'screenSize'"); + Assert.Equal (3, parts.Length); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 5fb770fd3a5..01e82d6d85b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -82,7 +82,7 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () Assert.Contains (".ctor", methods); Assert.Contains ("CreateInstance", methods); - Assert.Contains ("get_TargetType", methods); + // get_TargetType is inherited from JavaPeerProxy, not emitted on the proxy type } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 7a91bc0a029..e882980d817 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -561,7 +561,7 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () Assert.Contains (".ctor", methodNames); Assert.Contains ("CreateInstance", methodNames); - Assert.Contains ("get_TargetType", methodNames); + // get_TargetType is inherited from JavaPeerProxy, not emitted on the proxy type }); } 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);