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);