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