diff --git a/build-tools/automation/yaml-templates/stage-linux-tests.yaml b/build-tools/automation/yaml-templates/stage-linux-tests.yaml
index cda09f6fd87..5cce98d91b3 100644
--- a/build-tools/automation/yaml-templates/stage-linux-tests.yaml
+++ b/build-tools/automation/yaml-templates/stage-linux-tests.yaml
@@ -85,6 +85,7 @@ stages:
args: >-
build samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
-p:AndroidSdkDirectory="$(HOME)/android-toolchain/sdk"
+ -p:UseMonoRuntime=false
-bl:$(System.DefaultWorkingDirectory)/bin/Build$(XA.Build.Configuration)/build-helloworld.binlog
- template: /build-tools/automation/yaml-templates/upload-results.yaml
diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml
index 869ac45cee1..8c070befc25 100644
--- a/build-tools/automation/yaml-templates/stage-package-tests.yaml
+++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml
@@ -231,7 +231,7 @@ stages:
displayName: Test dotnet-local.sh
inputs:
scriptPath: dotnet-local.sh
- args: build samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
+ args: build samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj -p:UseMonoRuntime=false
- ${{ if ne(parameters.macTestAgentsUseCleanImages, true) }}:
- template: /build-tools/automation/yaml-templates/start-stop-emulator.yaml
diff --git a/eng/Versions.props b/eng/Versions.props
index bdb0ac17f80..d94a1826510 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,6 +15,7 @@
$(MicrosoftNETWorkloadEmscriptenCurrentManifest110100preview3PackageVersion)
11.0.100-preview.3.26171.106
0.11.5-preview.26171.106
+ 17.14.28
9.0.4
11.0.0-preview.1.26104.118
diff --git a/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj b/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
index 996e0ddb2da..f8644dda5db 100644
--- a/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
+++ b/samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj
@@ -3,6 +3,7 @@
$(DotNetAndroidTargetFramework)
Exe
HelloWorld
+ <_AndroidTypeMapImplementation>trimmable
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/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
index 938c60fd10a..39ca132cfe5 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
@@ -40,13 +40,30 @@ class ManifestGenerator
/// Generates the merged manifest and writes it to .
/// Returns the list of additional content provider names (for ApplicationRegistration.java).
///
- public IList Generate (
+ public IReadOnlyList Generate (
string? manifestTemplatePath,
IReadOnlyList allPeers,
AssemblyManifestInfo assemblyInfo,
string outputPath)
{
- var doc = LoadOrCreateManifest (manifestTemplatePath);
+ XDocument? template = null;
+ if (!manifestTemplatePath.IsNullOrEmpty () && File.Exists (manifestTemplatePath)) {
+ template = XDocument.Load (manifestTemplatePath);
+ }
+ return Generate (template, allPeers, assemblyInfo, outputPath);
+ }
+
+ ///
+ /// Generates the merged manifest from an optional pre-loaded template and writes it to .
+ /// Returns the list of additional content provider names (for ApplicationRegistration.java).
+ ///
+ public IReadOnlyList Generate (
+ XDocument? manifestTemplate,
+ IReadOnlyList allPeers,
+ AssemblyManifestInfo assemblyInfo,
+ string outputPath)
+ {
+ var doc = manifestTemplate ?? CreateDefaultManifest ();
var manifest = doc.Root;
if (manifest is null) {
throw new InvalidOperationException ("Manifest document has no root element.");
@@ -98,7 +115,7 @@ public IList Generate (
var providerNames = AddRuntimeProviders (app);
// Set ApplicationJavaClass
- if (!string.IsNullOrEmpty (ApplicationJavaClass) && app.Attribute (AttName) is null) {
+ if (!ApplicationJavaClass.IsNullOrEmpty () && app.Attribute (AttName) is null) {
app.SetAttributeValue (AttName, ApplicationJavaClass);
}
@@ -134,12 +151,8 @@ public IList Generate (
return providerNames;
}
- XDocument LoadOrCreateManifest (string? templatePath)
+ XDocument CreateDefaultManifest ()
{
- if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) {
- return XDocument.Load (templatePath);
- }
-
return new XDocument (
new XDeclaration ("1.0", "utf-8", null),
new XElement ("manifest",
@@ -151,18 +164,18 @@ void EnsureManifestAttributes (XElement manifest)
{
manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName);
- if (string.IsNullOrEmpty ((string?)manifest.Attribute ("package"))) {
+ if (((string?)manifest.Attribute ("package")).IsNullOrEmpty ()) {
manifest.SetAttributeValue ("package", PackageName);
}
if (manifest.Attribute (AndroidNs + "versionCode") is null) {
manifest.SetAttributeValue (AndroidNs + "versionCode",
- string.IsNullOrEmpty (VersionCode) ? "1" : VersionCode);
+ VersionCode.IsNullOrEmpty () ? "1" : VersionCode);
}
if (manifest.Attribute (AndroidNs + "versionName") is null) {
manifest.SetAttributeValue (AndroidNs + "versionName",
- string.IsNullOrEmpty (VersionName) ? "1.0" : VersionName);
+ VersionName.IsNullOrEmpty () ? "1.0" : VersionName);
}
// Add
@@ -181,14 +194,14 @@ XElement EnsureApplicationElement (XElement manifest)
manifest.Add (app);
}
- if (app.Attribute (AndroidNs + "label") is null && !string.IsNullOrEmpty (ApplicationLabel)) {
+ if (app.Attribute (AndroidNs + "label") is null && !ApplicationLabel.IsNullOrEmpty ()) {
app.SetAttributeValue (AndroidNs + "label", ApplicationLabel);
}
return app;
}
- IList AddRuntimeProviders (XElement app)
+ List AddRuntimeProviders (XElement app)
{
string packageName = "mono";
string className = "MonoRuntimeProvider";
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
deleted file mode 100644
index 166223cc44f..00000000000
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-#nullable enable
-
-using System.Collections.Generic;
-
-namespace Microsoft.Android.Sdk.TrimmableTypeMap;
-
-public enum ComponentKind
-{
- Activity,
- Service,
- BroadcastReceiver,
- ContentProvider,
- Application,
- Instrumentation,
-}
-
-public class ComponentInfo
-{
- public bool HasPublicDefaultConstructor { get; set; }
- public ComponentKind Kind { get; set; }
- public Dictionary Properties { get; set; } = new Dictionary ();
- public IReadOnlyList IntentFilters { get; set; } = [];
- public IReadOnlyList MetaData { get; set; } = [];
-}
-
-public class IntentFilterInfo
-{
- public IReadOnlyList Actions { get; set; } = [];
- public IReadOnlyList Categories { get; set; } = [];
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class MetaDataInfo
-{
- public string Name { get; set; } = "";
- public string? Value { get; set; }
- public string? Resource { get; set; }
-}
-
-public class PermissionInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class PermissionGroupInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class PermissionTreeInfo
-{
- public string Name { get; set; } = "";
- public Dictionary Properties { get; set; } = new Dictionary ();
-}
-
-public class UsesPermissionInfo
-{
- public string Name { get; set; } = "";
- public int? MaxSdkVersion { get; set; }
-}
-
-public class UsesFeatureInfo
-{
- public string? Name { get; set; }
- public bool Required { get; set; }
- public int GLESVersion { get; set; }
-}
-
-public class UsesLibraryInfo
-{
- public string Name { get; set; } = "";
- public bool Required { get; set; }
-}
-
-public class UsesConfigurationInfo
-{
- public bool ReqFiveWayNav { get; set; }
- public bool ReqHardKeyboard { get; set; }
- public string? ReqKeyboardType { get; set; }
- public string? ReqNavigation { get; set; }
- public string? ReqTouchScreen { get; set; }
-}
-
-public class PropertyInfo
-{
- public string Name { get; set; } = "";
- public string? Value { get; set; }
- public string? Resource { get; set; }
-}
-
-public class AssemblyManifestInfo
-{
- public List Permissions { get; set; } = new List ();
- public List PermissionGroups { get; set; } = new List ();
- public List PermissionTrees { get; set; } = new List ();
- public List UsesPermissions { get; set; } = new List ();
- public List UsesFeatures { get; set; } = new List ();
- public List UsesLibraries { get; set; } = new List ();
- public List UsesConfigurations { get; set; } = new List ();
- public List MetaData { get; set; } = new List ();
- public List Properties { get; set; } = new List ();
- public Dictionary? ApplicationProperties { get; set; }
-}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
index 249bdc8def1..a71b74269f2 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
@@ -11,12 +11,22 @@
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ True
+ Resources.resx
+
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
new file mode 100644
index 00000000000..b8b3b7b23ac
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs
@@ -0,0 +1,19 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+// The static methods in System.String are not NRT annotated in netstandard2.0,
+// so we need our own extension methods to make them nullable aware.
+static class NullableExtensions
+{
+ public static bool IsNullOrEmpty ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrEmpty (str);
+ }
+
+ public static bool IsNullOrWhiteSpace ([NotNullWhen (false)] this string? str)
+ {
+ return string.IsNullOrWhiteSpace (str);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.Designer.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.Designer.cs
new file mode 100644
index 00000000000..99849b1c83b
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.Designer.cs
@@ -0,0 +1,90 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Properties {
+ using System;
+ using System.Reflection;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Android.Sdk.TrimmableTypeMap.Properties.Resources", typeof(Resources).GetTypeInfo().Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to There can be only one type with an [Application] attribute; found: {0}.
+ ///
+ internal static string XA4212 {
+ get {
+ return ResourceManager.GetString("XA4212", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The type '{0}' must provide a public default constructor.
+ ///
+ internal static string XA4213 {
+ get {
+ return ResourceManager.GetString("XA4213", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Application cannot have both a type with an [Application] attribute and an [assembly:Application] attribute..
+ ///
+ internal static string XA4217 {
+ get {
+ return ResourceManager.GetString("XA4217", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.resx b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.resx
new file mode 100644
index 00000000000..442b268e552
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Properties/Resources.resx
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ There can be only one type with an [Application] attribute; found: {0}
+ {0} - Comma-separated list of types with [Application] attribute
+
+
+ The type '{0}' must provide a public default constructor
+
+
+ Application cannot have both a type with an [Application] attribute and an [assembly:Application] attribute.
+
+
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
index d364200e460..69611259087 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -98,14 +98,33 @@ void Build ()
// [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner
} else if (IsKnownComponentAttribute (attrName)) {
attrInfo ??= CreateTypeAttributeInfo (attrName);
- var name = TryGetNameProperty (ca);
+ var value = DecodeAttribute (ca);
+
+ // Capture all named properties
+ foreach (var named in value.NamedArguments) {
+ if (named.Name is not null) {
+ attrInfo.Properties [named.Name] = named.Value;
+ }
+ }
+
+ var name = TryGetNameFromDecodedAttribute (value);
if (name is not null) {
attrInfo.JniName = name.Replace ('.', '/');
}
if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
- applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent");
- applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity");
+ if (TryGetNamedArgument (value, "BackupAgent", out var backupAgent)) {
+ applicationAttributeInfo.BackupAgent = backupAgent;
+ }
+ if (TryGetNamedArgument (value, "ManageSpaceActivity", out var manageSpace)) {
+ applicationAttributeInfo.ManageSpaceActivity = manageSpace;
+ }
}
+ } else if (attrName == "IntentFilterAttribute") {
+ attrInfo ??= new TypeAttributeInfo ("IntentFilterAttribute");
+ attrInfo.IntentFilters.Add (ParseIntentFilterAttribute (ca));
+ } else if (attrName == "MetaDataAttribute") {
+ attrInfo ??= new TypeAttributeInfo ("MetaDataAttribute");
+ attrInfo.MetaData.Add (ParseMetaDataAttribute (ca));
} else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) {
// Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName])
var name = TryGetNameProperty (ca);
@@ -239,6 +258,14 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value)
}
var value = DecodeAttribute (ca);
+ return TryGetNameFromDecodedAttribute (value);
+ }
+
+ static string? TryGetNameFromDecodedAttribute (CustomAttributeValue value)
+ {
+ if (TryGetNamedArgument (value, "Name", out var name) && !string.IsNullOrEmpty (name)) {
+ return name;
+ }
// Fall back to first constructor argument (e.g., [CustomJniName("...")])
if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) {
@@ -248,6 +275,68 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value)
return null;
}
+ IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+
+ // First ctor argument is string[] actions
+ var actions = new List ();
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is IReadOnlyCollection> actionArgs) {
+ foreach (var arg in actionArgs) {
+ if (arg.Value is string action) {
+ actions.Add (action);
+ }
+ }
+ }
+
+ var categories = new List ();
+ // Categories is a string[] property — the SRM decoder sees it as
+ // IReadOnlyCollection>, not string.
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Categories" && named.Value is IReadOnlyCollection> catArgs) {
+ foreach (var arg in catArgs) {
+ if (arg.Value is string cat) {
+ categories.Add (cat);
+ }
+ }
+ }
+ }
+
+ var properties = new Dictionary (StringComparer.Ordinal);
+ foreach (var named in value.NamedArguments) {
+ if (named.Name is not null && named.Name != "Categories") {
+ properties [named.Name] = named.Value;
+ }
+ }
+
+ return new IntentFilterInfo {
+ Actions = actions,
+ Categories = categories,
+ Properties = properties,
+ };
+ }
+
+ MetaDataInfo ParseMetaDataAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+
+ string name = "";
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string nameArg) {
+ name = nameArg;
+ }
+
+ string? metaValue = null;
+ string? resource = null;
+ TryGetNamedArgument (value, "Value", out metaValue);
+ TryGetNamedArgument (value, "Resource", out resource);
+
+ return new MetaDataInfo {
+ Name = name,
+ Value = metaValue,
+ Resource = resource,
+ };
+ }
+
static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull
{
foreach (var named in value.NamedArguments) {
@@ -260,6 +349,193 @@ static bool TryGetNamedArgument (CustomAttributeValue value, string a
return false;
}
+ ///
+ /// Scans assembly-level custom attributes for manifest-related data.
+ ///
+ internal void ScanAssemblyAttributes (AssemblyManifestInfo info)
+ {
+ var asmDef = Reader.GetAssemblyDefinition ();
+ foreach (var caHandle in asmDef.GetCustomAttributes ()) {
+ var ca = Reader.GetCustomAttribute (caHandle);
+ var attrName = GetCustomAttributeName (ca, Reader);
+ if (attrName is null) {
+ continue;
+ }
+
+ switch (attrName) {
+ case "PermissionAttribute":
+ info.Permissions.Add (ParsePermissionAttribute (ca));
+ break;
+ case "PermissionGroupAttribute":
+ info.PermissionGroups.Add (ParsePermissionGroupAttribute (ca));
+ break;
+ case "PermissionTreeAttribute":
+ info.PermissionTrees.Add (ParsePermissionTreeAttribute (ca));
+ break;
+ case "UsesPermissionAttribute":
+ info.UsesPermissions.Add (ParseUsesPermissionAttribute (ca));
+ break;
+ case "UsesFeatureAttribute":
+ info.UsesFeatures.Add (ParseUsesFeatureAttribute (ca));
+ break;
+ case "UsesLibraryAttribute":
+ info.UsesLibraries.Add (ParseUsesLibraryAttribute (ca));
+ break;
+ case "UsesConfigurationAttribute":
+ info.UsesConfigurations.Add (ParseUsesConfigurationAttribute (ca));
+ break;
+ case "MetaDataAttribute":
+ info.MetaData.Add (ParseMetaDataAttribute (ca));
+ break;
+ case "PropertyAttribute":
+ info.Properties.Add (ParsePropertyAttribute (ca));
+ break;
+ case "ApplicationAttribute":
+ info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal);
+ var appValue = DecodeAttribute (ca);
+ foreach (var named in appValue.NamedArguments) {
+ if (named.Name is not null) {
+ info.ApplicationProperties [named.Name] = named.Value;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ (string name, Dictionary props) ParseNameAndProperties (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string name = "";
+ var props = new Dictionary (StringComparer.Ordinal);
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string n) {
+ name = n;
+ }
+ if (named.Name is not null) {
+ props [named.Name] = named.Value;
+ }
+ }
+ return (name, props);
+ }
+
+ PermissionInfo ParsePermissionAttribute (CustomAttribute ca)
+ {
+ var (name, props) = ParseNameAndProperties (ca);
+ return new PermissionInfo { Name = name, Properties = props };
+ }
+
+ PermissionGroupInfo ParsePermissionGroupAttribute (CustomAttribute ca)
+ {
+ var (name, props) = ParseNameAndProperties (ca);
+ return new PermissionGroupInfo { Name = name, Properties = props };
+ }
+
+ PermissionTreeInfo ParsePermissionTreeAttribute (CustomAttribute ca)
+ {
+ var (name, props) = ParseNameAndProperties (ca);
+ return new PermissionTreeInfo { Name = name, Properties = props };
+ }
+
+ UsesPermissionInfo ParseUsesPermissionAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string name = "";
+ int? maxSdk = null;
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) {
+ name = n;
+ }
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string nameVal) {
+ name = nameVal;
+ } else if (named.Name == "MaxSdkVersion" && named.Value is int max) {
+ maxSdk = max;
+ }
+ }
+ return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk };
+ }
+
+ UsesFeatureInfo ParseUsesFeatureAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string? name = null;
+ int glesVersion = 0;
+ bool required = true;
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string n) {
+ name = n;
+ } else if (named.Name == "GLESVersion" && named.Value is int v) {
+ glesVersion = v;
+ } else if (named.Name == "Required" && named.Value is bool r) {
+ required = r;
+ }
+ }
+ return new UsesFeatureInfo { Name = name, GLESVersion = glesVersion, Required = required };
+ }
+
+ UsesLibraryInfo ParseUsesLibraryAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string name = "";
+ bool required = true;
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) {
+ name = n;
+ }
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string nameVal) {
+ name = nameVal;
+ } else if (named.Name == "Required" && named.Value is bool r) {
+ required = r;
+ }
+ }
+ return new UsesLibraryInfo { Name = name, Required = required };
+ }
+
+ UsesConfigurationInfo ParseUsesConfigurationAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ bool reqFiveWayNav = false;
+ bool reqHardKeyboard = false;
+ string? reqKeyboardType = null;
+ string? reqNavigation = null;
+ string? reqTouchScreen = null;
+ foreach (var named in value.NamedArguments) {
+ switch (named.Name) {
+ case "ReqFiveWayNav" when named.Value is bool b: reqFiveWayNav = b; break;
+ case "ReqHardKeyboard" when named.Value is bool b: reqHardKeyboard = b; break;
+ case "ReqKeyboardType" when named.Value is string s: reqKeyboardType = s; break;
+ case "ReqNavigation" when named.Value is string s: reqNavigation = s; break;
+ case "ReqTouchScreen" when named.Value is string s: reqTouchScreen = s; break;
+ }
+ }
+ return new UsesConfigurationInfo {
+ ReqFiveWayNav = reqFiveWayNav,
+ ReqHardKeyboard = reqHardKeyboard,
+ ReqKeyboardType = reqKeyboardType,
+ ReqNavigation = reqNavigation,
+ ReqTouchScreen = reqTouchScreen,
+ };
+ }
+
+ PropertyInfo ParsePropertyAttribute (CustomAttribute ca)
+ {
+ var value = DecodeAttribute (ca);
+ string name = "";
+ string? propValue = null;
+ string? resource = null;
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string n) {
+ name = n;
+ }
+ foreach (var named in value.NamedArguments) {
+ switch (named.Name) {
+ case "Name" when named.Value is string s: name = s; break;
+ case "Value" when named.Value is string s: propValue = s; break;
+ case "Resource" when named.Value is string s: resource = s; break;
+ }
+ }
+ return new PropertyInfo { Name = name, Value = propValue, Resource = resource };
+ }
+
public void Dispose ()
{
peReader.Dispose ();
@@ -290,6 +566,21 @@ class TypeAttributeInfo (string attributeName)
{
public string AttributeName { get; } = attributeName;
public string? JniName { get; set; }
+
+ ///
+ /// All named property values from the component attribute.
+ ///
+ public Dictionary Properties { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Intent filters declared on this type via [IntentFilter] attributes.
+ ///
+ public List IntentFilters { get; } = [];
+
+ ///
+ /// Metadata entries declared on this type via [MetaData] attributes.
+ ///
+ public List MetaData { get; } = [];
}
sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs
new file mode 100644
index 00000000000..804006fa6c0
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs
@@ -0,0 +1,29 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Assembly-level manifest attributes collected from all scanned assemblies.
+/// Aggregated across assemblies — used to generate top-level manifest elements
+/// like ]]>, ]]>, etc.
+///
+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; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs
new file mode 100644
index 00000000000..79a9d07dab6
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentInfo.cs
@@ -0,0 +1,53 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// The kind of Android component (Activity, Service, etc.).
+///
+public enum ComponentKind
+{
+ Activity,
+ Service,
+ BroadcastReceiver,
+ ContentProvider,
+ Application,
+ Instrumentation,
+}
+
+///
+/// Describes an Android component attribute ([Activity], [Service], etc.) on a Java peer type.
+/// All named property values from the attribute are stored in .
+///
+public sealed record ComponentInfo
+{
+ ///
+ /// The kind of component.
+ ///
+ public required ComponentKind Kind { get; init; }
+
+ ///
+ /// All named property values from the component attribute.
+ /// Keys are property names (e.g., "Label", "Exported", "MainLauncher").
+ /// Values are the raw decoded values (string, bool, int for enums, etc.).
+ ///
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+
+ ///
+ /// Intent filters declared on this component via [IntentFilter] attributes.
+ ///
+ public IReadOnlyList IntentFilters { get; init; } = [];
+
+ ///
+ /// Metadata entries declared on this component via [MetaData] attributes.
+ ///
+ public IReadOnlyList MetaData { get; init; } = [];
+
+ ///
+ /// Whether the component type has a public parameterless constructor.
+ /// Required for manifest inclusion — XA4213 error if missing.
+ ///
+ public bool HasPublicDefaultConstructor { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs
new file mode 100644
index 00000000000..4e685496097
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/IntentFilterInfo.cs
@@ -0,0 +1,26 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Describes an [IntentFilter] attribute on a component type.
+///
+public sealed record IntentFilterInfo
+{
+ ///
+ /// Action names from the first constructor argument (string[]).
+ ///
+ public IReadOnlyList Actions { get; init; } = [];
+
+ ///
+ /// Category names.
+ ///
+ public IReadOnlyList Categories { get; init; } = [];
+
+ ///
+ /// Named properties (DataScheme, DataHost, DataPath, Label, Icon, Priority, etc.).
+ ///
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index adb6a052f5e..aa9aa497e55 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -119,9 +119,8 @@ public sealed record JavaPeerInfo
public bool IsGenericDefinition { get; init; }
///
- /// Component attribute information ([Activity], [Service], [BroadcastReceiver],
- /// [ContentProvider], [Application], [Instrumentation]).
- /// Null for types that are not Android components.
+ /// Android component attribute data ([Activity], [Service], [BroadcastReceiver], [ContentProvider],
+ /// [Application], [Instrumentation]) if present on this type. Used for manifest generation.
///
public ComponentInfo? ComponentAttribute { get; init; }
}
@@ -316,3 +315,4 @@ public enum ActivationCtorStyle
///
JavaInterop,
}
+
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/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs
new file mode 100644
index 00000000000..f363e824cd8
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetaDataInfo.cs
@@ -0,0 +1,24 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Describes a [MetaData] attribute on a component type.
+///
+public sealed record MetaDataInfo
+{
+ ///
+ /// The metadata name (first constructor argument).
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// The Value property, if set.
+ ///
+ public string? Value { get; init; }
+
+ ///
+ /// The Resource property, if set.
+ ///
+ public string? Resource { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs
new file mode 100644
index 00000000000..d2ce40faf3a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionGroupInfo.cs
@@ -0,0 +1,11 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record PermissionGroupInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs
new file mode 100644
index 00000000000..908e2f2db60
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionInfo.cs
@@ -0,0 +1,11 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record PermissionInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs
new file mode 100644
index 00000000000..a7ddc2a9933
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PermissionTreeInfo.cs
@@ -0,0 +1,11 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record PermissionTreeInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyDictionary Properties { get; init; } = new Dictionary ();
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs
new file mode 100644
index 00000000000..826f413bd08
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/PropertyInfo.cs
@@ -0,0 +1,10 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+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/UsesConfigurationInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs
new file mode 100644
index 00000000000..af2c13a27d3
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesConfigurationInfo.cs
@@ -0,0 +1,12 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+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; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs
new file mode 100644
index 00000000000..91808621911
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesFeatureInfo.cs
@@ -0,0 +1,21 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Describes a [UsesFeature] attribute.
+///
+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;
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs
new file mode 100644
index 00000000000..39b75fabb17
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesLibraryInfo.cs
@@ -0,0 +1,9 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record UsesLibraryInfo
+{
+ public required string Name { get; init; }
+ public bool Required { get; init; } = true;
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs
new file mode 100644
index 00000000000..84202cd6ec3
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs
@@ -0,0 +1,9 @@
+#nullable enable
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+public sealed record UsesPermissionInfo
+{
+ public required string Name { get; init; }
+ public int? MaxSdkVersion { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
new file mode 100644
index 00000000000..ef828cea272
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs
@@ -0,0 +1,248 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Core logic for generating trimmable TypeMap assemblies, JCW Java sources, and manifest.
+/// Extracted from the MSBuild task so it can be tested directly without MSBuild ceremony.
+///
+public class TrimmableTypeMapGenerator
+{
+ readonly TaskLoggingHelper log;
+
+ public TrimmableTypeMapGenerator (TaskLoggingHelper log)
+ {
+ this.log = log;
+ }
+
+ ///
+ /// Runs the full generation pipeline: scan assemblies, generate typemap
+ /// assemblies, generate JCW Java sources, and optionally generate the manifest.
+ ///
+ public TrimmableTypeMapResult Execute (
+ IReadOnlyList assemblyPaths,
+ string outputDirectory,
+ string javaSourceOutputDirectory,
+ Version systemRuntimeVersion,
+ HashSet frameworkAssemblyNames,
+ ManifestConfig? manifestConfig,
+ string? manifestTemplatePath,
+ string? mergedManifestOutputPath,
+ string? acwMapOutputPath = null)
+ {
+ Directory.CreateDirectory (outputDirectory);
+ Directory.CreateDirectory (javaSourceOutputDirectory);
+
+ var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblyPaths);
+
+ if (allPeers.Count == 0) {
+ log.LogMessage (MessageImportance.Low, "No Java peer types found, skipping typemap generation.");
+ return new TrimmableTypeMapResult ([], [], null);
+ }
+
+ var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory);
+
+ // 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.LogMessage (MessageImportance.Low, "Generating JCW files for {0} types (filtered from {1} total).", jcwPeers.Count, allPeers.Count);
+ var generatedJavaFiles = GenerateJcwJavaSources (jcwPeers, javaSourceOutputDirectory);
+
+ // Generate manifest if output path is configured
+ string[]? additionalProviderSources = null;
+ if (mergedManifestOutputPath is not null && mergedManifestOutputPath.Length > 0 && manifestConfig is not null && !manifestConfig.PackageName.IsNullOrEmpty ()) {
+ additionalProviderSources = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplatePath, mergedManifestOutputPath);
+ }
+
+ // Write acw-map.txt so _ConvertCustomView and _UpdateAndroidResgen can resolve custom view names.
+ if (!acwMapOutputPath.IsNullOrEmpty ()) {
+ using var writer = new StreamWriter (acwMapOutputPath!, append: false);
+ AcwMapWriter.Write (writer, allPeers);
+ log.LogMessage (MessageImportance.Low, "Written acw-map.txt with {0} entries to {1}.", allPeers.Count, acwMapOutputPath);
+ }
+
+ return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles, additionalProviderSources);
+ }
+
+ // Future optimization: the scanner currently scans all assemblies on every run.
+ // For incremental builds, we could:
+ // 1. Add a Scan(allPaths, changedPaths) overload that only produces JavaPeerInfo
+ // for changed assemblies while still indexing all assemblies for cross-assembly
+ // 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 peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList assemblyPaths)
+ {
+ using var scanner = new JavaPeerScanner ();
+ var peers = scanner.Scan (assemblyPaths);
+ var manifestInfo = scanner.ScanAssemblyManifestInfo ();
+ log.LogMessage (MessageImportance.Low, "Scanned {0} assemblies, found {1} Java peer types.", assemblyPaths.Count, peers.Count);
+ return (peers, manifestInfo);
+ }
+
+ List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion,
+ IReadOnlyList assemblyPaths, string outputDir)
+ {
+ // Build a map from assembly name → source path for timestamp comparison
+ var sourcePathByName = new Dictionary (StringComparer.Ordinal);
+ foreach (var path in assemblyPaths) {
+ var name = Path.GetFileNameWithoutExtension (path);
+ sourcePathByName [name] = path;
+ }
+
+ var peersByAssembly = allPeers
+ .GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
+ .OrderBy (g => g.Key, StringComparer.Ordinal);
+
+ var generatedAssemblies = new List ();
+ var perAssemblyNames = new List ();
+ var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
+ bool anyRegenerated = false;
+
+ foreach (var group in peersByAssembly) {
+ string assemblyName = $"_{group.Key}.TypeMap";
+ string outputPath = Path.Combine (outputDir, assemblyName + ".dll");
+ perAssemblyNames.Add (assemblyName);
+
+ if (IsUpToDate (outputPath, group.Key, sourcePathByName)) {
+ log.LogMessage (MessageImportance.Low, " {0}: up to date, skipping", assemblyName);
+ generatedAssemblies.Add (outputPath);
+ continue;
+ }
+
+ generator.Generate (group.ToList (), outputPath, assemblyName);
+ generatedAssemblies.Add (outputPath);
+ anyRegenerated = true;
+
+ log.LogMessage (MessageImportance.Low, " {0}: {1} types", assemblyName, group.Count ());
+ }
+
+ // Root assembly references all per-assembly typemaps — regenerate if any changed
+ string rootOutputPath = Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll");
+ if (anyRegenerated || !File.Exists (rootOutputPath)) {
+ var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
+ rootGenerator.Generate (perAssemblyNames, rootOutputPath);
+ log.LogMessage (MessageImportance.Low, " Root: {0} per-assembly refs", perAssemblyNames.Count);
+ } else {
+ log.LogMessage (MessageImportance.Low, " Root: up to date, skipping");
+ }
+ generatedAssemblies.Add (rootOutputPath);
+
+ log.LogMessage (MessageImportance.Low, "Generated {0} typemap assemblies.", generatedAssemblies.Count);
+ return generatedAssemblies;
+ }
+
+ internal static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName)
+ {
+ if (!File.Exists (outputPath)) {
+ return false;
+ }
+ if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) {
+ return false;
+ }
+ return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath);
+ }
+
+ List GenerateJcwJavaSources (List allPeers, string javaSourceOutputDirectory)
+ {
+ var jcwGenerator = new JcwJavaSourceGenerator ();
+ var files = jcwGenerator.Generate (allPeers, javaSourceOutputDirectory);
+ log.LogMessage (MessageImportance.Low, "Generated {0} JCW Java source files.", files.Count);
+ return files.ToList ();
+ }
+
+ string[]? GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo,
+ ManifestConfig config, string? manifestTemplatePath, string mergedManifestOutputPath)
+ {
+ // Validate components
+ ValidateComponents (allPeers, assemblyManifestInfo);
+ if (log.HasLoggedErrors) {
+ return null;
+ }
+
+ string minSdk = "21";
+ if (!config.SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (config.SupportedOSPlatformVersion, out var sopv)) {
+ minSdk = sopv.Major.ToString (CultureInfo.InvariantCulture);
+ }
+
+ string targetSdk = config.AndroidApiLevel ?? "36";
+ if (Version.TryParse (targetSdk, out var apiVersion)) {
+ targetSdk = apiVersion.Major.ToString (CultureInfo.InvariantCulture);
+ }
+
+ bool forceDebuggable = !config.CheckedBuild.IsNullOrEmpty ();
+
+ var generator = new ManifestGenerator {
+ PackageName = config.PackageName,
+ ApplicationLabel = config.ApplicationLabel ?? config.PackageName,
+ VersionCode = config.VersionCode ?? "",
+ VersionName = config.VersionName ?? "",
+ MinSdkVersion = minSdk,
+ TargetSdkVersion = targetSdk,
+ AndroidRuntime = config.AndroidRuntime ?? "coreclr",
+ Debug = config.Debug,
+ NeedsInternet = config.NeedsInternet,
+ EmbedAssemblies = config.EmbedAssemblies,
+ ForceDebuggable = forceDebuggable,
+ ForceExtractNativeLibs = forceDebuggable,
+ ManifestPlaceholders = config.ManifestPlaceholders,
+ ApplicationJavaClass = config.ApplicationJavaClass,
+ };
+
+ var providerNames = generator.Generate (manifestTemplatePath, allPeers, assemblyManifestInfo, mergedManifestOutputPath);
+ return providerNames.ToArray ();
+ }
+
+ public static Version ParseTargetFrameworkVersion (string tfv)
+ {
+ if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
+ tfv = tfv.Substring (1);
+ }
+ if (Version.TryParse (tfv, out var version)) {
+ return version;
+ }
+ 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.LogError (null, "XA4213", null, null, 0, 0, 0, 0, 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;
+ if (applicationTypes.Count > 1) {
+ log.LogError (null, "XA4212", null, null, 0, 0, 0, 0, Properties.Resources.XA4212, string.Join (", ", applicationTypes));
+ } else if (applicationTypes.Count > 0 && hasAssemblyLevelApplication) {
+ log.LogError (null, "XA4217", null, null, 0, 0, 0, 0, Properties.Resources.XA4217);
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs
new file mode 100644
index 00000000000..6165ba56f4b
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs
@@ -0,0 +1,31 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Configuration for manifest generation, passed from the MSBuild task.
+///
+public record ManifestConfig (
+ string PackageName,
+ string? ApplicationLabel,
+ string? VersionCode,
+ string? VersionName,
+ string? AndroidApiLevel,
+ string? SupportedOSPlatformVersion,
+ string? AndroidRuntime,
+ bool Debug,
+ bool NeedsInternet,
+ bool EmbedAssemblies,
+ string? ManifestPlaceholders,
+ string? CheckedBuild,
+ string? ApplicationJavaClass);
+
+///
+/// Result of the trimmable type map generation.
+///
+public record TrimmableTypeMapResult (
+ IReadOnlyList GeneratedAssemblies,
+ IReadOnlyList GeneratedJavaFiles,
+ string[]? AdditionalProviderSources);
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/ApplicationRegistration.Trimmable.java b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java
new file mode 100644
index 00000000000..d03cc3b3c7e
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/ApplicationRegistration.Trimmable.java
@@ -0,0 +1,13 @@
+package net.dot.android;
+
+public class ApplicationRegistration {
+
+ public static android.content.Context Context;
+
+ // In the trimmable typemap path, Application/Instrumentation types are activated
+ // via Runtime.registerNatives() + UCO wrappers, not Runtime.register(), so this
+ // method is intentionally empty.
+ public static void registerApplications ()
+ {
+ }
+}
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..00ccdf58e3a 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,185 @@
<_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)"
+ AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt">
-
+
-
+
+
-
-
-
-
+
+
- <_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)/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_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..38868b288e3 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs
@@ -12,9 +12,8 @@
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.
+/// MSBuild task adapter for .
+/// Opens files and maps ITaskItem to/from strings, then delegates to the core class.
///
public class GenerateTrimmableTypeMap : AndroidTask
{
@@ -29,12 +28,14 @@ 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; } = "";
+ ///
+ /// Output path for the merged acw-map.txt consumed by _ConvertCustomView and _UpdateAndroidResgen.
+ ///
+ public string? AcwMapOutputFile { get; set; }
+
///
/// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime
/// assembly reference version in generated typemap assemblies.
@@ -42,188 +43,133 @@ public class GenerateTrimmableTypeMap : AndroidTask
[Required]
public string TargetFrameworkVersion { get; set; } = "";
- [Output]
- public ITaskItem []? GeneratedAssemblies { get; set; }
-
- [Output]
- 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.
+ /// User's AndroidManifest.xml template. May be null if no template exists.
///
- [Output]
- public ITaskItem []? PerAssemblyAcwMapFiles { get; set; }
-
- public override bool RunTask ()
- {
- var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion);
- var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies);
+ public string? ManifestTemplate { get; set; }
- Directory.CreateDirectory (OutputDirectory);
- Directory.CreateDirectory (JavaSourceOutputDirectory);
- Directory.CreateDirectory (AcwMapDirectory);
+ ///
+ /// Output path for the merged AndroidManifest.xml.
+ ///
+ public string? MergedAndroidManifestOutput { get; set; }
- var allPeers = ScanAssemblies (assemblyPaths);
- if (allPeers.Count == 0) {
- Log.LogDebugMessage ("No Java peer types found, skipping typemap generation.");
- return !Log.HasLoggedErrors;
- }
+ ///
+ /// Android package name (e.g., "com.example.myapp").
+ ///
+ public string? PackageName { get; set; }
- GeneratedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths);
- GeneratedJavaFiles = GenerateJcwJavaSources (allPeers);
- PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (allPeers);
+ ///
+ /// Application label for the manifest.
+ ///
+ public string? ApplicationLabel { get; set; }
- return !Log.HasLoggedErrors;
- }
+ public string? VersionCode { get; set; }
- // Future optimization: the scanner currently scans all assemblies on every run.
- // For incremental builds, we could:
- // 1. Add a Scan(allPaths, changedPaths) overload that only produces JavaPeerInfo
- // for changed assemblies while still indexing all assemblies for cross-assembly
- // 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)
- {
- using var scanner = new JavaPeerScanner ();
- var peers = scanner.Scan (assemblyPaths);
- Log.LogDebugMessage ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types.");
- return peers;
- }
+ public string? VersionName { get; set; }
- ITaskItem [] GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion,
- IReadOnlyList assemblyPaths)
- {
- // Build a map from assembly name → source path for timestamp comparison
- var sourcePathByName = new Dictionary (StringComparer.Ordinal);
- foreach (var path in assemblyPaths) {
- var name = Path.GetFileNameWithoutExtension (path);
- sourcePathByName [name] = path;
- }
+ ///
+ /// Target Android API level (e.g., "36").
+ ///
+ public string? AndroidApiLevel { get; set; }
- var peersByAssembly = allPeers
- .GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
- .OrderBy (g => g.Key, StringComparer.Ordinal);
+ ///
+ /// Supported OS platform version (e.g., "21.0").
+ ///
+ public string? SupportedOSPlatformVersion { get; set; }
- var generatedAssemblies = new List ();
- var perAssemblyNames = new List ();
- var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
- bool anyRegenerated = false;
+ ///
+ /// Android runtime type ("mono", "coreclr", "nativeaot").
+ ///
+ public string? AndroidRuntime { get; set; }
- foreach (var group in peersByAssembly) {
- string assemblyName = $"_{group.Key}.TypeMap";
- string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll");
- perAssemblyNames.Add (assemblyName);
+ public bool Debug { get; set; }
- if (IsUpToDate (outputPath, group.Key, sourcePathByName)) {
- Log.LogDebugMessage ($" {assemblyName}: up to date, skipping");
- generatedAssemblies.Add (new TaskItem (outputPath));
- continue;
- }
+ public bool NeedsInternet { get; set; }
- generator.Generate (group.ToList (), outputPath, assemblyName);
- generatedAssemblies.Add (new TaskItem (outputPath));
- anyRegenerated = true;
+ public bool EmbedAssemblies { get; set; }
- Log.LogDebugMessage ($" {assemblyName}: {group.Count ()} types");
- }
+ ///
+ /// Manifest placeholder values (e.g., "applicationId=com.example.app;key=value").
+ ///
+ public string? ManifestPlaceholders { get; set; }
- // Root assembly references all per-assembly typemaps — regenerate if any changed
- string rootOutputPath = Path.Combine (OutputDirectory, "_Microsoft.Android.TypeMaps.dll");
- if (anyRegenerated || !File.Exists (rootOutputPath)) {
- var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
- rootGenerator.Generate (perAssemblyNames, rootOutputPath);
- Log.LogDebugMessage ($" Root: {perAssemblyNames.Count} per-assembly refs");
- } else {
- Log.LogDebugMessage ($" Root: up to date, skipping");
- }
- generatedAssemblies.Add (new TaskItem (rootOutputPath));
+ ///
+ /// When set, forces android:debuggable="true" and android:extractNativeLibs="true".
+ ///
+ public string? CheckedBuild { get; set; }
- Log.LogDebugMessage ($"Generated {generatedAssemblies.Count} typemap assemblies.");
- return generatedAssemblies.ToArray ();
- }
+ ///
+ /// Optional custom Application Java class name.
+ ///
+ public string? ApplicationJavaClass { get; set; }
- static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName)
- {
- if (!File.Exists (outputPath)) {
- return false;
- }
- if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) {
- return false;
- }
- return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath);
- }
+ [Output]
+ public ITaskItem []? GeneratedAssemblies { get; set; }
- ITaskItem [] GenerateJcwJavaSources (List allPeers)
- {
- var jcwGenerator = new JcwJavaSourceGenerator ();
- var files = jcwGenerator.Generate (allPeers, JavaSourceOutputDirectory);
- Log.LogDebugMessage ($"Generated {files.Count} JCW Java source files.");
+ [Output]
+ public ITaskItem []? GeneratedJavaFiles { get; set; }
- var items = new ITaskItem [files.Count];
- for (int i = 0; i < files.Count; i++) {
- items [i] = new TaskItem (files [i]);
- }
- return items;
- }
+ ///
+ /// Content provider names for ApplicationRegistration.java.
+ ///
+ [Output]
+ public string []? AdditionalProviderSources { get; set; }
- ITaskItem [] GeneratePerAssemblyAcwMaps (List allPeers)
+ public override bool RunTask ()
{
- var peersByAssembly = allPeers
- .GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
- .OrderBy (g => g.Key, StringComparer.Ordinal);
-
- var outputFiles = new List ();
-
- 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);
+ var systemRuntimeVersion = TrimmableTypeMapGenerator.ParseTargetFrameworkVersion (TargetFrameworkVersion);
+ // 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));
}
-
- Log.LogDebugMessage (written
- ? $" acw-map.{group.Key}.txt: {peers.Count} types"
- : $" acw-map.{group.Key}.txt: unchanged");
-
- var item = new TaskItem (outputFile);
- item.SetMetadata ("AssemblyName", group.Key);
- outputFiles.Add (item);
}
- Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files.");
- return outputFiles.ToArray ();
- }
+ Directory.CreateDirectory (AcwMapDirectory);
- static Version ParseTargetFrameworkVersion (string tfv)
- {
- if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
- tfv = tfv.Substring (1);
+ ManifestConfig? manifestConfig = null;
+ if (!MergedAndroidManifestOutput.IsNullOrEmpty () && !PackageName.IsNullOrEmpty ()) {
+ manifestConfig = new ManifestConfig (
+ PackageName: PackageName,
+ ApplicationLabel: ApplicationLabel,
+ VersionCode: VersionCode,
+ VersionName: VersionName,
+ AndroidApiLevel: AndroidApiLevel,
+ SupportedOSPlatformVersion: SupportedOSPlatformVersion,
+ AndroidRuntime: AndroidRuntime,
+ Debug: Debug,
+ NeedsInternet: NeedsInternet,
+ EmbedAssemblies: EmbedAssemblies,
+ ManifestPlaceholders: ManifestPlaceholders,
+ CheckedBuild: CheckedBuild,
+ ApplicationJavaClass: ApplicationJavaClass);
}
- if (Version.TryParse (tfv, out var version)) {
- return version;
- }
- throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version.");
- }
- ///
- /// Filters resolved assemblies to only those that reference Mono.Android or Java.Interop
- /// (i.e., assemblies that could contain [Register] types). Skips BCL assemblies.
- ///
- static IReadOnlyList GetJavaInteropAssemblyPaths (ITaskItem [] items)
- {
- var paths = new List (items.Length);
- foreach (var item in items) {
- if (MonoAndroidHelper.IsMonoAndroidAssembly (item)) {
- paths.Add (item.ItemSpec);
- }
- }
- return paths;
+ var generator = new TrimmableTypeMapGenerator (Log);
+ var result = generator.Execute (
+ assemblyPaths,
+ OutputDirectory,
+ JavaSourceOutputDirectory,
+ systemRuntimeVersion,
+ frameworkAssemblyNames,
+ manifestConfig,
+ ManifestTemplate,
+ MergedAndroidManifestOutput,
+ AcwMapOutputFile);
+
+ GeneratedAssemblies = result.GeneratedAssemblies.Select (p => (ITaskItem) new TaskItem (p)).ToArray ();
+ GeneratedJavaFiles = result.GeneratedJavaFiles.Select (p => (ITaskItem) new TaskItem (p)).ToArray ();
+ AdditionalProviderSources = result.AdditionalProviderSources;
+
+ return !Log.HasLoggedErrors;
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs
index 3c4665ae958..426a6fa5f6d 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs
@@ -24,8 +24,8 @@ public void Execute_EmptyAssemblyList_Succeeds ()
var task = CreateTask ([], outputDir, javaDir);
Assert.IsTrue (task.Execute (), "Task should succeed with empty assembly list.");
- Assert.IsNull (task.GeneratedAssemblies);
- Assert.IsNull (task.GeneratedJavaFiles);
+ Assert.IsEmpty (task.GeneratedAssemblies);
+ Assert.IsEmpty (task.GeneratedJavaFiles);
}
[Test]
@@ -198,8 +198,8 @@ public void Execute_NoPeersFound_ReturnsEmpty ()
var task = CreateTask (new [] { new TaskItem (nunitDll) }, outputDir, javaDir, messages);
Assert.IsTrue (task.Execute (), "Task should succeed with no peer types.");
- Assert.IsNull (task.GeneratedAssemblies);
- Assert.IsNull (task.GeneratedJavaFiles);
+ Assert.IsEmpty (task.GeneratedAssemblies);
+ Assert.IsEmpty (task.GeneratedJavaFiles);
Assert.IsTrue (messages.Any (m => m.Message.Contains ("No Java peer types found")),
"Should log that no peers were found.");
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc
index f25e2da1a1d..3ee158051f1 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc
@@ -11,34 +11,34 @@
"Size": 18288
},
"lib/arm64-v8a/lib_Java.Interop.dll.so": {
- "Size": 88504
+ "Size": 88056
},
"lib/arm64-v8a/lib_Mono.Android.dll.so": {
- "Size": 125280
+ "Size": 117648
},
"lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": {
- "Size": 26520
+ "Size": 26536
},
"lib/arm64-v8a/lib_System.Console.dll.so": {
- "Size": 24424
+ "Size": 24432
},
"lib/arm64-v8a/lib_System.Linq.dll.so": {
- "Size": 25504
+ "Size": 25512
},
"lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": {
- "Size": 692016
+ "Size": 634840
},
"lib/arm64-v8a/lib_System.Runtime.dll.so": {
- "Size": 20288
+ "Size": 20296
},
"lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": {
- "Size": 21632
+ "Size": 21640
},
"lib/arm64-v8a/lib_UnnamedProject.dll.so": {
"Size": 20032
},
"lib/arm64-v8a/libarc.bin.so": {
- "Size": 19112
+ "Size": 19176
},
"lib/arm64-v8a/libmono-component-marshal-ilgen.so": {
"Size": 36616
@@ -98,5 +98,5 @@
"Size": 1904
}
},
- "PackageSize": 3324437
-}
\ No newline at end of file
+ "PackageSize": 3267093
+}
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/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index 10489637722..d892b0161da 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -82,7 +82,6 @@ public void Generate_ProxyType_HasCtorAndCreateInstance ()
Assert.Contains (".ctor", methods);
Assert.Contains ("CreateInstance", methods);
- Assert.Contains ("get_TargetType", methods);
}
[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..54ec3dec323 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -561,7 +561,6 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
Assert.Contains (".ctor", methodNames);
Assert.Contains ("CreateInstance", methodNames);
- Assert.Contains ("get_TargetType", methodNames);
});
}
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);