diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs
new file mode 100644
index 00000000000..b25a1407a4f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs
@@ -0,0 +1,130 @@
+#nullable enable
+
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Converts Android enum integer values to their XML attribute string representations.
+/// Ported from ManifestDocumentElement.cs.
+///
+static class AndroidEnumConverter
+{
+ public static string? LaunchModeToString (int value) => value switch {
+ 1 => "singleTop",
+ 2 => "singleTask",
+ 3 => "singleInstance",
+ 4 => "singleInstancePerTask",
+ _ => null,
+ };
+
+ public static string? ScreenOrientationToString (int value) => value switch {
+ 0 => "landscape",
+ 1 => "portrait",
+ 3 => "sensor",
+ 4 => "nosensor",
+ 5 => "user",
+ 6 => "behind",
+ 7 => "reverseLandscape",
+ 8 => "reversePortrait",
+ 9 => "sensorLandscape",
+ 10 => "sensorPortrait",
+ 11 => "fullSensor",
+ 12 => "userLandscape",
+ 13 => "userPortrait",
+ 14 => "fullUser",
+ 15 => "locked",
+ -1 => "unspecified",
+ _ => null,
+ };
+
+ public static string? ConfigChangesToString (int value)
+ {
+ var parts = new List ();
+ if ((value & 0x0001) != 0) parts.Add ("mcc");
+ if ((value & 0x0002) != 0) parts.Add ("mnc");
+ if ((value & 0x0004) != 0) parts.Add ("locale");
+ if ((value & 0x0008) != 0) parts.Add ("touchscreen");
+ if ((value & 0x0010) != 0) parts.Add ("keyboard");
+ if ((value & 0x0020) != 0) parts.Add ("keyboardHidden");
+ if ((value & 0x0040) != 0) parts.Add ("navigation");
+ if ((value & 0x0080) != 0) parts.Add ("orientation");
+ if ((value & 0x0100) != 0) parts.Add ("screenLayout");
+ if ((value & 0x0200) != 0) parts.Add ("uiMode");
+ if ((value & 0x0400) != 0) parts.Add ("screenSize");
+ if ((value & 0x0800) != 0) parts.Add ("smallestScreenSize");
+ if ((value & 0x1000) != 0) parts.Add ("density");
+ if ((value & 0x2000) != 0) parts.Add ("layoutDirection");
+ if ((value & 0x4000) != 0) parts.Add ("colorMode");
+ if ((value & 0x8000) != 0) parts.Add ("grammaticalGender");
+ if ((value & 0x10000000) != 0) parts.Add ("fontWeightAdjustment");
+ if ((value & 0x40000000) != 0) parts.Add ("fontScale");
+ return parts.Count > 0 ? string.Join ("|", parts) : null;
+ }
+
+ public static string? SoftInputToString (int value)
+ {
+ var parts = new List ();
+ int state = value & 0x0f;
+ int adjust = value & 0xf0;
+ if (state == 1) parts.Add ("stateUnchanged");
+ else if (state == 2) parts.Add ("stateHidden");
+ else if (state == 3) parts.Add ("stateAlwaysHidden");
+ else if (state == 4) parts.Add ("stateVisible");
+ else if (state == 5) parts.Add ("stateAlwaysVisible");
+ if (adjust == 0x10) parts.Add ("adjustResize");
+ else if (adjust == 0x20) parts.Add ("adjustPan");
+ else if (adjust == 0x30) parts.Add ("adjustNothing");
+ return parts.Count > 0 ? string.Join ("|", parts) : null;
+ }
+
+ public static string? DocumentLaunchModeToString (int value) => value switch {
+ 1 => "intoExisting",
+ 2 => "always",
+ 3 => "never",
+ _ => null,
+ };
+
+ public static string? UiOptionsToString (int value) => value switch {
+ 1 => "splitActionBarWhenNarrow",
+ _ => null,
+ };
+
+ public static string? ForegroundServiceTypeToString (int value)
+ {
+ var parts = new List ();
+ if ((value & 0x00000001) != 0) parts.Add ("dataSync");
+ if ((value & 0x00000002) != 0) parts.Add ("mediaPlayback");
+ if ((value & 0x00000004) != 0) parts.Add ("phoneCall");
+ if ((value & 0x00000008) != 0) parts.Add ("location");
+ if ((value & 0x00000010) != 0) parts.Add ("connectedDevice");
+ if ((value & 0x00000020) != 0) parts.Add ("mediaProjection");
+ if ((value & 0x00000040) != 0) parts.Add ("camera");
+ if ((value & 0x00000080) != 0) parts.Add ("microphone");
+ if ((value & 0x00000100) != 0) parts.Add ("health");
+ if ((value & 0x00000200) != 0) parts.Add ("remoteMessaging");
+ if ((value & 0x00000400) != 0) parts.Add ("systemExempted");
+ if ((value & 0x00000800) != 0) parts.Add ("shortService");
+ if ((value & 0x40000000) != 0) parts.Add ("specialUse");
+ return parts.Count > 0 ? string.Join ("|", parts) : null;
+ }
+
+ public static string? ProtectionToString (int value)
+ {
+ int baseValue = value & 0x0f;
+ return baseValue switch {
+ 0 => "normal",
+ 1 => "dangerous",
+ 2 => "signature",
+ 3 => "signatureOrSystem",
+ _ => null,
+ };
+ }
+
+ public static string? ActivityPersistableModeToString (int value) => value switch {
+ 0 => "persistRootOnly",
+ 1 => "persistAcrossReboots",
+ 2 => "persistNever",
+ _ => null,
+ };
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs
new file mode 100644
index 00000000000..8ba00724f87
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs
@@ -0,0 +1,171 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Adds assembly-level manifest elements (permissions, uses-permissions, uses-features,
+/// uses-library, uses-configuration, meta-data, property).
+///
+static class AssemblyLevelElementBuilder
+{
+ static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
+ static readonly XName AttName = ManifestConstants.AttName;
+
+ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, AssemblyManifestInfo info)
+ {
+ var existingPermissions = new HashSet (
+ manifest.Elements ("permission").Select (e => (string?)e.Attribute (AttName)).OfType ());
+ var existingUsesPermissions = new HashSet (
+ manifest.Elements ("uses-permission").Select (e => (string?)e.Attribute (AttName)).OfType ());
+
+ // elements
+ foreach (var perm in info.Permissions) {
+ if (string.IsNullOrEmpty (perm.Name) || existingPermissions.Contains (perm.Name)) {
+ continue;
+ }
+ var element = new XElement ("permission", new XAttribute (AttName, perm.Name));
+ PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Label", "label");
+ PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Description", "description");
+ PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Icon", "icon");
+ PropertyMapper.MapDictionaryProperties (element, perm.Properties, "PermissionGroup", "permissionGroup");
+ PropertyMapper.MapDictionaryEnumProperty (element, perm.Properties, "ProtectionLevel", "protectionLevel", AndroidEnumConverter.ProtectionToString);
+ manifest.Add (element);
+ }
+
+ // elements
+ foreach (var pg in info.PermissionGroups) {
+ if (string.IsNullOrEmpty (pg.Name)) {
+ continue;
+ }
+ var element = new XElement ("permission-group", new XAttribute (AttName, pg.Name));
+ PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Label", "label");
+ PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Description", "description");
+ PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Icon", "icon");
+ manifest.Add (element);
+ }
+
+ // elements
+ foreach (var pt in info.PermissionTrees) {
+ if (string.IsNullOrEmpty (pt.Name)) {
+ continue;
+ }
+ var element = new XElement ("permission-tree", new XAttribute (AttName, pt.Name));
+ PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Label", "label");
+ PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Icon", "icon");
+ manifest.Add (element);
+ }
+
+ // elements
+ foreach (var up in info.UsesPermissions) {
+ if (string.IsNullOrEmpty (up.Name) || existingUsesPermissions.Contains (up.Name)) {
+ continue;
+ }
+ var element = new XElement ("uses-permission", new XAttribute (AttName, up.Name));
+ if (up.MaxSdkVersion.HasValue) {
+ element.SetAttributeValue (AndroidNs + "maxSdkVersion", up.MaxSdkVersion.Value.ToString (CultureInfo.InvariantCulture));
+ }
+ manifest.Add (element);
+ }
+
+ // elements
+ var existingFeatures = new HashSet (
+ manifest.Elements ("uses-feature").Select (e => (string?)e.Attribute (AttName)).OfType ());
+ foreach (var uf in info.UsesFeatures) {
+ if (uf.Name is not null && !existingFeatures.Contains (uf.Name)) {
+ var element = new XElement ("uses-feature",
+ new XAttribute (AttName, uf.Name),
+ new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false"));
+ manifest.Add (element);
+ } else if (uf.GLESVersion != 0) {
+ var versionStr = $"0x{uf.GLESVersion:X8}";
+ if (!manifest.Elements ("uses-feature").Any (e => (string?)e.Attribute (AndroidNs + "glEsVersion") == versionStr)) {
+ var element = new XElement ("uses-feature",
+ new XAttribute (AndroidNs + "glEsVersion", versionStr),
+ new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false"));
+ manifest.Add (element);
+ }
+ }
+ }
+
+ // elements inside
+ foreach (var ul in info.UsesLibraries) {
+ if (string.IsNullOrEmpty (ul.Name)) {
+ continue;
+ }
+ if (!app.Elements ("uses-library").Any (e => (string?)e.Attribute (AttName) == ul.Name)) {
+ app.Add (new XElement ("uses-library",
+ new XAttribute (AttName, ul.Name),
+ new XAttribute (AndroidNs + "required", ul.Required ? "true" : "false")));
+ }
+ }
+
+ // Assembly-level inside
+ foreach (var md in info.MetaData) {
+ if (string.IsNullOrEmpty (md.Name)) {
+ continue;
+ }
+ if (!app.Elements ("meta-data").Any (e => (string?)e.Attribute (AndroidNs + "name") == md.Name)) {
+ app.Add (ComponentElementBuilder.CreateMetaDataElement (md));
+ }
+ }
+
+ // Assembly-level inside
+ foreach (var prop in info.Properties) {
+ if (string.IsNullOrEmpty (prop.Name)) {
+ continue;
+ }
+ if (!app.Elements ("property").Any (e => (string?)e.Attribute (AndroidNs + "name") == prop.Name)) {
+ var element = new XElement ("property",
+ new XAttribute (AndroidNs + "name", prop.Name));
+ if (prop.Value is not null) {
+ element.SetAttributeValue (AndroidNs + "value", prop.Value);
+ }
+ if (prop.Resource is not null) {
+ element.SetAttributeValue (AndroidNs + "resource", prop.Resource);
+ }
+ app.Add (element);
+ }
+ }
+
+ // elements
+ foreach (var uc in info.UsesConfigurations) {
+ var element = new XElement ("uses-configuration");
+ if (uc.ReqFiveWayNav) {
+ element.SetAttributeValue (AndroidNs + "reqFiveWayNav", "true");
+ }
+ if (uc.ReqHardKeyboard) {
+ element.SetAttributeValue (AndroidNs + "reqHardKeyboard", "true");
+ }
+ if (uc.ReqKeyboardType is not null) {
+ element.SetAttributeValue (AndroidNs + "reqKeyboardType", uc.ReqKeyboardType);
+ }
+ if (uc.ReqNavigation is not null) {
+ element.SetAttributeValue (AndroidNs + "reqNavigation", uc.ReqNavigation);
+ }
+ if (uc.ReqTouchScreen is not null) {
+ element.SetAttributeValue (AndroidNs + "reqTouchScreen", uc.ReqTouchScreen);
+ }
+ manifest.Add (element);
+ }
+ }
+
+ internal static void ApplyApplicationProperties (XElement app, Dictionary properties)
+ {
+ PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true);
+ }
+
+ internal static void AddInternetPermission (XElement manifest)
+ {
+ if (!manifest.Elements ("uses-permission").Any (p =>
+ (string?)p.Attribute (AndroidNs + "name") == "android.permission.INTERNET")) {
+ manifest.Add (new XElement ("uses-permission",
+ new XAttribute (AndroidNs + "name", "android.permission.INTERNET")));
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs
new file mode 100644
index 00000000000..9f6f8e7f516
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs
@@ -0,0 +1,184 @@
+#nullable enable
+
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Builds XML elements for individual Android components (Activity, Service, BroadcastReceiver, ContentProvider).
+///
+static class ComponentElementBuilder
+{
+ static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
+ static readonly XName AttName = ManifestConstants.AttName;
+
+ internal static XElement? CreateComponentElement (JavaPeerInfo peer, string jniName)
+ {
+ var component = peer.ComponentAttribute;
+ if (component is null) {
+ return null;
+ }
+
+ string elementName = component.Kind switch {
+ ComponentKind.Activity => "activity",
+ ComponentKind.Service => "service",
+ ComponentKind.BroadcastReceiver => "receiver",
+ ComponentKind.ContentProvider => "provider",
+ _ => throw new NotSupportedException ($"Unsupported component kind: {component.Kind}"),
+ };
+
+ var element = new XElement (elementName, new XAttribute (AttName, jniName));
+
+ // Map known properties to android: attributes
+ PropertyMapper.MapComponentProperties (element, component);
+
+ // Add intent filters
+ foreach (var intentFilter in component.IntentFilters) {
+ element.Add (CreateIntentFilterElement (intentFilter));
+ }
+
+ // Handle MainLauncher for activities
+ if (component.Kind == ComponentKind.Activity && component.Properties.TryGetValue ("MainLauncher", out var ml) && ml is bool b && b) {
+ AddLauncherIntentFilter (element);
+ }
+
+ // Add metadata
+ foreach (var meta in component.MetaData) {
+ element.Add (CreateMetaDataElement (meta));
+ }
+
+ return element;
+ }
+
+ internal static void AddLauncherIntentFilter (XElement activity)
+ {
+ // Check if there's already a launcher intent filter
+ if (activity.Elements ("intent-filter").Any (f =>
+ f.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.MAIN") &&
+ f.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.LAUNCHER"))) {
+ return;
+ }
+
+ // Add android:exported="true" if not already present
+ if (activity.Attribute (AndroidNs + "exported") is null) {
+ activity.Add (new XAttribute (AndroidNs + "exported", "true"));
+ }
+
+ var filter = new XElement ("intent-filter",
+ new XElement ("action", new XAttribute (AttName, "android.intent.action.MAIN")),
+ new XElement ("category", new XAttribute (AttName, "android.intent.category.LAUNCHER")));
+ activity.AddFirst (filter);
+ }
+
+ internal static XElement CreateIntentFilterElement (IntentFilterInfo intentFilter)
+ {
+ var filter = new XElement ("intent-filter");
+
+ foreach (var action in intentFilter.Actions) {
+ filter.Add (new XElement ("action", new XAttribute (AttName, action)));
+ }
+
+ foreach (var category in intentFilter.Categories) {
+ filter.Add (new XElement ("category", new XAttribute (AttName, category)));
+ }
+
+ // Map IntentFilter properties to XML attributes
+ if (intentFilter.Properties.TryGetValue ("Label", out var label) && label is string labelStr) {
+ filter.SetAttributeValue (AndroidNs + "label", labelStr);
+ }
+ if (intentFilter.Properties.TryGetValue ("Icon", out var icon) && icon is string iconStr) {
+ filter.SetAttributeValue (AndroidNs + "icon", iconStr);
+ }
+ if (intentFilter.Properties.TryGetValue ("Priority", out var priority) && priority is int priorityInt) {
+ filter.SetAttributeValue (AndroidNs + "priority", priorityInt.ToString (CultureInfo.InvariantCulture));
+ }
+
+ // Data elements
+ AddIntentFilterDataElement (filter, intentFilter);
+
+ return filter;
+ }
+
+ internal static void AddIntentFilterDataElement (XElement filter, IntentFilterInfo intentFilter)
+ {
+ var dataElement = new XElement ("data");
+ bool hasData = false;
+
+ if (intentFilter.Properties.TryGetValue ("DataScheme", out var scheme) && scheme is string schemeStr) {
+ dataElement.SetAttributeValue (AndroidNs + "scheme", schemeStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataHost", out var host) && host is string hostStr) {
+ dataElement.SetAttributeValue (AndroidNs + "host", hostStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataPath", out var path) && path is string pathStr) {
+ dataElement.SetAttributeValue (AndroidNs + "path", pathStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataPathPattern", out var pattern) && pattern is string patternStr) {
+ dataElement.SetAttributeValue (AndroidNs + "pathPattern", patternStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataPathPrefix", out var prefix) && prefix is string prefixStr) {
+ dataElement.SetAttributeValue (AndroidNs + "pathPrefix", prefixStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataMimeType", out var mime) && mime is string mimeStr) {
+ dataElement.SetAttributeValue (AndroidNs + "mimeType", mimeStr);
+ hasData = true;
+ }
+ if (intentFilter.Properties.TryGetValue ("DataPort", out var port) && port is string portStr) {
+ dataElement.SetAttributeValue (AndroidNs + "port", portStr);
+ hasData = true;
+ }
+
+ if (hasData) {
+ filter.Add (dataElement);
+ }
+ }
+
+ internal static XElement CreateMetaDataElement (MetaDataInfo meta)
+ {
+ var element = new XElement ("meta-data",
+ new XAttribute (AndroidNs + "name", meta.Name));
+
+ if (meta.Value is not null) {
+ element.SetAttributeValue (AndroidNs + "value", meta.Value);
+ }
+ if (meta.Resource is not null) {
+ element.SetAttributeValue (AndroidNs + "resource", meta.Resource);
+ }
+ return element;
+ }
+
+ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer)
+ {
+ string jniName = peer.JavaName.Replace ('/', '.');
+ app.SetAttributeValue (AttName, jniName);
+
+ var component = peer.ComponentAttribute;
+ if (component is null) {
+ return;
+ }
+ PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings);
+ }
+
+ internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer)
+ {
+ string jniName = peer.JavaName.Replace ('/', '.');
+ var element = new XElement ("instrumentation",
+ new XAttribute (AttName, jniName));
+
+ var component = peer.ComponentAttribute;
+ if (component is null) {
+ return;
+ }
+ PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings);
+
+ manifest.Add (element);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs
new file mode 100644
index 00000000000..4ed62e9baa6
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs
@@ -0,0 +1,11 @@
+#nullable enable
+
+using System.Xml.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+static class ManifestConstants
+{
+ public static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android";
+ public static readonly XName AttName = AndroidNs + "name";
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
new file mode 100644
index 00000000000..938c60fd10a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs
@@ -0,0 +1,288 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates AndroidManifest.xml from component attributes captured by the JavaPeerScanner.
+/// This is the trimmable-path equivalent of ManifestDocument — it works from ComponentInfo
+/// records instead of Cecil TypeDefinitions.
+///
+class ManifestGenerator
+{
+ static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
+ static readonly XName AttName = ManifestConstants.AttName;
+ static readonly char [] PlaceholderSeparators = [';'];
+
+ int appInitOrder = 2000000000;
+
+ public string PackageName { get; set; } = "";
+ public string ApplicationLabel { get; set; } = "";
+ public string VersionCode { get; set; } = "";
+ public string VersionName { get; set; } = "";
+ public string MinSdkVersion { get; set; } = "21";
+ public string TargetSdkVersion { get; set; } = "36";
+ public string AndroidRuntime { get; set; } = "coreclr";
+ public bool Debug { get; set; }
+ public bool NeedsInternet { get; set; }
+ public bool EmbedAssemblies { get; set; }
+ public bool ForceDebuggable { get; set; }
+ public bool ForceExtractNativeLibs { get; set; }
+ public string? ManifestPlaceholders { get; set; }
+ public string? ApplicationJavaClass { get; set; }
+
+ ///
+ /// Generates the merged manifest and writes it to .
+ /// Returns the list of additional content provider names (for ApplicationRegistration.java).
+ ///
+ public IList Generate (
+ string? manifestTemplatePath,
+ IReadOnlyList allPeers,
+ AssemblyManifestInfo assemblyInfo,
+ string outputPath)
+ {
+ var doc = LoadOrCreateManifest (manifestTemplatePath);
+ var manifest = doc.Root;
+ if (manifest is null) {
+ throw new InvalidOperationException ("Manifest document has no root element.");
+ }
+
+ EnsureManifestAttributes (manifest);
+ var app = EnsureApplicationElement (manifest);
+
+ // Apply assembly-level [Application] properties
+ if (assemblyInfo.ApplicationProperties is not null) {
+ AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties);
+ }
+
+ var existingTypes = new HashSet (
+ app.Descendants ().Select (a => (string?)a.Attribute (AttName)).OfType ());
+
+ // Add components from scanned types
+ foreach (var peer in allPeers) {
+ if (peer.IsAbstract || peer.ComponentAttribute is null) {
+ continue;
+ }
+
+ // Skip Application types (handled separately via assembly-level attribute)
+ if (peer.ComponentAttribute.Kind == ComponentKind.Application) {
+ ComponentElementBuilder.UpdateApplicationElement (app, peer);
+ continue;
+ }
+
+ if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) {
+ ComponentElementBuilder.AddInstrumentation (manifest, peer);
+ continue;
+ }
+
+ string jniName = peer.JavaName.Replace ('/', '.');
+ if (existingTypes.Contains (jniName)) {
+ continue;
+ }
+
+ var element = ComponentElementBuilder.CreateComponentElement (peer, jniName);
+ if (element is not null) {
+ app.Add (element);
+ }
+ }
+
+ // Add assembly-level manifest elements
+ AssemblyLevelElementBuilder.AddAssemblyLevelElements (manifest, app, assemblyInfo);
+
+ // Add runtime provider
+ var providerNames = AddRuntimeProviders (app);
+
+ // Set ApplicationJavaClass
+ if (!string.IsNullOrEmpty (ApplicationJavaClass) && app.Attribute (AttName) is null) {
+ app.SetAttributeValue (AttName, ApplicationJavaClass);
+ }
+
+ // Handle debuggable
+ bool needDebuggable = Debug && app.Attribute (AndroidNs + "debuggable") is null;
+ if (ForceDebuggable || needDebuggable) {
+ app.SetAttributeValue (AndroidNs + "debuggable", "true");
+ }
+
+ // Handle extractNativeLibs
+ if (ForceExtractNativeLibs) {
+ app.SetAttributeValue (AndroidNs + "extractNativeLibs", "true");
+ }
+
+ // Add internet permission for debug
+ if (Debug || NeedsInternet) {
+ AssemblyLevelElementBuilder.AddInternetPermission (manifest);
+ }
+
+ // Apply manifest placeholders
+ string? placeholders = ManifestPlaceholders;
+ if (placeholders is not null && placeholders.Length > 0) {
+ ApplyPlaceholders (doc, placeholders);
+ }
+
+ // Write output
+ var outputDir = Path.GetDirectoryName (outputPath);
+ if (outputDir is not null) {
+ Directory.CreateDirectory (outputDir);
+ }
+ doc.Save (outputPath);
+
+ return providerNames;
+ }
+
+ XDocument LoadOrCreateManifest (string? templatePath)
+ {
+ if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) {
+ return XDocument.Load (templatePath);
+ }
+
+ return new XDocument (
+ new XDeclaration ("1.0", "utf-8", null),
+ new XElement ("manifest",
+ new XAttribute (XNamespace.Xmlns + "android", AndroidNs.NamespaceName),
+ new XAttribute ("package", PackageName)));
+ }
+
+ void EnsureManifestAttributes (XElement manifest)
+ {
+ manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName);
+
+ if (string.IsNullOrEmpty ((string?)manifest.Attribute ("package"))) {
+ manifest.SetAttributeValue ("package", PackageName);
+ }
+
+ if (manifest.Attribute (AndroidNs + "versionCode") is null) {
+ manifest.SetAttributeValue (AndroidNs + "versionCode",
+ string.IsNullOrEmpty (VersionCode) ? "1" : VersionCode);
+ }
+
+ if (manifest.Attribute (AndroidNs + "versionName") is null) {
+ manifest.SetAttributeValue (AndroidNs + "versionName",
+ string.IsNullOrEmpty (VersionName) ? "1.0" : VersionName);
+ }
+
+ // Add
+ if (!manifest.Elements ("uses-sdk").Any ()) {
+ manifest.AddFirst (new XElement ("uses-sdk",
+ new XAttribute (AndroidNs + "minSdkVersion", MinSdkVersion),
+ new XAttribute (AndroidNs + "targetSdkVersion", TargetSdkVersion)));
+ }
+ }
+
+ XElement EnsureApplicationElement (XElement manifest)
+ {
+ var app = manifest.Element ("application");
+ if (app is null) {
+ app = new XElement ("application");
+ manifest.Add (app);
+ }
+
+ if (app.Attribute (AndroidNs + "label") is null && !string.IsNullOrEmpty (ApplicationLabel)) {
+ app.SetAttributeValue (AndroidNs + "label", ApplicationLabel);
+ }
+
+ return app;
+ }
+
+ IList AddRuntimeProviders (XElement app)
+ {
+ string packageName = "mono";
+ string className = "MonoRuntimeProvider";
+
+ if (string.Equals (AndroidRuntime, "nativeaot", StringComparison.OrdinalIgnoreCase)) {
+ packageName = "net.dot.jni.nativeaot";
+ className = "NativeAotRuntimeProvider";
+ }
+
+ // Check if runtime provider already exists in template
+ string runtimeProviderName = $"{packageName}.{className}";
+ if (!app.Elements ("provider").Any (p => {
+ var name = (string?)p.Attribute (ManifestConstants.AttName);
+ return name == runtimeProviderName ||
+ ((string?)p.Attribute (AndroidNs.GetName ("authorities")))?.EndsWith (".__mono_init__", StringComparison.Ordinal) == true;
+ })) {
+ app.Add (CreateRuntimeProvider (runtimeProviderName, null, --appInitOrder));
+ }
+
+ var providerNames = new List ();
+ var processAttrName = AndroidNs.GetName ("process");
+ var procs = new List ();
+
+ foreach (var el in app.Elements ()) {
+ var proc = el.Attribute (processAttrName);
+ if (proc is null || procs.Contains (proc.Value)) {
+ continue;
+ }
+ if (el.Name.NamespaceName != "") {
+ continue;
+ }
+ switch (el.Name.LocalName) {
+ case "provider":
+ var autho = el.Attribute (AndroidNs.GetName ("authorities"));
+ if (autho is not null && autho.Value.EndsWith (".__mono_init__", StringComparison.Ordinal)) {
+ continue;
+ }
+ goto case "activity";
+ case "activity":
+ case "receiver":
+ case "service":
+ procs.Add (proc.Value);
+ string providerName = $"{className}_{procs.Count}";
+ providerNames.Add (providerName);
+ app.Add (CreateRuntimeProvider ($"{packageName}.{providerName}", proc.Value, --appInitOrder));
+ break;
+ }
+ }
+
+ return providerNames;
+ }
+
+ XElement CreateRuntimeProvider (string name, string? processName, int initOrder)
+ {
+ return new XElement ("provider",
+ new XAttribute (AndroidNs + "name", name),
+ new XAttribute (AndroidNs + "exported", "false"),
+ new XAttribute (AndroidNs + "initOrder", initOrder),
+ processName is not null ? new XAttribute (AndroidNs + "process", processName) : null,
+ new XAttribute (AndroidNs + "authorities", PackageName + "." + name + ".__mono_init__"));
+ }
+
+ ///
+ /// Replaces ${key} placeholders in all attribute values throughout the document.
+ /// Placeholder format: "key1=value1;key2=value2"
+ ///
+ static void ApplyPlaceholders (XDocument doc, string placeholders)
+ {
+ var replacements = new Dictionary (StringComparer.Ordinal);
+ foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) {
+ var eqIndex = entry.IndexOf ('=');
+ if (eqIndex > 0) {
+ var key = entry.Substring (0, eqIndex).Trim ();
+ var value = entry.Substring (eqIndex + 1).Trim ();
+ replacements ["${" + key + "}"] = value;
+ }
+ }
+
+ if (replacements.Count == 0) {
+ return;
+ }
+
+ foreach (var element in doc.Descendants ()) {
+ foreach (var attr in element.Attributes ()) {
+ var val = attr.Value;
+ foreach (var kvp in replacements) {
+ if (val.Contains (kvp.Key)) {
+ val = val.Replace (kvp.Key, kvp.Value);
+ }
+ }
+ if (val != attr.Value) {
+ attr.Value = val;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
new file mode 100644
index 00000000000..166223cc44f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestModel.cs
@@ -0,0 +1,105 @@
+#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/Generator/PropertyMapper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs
new file mode 100644
index 00000000000..32b07671600
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs
@@ -0,0 +1,200 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Defines the property mapping infrastructure for converting
+/// properties to Android manifest XML attributes.
+///
+static class PropertyMapper
+{
+ static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
+
+ internal enum MappingKind { String, Bool, Enum }
+
+ internal readonly struct PropertyMapping
+ {
+ public string PropertyName { get; }
+ public string XmlAttributeName { get; }
+ public MappingKind Kind { get; }
+ public Func? EnumConverter { get; }
+
+ public PropertyMapping (string propertyName, string xmlAttributeName, MappingKind kind = MappingKind.String, Func? enumConverter = null)
+ {
+ PropertyName = propertyName;
+ XmlAttributeName = xmlAttributeName;
+ Kind = kind;
+ EnumConverter = enumConverter;
+ }
+ }
+
+ internal static readonly PropertyMapping[] CommonMappings = [
+ new ("Label", "label"),
+ new ("Description", "description"),
+ new ("Icon", "icon"),
+ new ("RoundIcon", "roundIcon"),
+ new ("Permission", "permission"),
+ new ("Process", "process"),
+ new ("Enabled", "enabled", MappingKind.Bool),
+ new ("DirectBootAware", "directBootAware", MappingKind.Bool),
+ new ("Exported", "exported", MappingKind.Bool),
+ ];
+
+ internal static readonly PropertyMapping[] ActivityMappings = [
+ new ("Theme", "theme"),
+ new ("ParentActivity", "parentActivityName"),
+ new ("TaskAffinity", "taskAffinity"),
+ new ("AllowTaskReparenting", "allowTaskReparenting", MappingKind.Bool),
+ new ("AlwaysRetainTaskState", "alwaysRetainTaskState", MappingKind.Bool),
+ new ("ClearTaskOnLaunch", "clearTaskOnLaunch", MappingKind.Bool),
+ new ("ExcludeFromRecents", "excludeFromRecents", MappingKind.Bool),
+ new ("FinishOnCloseSystemDialogs", "finishOnCloseSystemDialogs", MappingKind.Bool),
+ new ("FinishOnTaskLaunch", "finishOnTaskLaunch", MappingKind.Bool),
+ new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool),
+ new ("NoHistory", "noHistory", MappingKind.Bool),
+ new ("MultiProcess", "multiprocess", MappingKind.Bool),
+ new ("StateNotNeeded", "stateNotNeeded", MappingKind.Bool),
+ new ("Immersive", "immersive", MappingKind.Bool),
+ new ("ResizeableActivity", "resizeableActivity", MappingKind.Bool),
+ new ("SupportsPictureInPicture", "supportsPictureInPicture", MappingKind.Bool),
+ new ("ShowForAllUsers", "showForAllUsers", MappingKind.Bool),
+ new ("TurnScreenOn", "turnScreenOn", MappingKind.Bool),
+ new ("LaunchMode", "launchMode", MappingKind.Enum, AndroidEnumConverter.LaunchModeToString),
+ new ("ScreenOrientation", "screenOrientation", MappingKind.Enum, AndroidEnumConverter.ScreenOrientationToString),
+ new ("ConfigurationChanges", "configChanges", MappingKind.Enum, AndroidEnumConverter.ConfigChangesToString),
+ new ("WindowSoftInputMode", "windowSoftInputMode", MappingKind.Enum, AndroidEnumConverter.SoftInputToString),
+ new ("DocumentLaunchMode", "documentLaunchMode", MappingKind.Enum, AndroidEnumConverter.DocumentLaunchModeToString),
+ new ("UiOptions", "uiOptions", MappingKind.Enum, AndroidEnumConverter.UiOptionsToString),
+ new ("PersistableMode", "persistableMode", MappingKind.Enum, AndroidEnumConverter.ActivityPersistableModeToString),
+ ];
+
+ internal static readonly PropertyMapping[] ServiceMappings = [
+ new ("IsolatedProcess", "isolatedProcess", MappingKind.Bool),
+ new ("ForegroundServiceType", "foregroundServiceType", MappingKind.Enum, AndroidEnumConverter.ForegroundServiceTypeToString),
+ ];
+
+ internal static readonly PropertyMapping[] ContentProviderMappings = [
+ new ("Authorities", "authorities"),
+ new ("GrantUriPermissions", "grantUriPermissions", MappingKind.Bool),
+ new ("Syncable", "syncable", MappingKind.Bool),
+ new ("MultiProcess", "multiprocess", MappingKind.Bool),
+ ];
+
+ internal static readonly PropertyMapping[] ApplicationElementMappings = [
+ new ("Label", "label"),
+ new ("Icon", "icon"),
+ new ("RoundIcon", "roundIcon"),
+ new ("Theme", "theme"),
+ new ("AllowBackup", "allowBackup", MappingKind.Bool),
+ new ("SupportsRtl", "supportsRtl", MappingKind.Bool),
+ new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool),
+ new ("LargeHeap", "largeHeap", MappingKind.Bool),
+ new ("Debuggable", "debuggable", MappingKind.Bool),
+ new ("UsesCleartextTraffic", "usesCleartextTraffic", MappingKind.Bool),
+ ];
+
+ internal static readonly PropertyMapping[] InstrumentationMappings = [
+ new ("Label", "label"),
+ new ("Icon", "icon"),
+ new ("TargetPackage", "targetPackage"),
+ new ("FunctionalTest", "functionalTest", MappingKind.Bool),
+ new ("HandleProfiling", "handleProfiling", MappingKind.Bool),
+ ];
+
+ internal static readonly PropertyMapping[] ApplicationPropertyMappings = [
+ new ("Label", "label"),
+ new ("Icon", "icon"),
+ new ("RoundIcon", "roundIcon"),
+ new ("Theme", "theme"),
+ new ("NetworkSecurityConfig", "networkSecurityConfig"),
+ new ("Description", "description"),
+ new ("Logo", "logo"),
+ new ("Permission", "permission"),
+ new ("Process", "process"),
+ new ("TaskAffinity", "taskAffinity"),
+ new ("AllowBackup", "allowBackup", MappingKind.Bool),
+ new ("SupportsRtl", "supportsRtl", MappingKind.Bool),
+ new ("HardwareAccelerated", "hardwareAccelerated", MappingKind.Bool),
+ new ("LargeHeap", "largeHeap", MappingKind.Bool),
+ new ("Debuggable", "debuggable", MappingKind.Bool),
+ new ("UsesCleartextTraffic", "usesCleartextTraffic", MappingKind.Bool),
+ new ("RestoreAnyVersion", "restoreAnyVersion", MappingKind.Bool),
+ ];
+
+ internal static void ApplyMappings (XElement element, IReadOnlyDictionary properties, PropertyMapping[] mappings, bool skipExisting = false)
+ {
+ foreach (var m in mappings) {
+ if (!properties.TryGetValue (m.PropertyName, out var value) || value is null) {
+ continue;
+ }
+ if (skipExisting && element.Attribute (AndroidNs + m.XmlAttributeName) is not null) {
+ continue;
+ }
+ switch (m.Kind) {
+ case MappingKind.String when value is string s && !string.IsNullOrEmpty (s):
+ element.SetAttributeValue (AndroidNs + m.XmlAttributeName, s);
+ break;
+ case MappingKind.Bool when value is bool b:
+ element.SetAttributeValue (AndroidNs + m.XmlAttributeName, b ? "true" : "false");
+ break;
+ case MappingKind.Enum when m.EnumConverter is not null:
+ int intValue = value switch { int i => i, long l => (int)l, short s => s, byte b => b, _ => 0 };
+ var strValue = m.EnumConverter (intValue);
+ if (strValue is not null) {
+ element.SetAttributeValue (AndroidNs + m.XmlAttributeName, strValue);
+ }
+ break;
+ }
+ }
+ }
+
+ internal static void MapComponentProperties (XElement element, ComponentInfo component)
+ {
+ ApplyMappings (element, component.Properties, CommonMappings);
+
+ var extra = component.Kind switch {
+ ComponentKind.Activity => ActivityMappings,
+ ComponentKind.Service => ServiceMappings,
+ ComponentKind.ContentProvider => ContentProviderMappings,
+ _ => null,
+ };
+ if (extra is not null) {
+ ApplyMappings (element, component.Properties, extra);
+ }
+
+ // Handle InitOrder for ContentProvider (int, not a standard mapping)
+ if (component.Kind == ComponentKind.ContentProvider && component.Properties.TryGetValue ("InitOrder", out var initOrder) && initOrder is int order) {
+ element.SetAttributeValue (AndroidNs + "initOrder", order.ToString (CultureInfo.InvariantCulture));
+ }
+ }
+
+ internal static void MapDictionaryProperties (XElement element, IReadOnlyDictionary props, string propertyName, string xmlAttrName)
+ {
+ if (props.TryGetValue (propertyName, out var value) && value is string s && !string.IsNullOrEmpty (s)) {
+ element.SetAttributeValue (AndroidNs + xmlAttrName, s);
+ }
+ }
+
+ internal static void MapDictionaryEnumProperty (XElement element, IReadOnlyDictionary props, string propertyName, string xmlAttrName, Func converter)
+ {
+ if (!props.TryGetValue (propertyName, out var value)) {
+ return;
+ }
+ int intValue = value switch {
+ int i => i,
+ long l => (int)l,
+ short s => s,
+ byte b => b,
+ _ => 0,
+ };
+ var strValue = converter (intValue);
+ if (strValue is not null) {
+ element.SetAttributeValue (AndroidNs + xmlAttrName, strValue);
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 9fceaeaa3ac..adb6a052f5e 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -117,6 +117,13 @@ public sealed record JavaPeerInfo
/// Generic types get TypeMap entries but CreateInstance throws NotSupportedException.
///
public bool IsGenericDefinition { get; init; }
+
+ ///
+ /// Component attribute information ([Activity], [Service], [BroadcastReceiver],
+ /// [ContentProvider], [Application], [Instrumentation]).
+ /// Null for types that are not Android components.
+ ///
+ public ComponentInfo? ComponentAttribute { get; init; }
}
///
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs
new file mode 100644
index 00000000000..b41fce30764
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs
@@ -0,0 +1,617 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Android.Sdk.TrimmableTypeMap;
+
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class ManifestGeneratorTests : IDisposable
+{
+ static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android";
+ static readonly XName AttName = AndroidNs + "name";
+
+ string tempDir;
+
+ public ManifestGeneratorTests ()
+ {
+ tempDir = Path.Combine (Path.GetTempPath (), "ManifestGeneratorTests_" + Guid.NewGuid ().ToString ("N"));
+ Directory.CreateDirectory (tempDir);
+ }
+
+ public void Dispose ()
+ {
+ if (Directory.Exists (tempDir)) {
+ Directory.Delete (tempDir, recursive: true);
+ }
+ }
+
+ ManifestGenerator CreateDefaultGenerator () => new ManifestGenerator {
+ PackageName = "com.example.app",
+ ApplicationLabel = "My App",
+ VersionCode = "1",
+ VersionName = "1.0",
+ MinSdkVersion = "21",
+ TargetSdkVersion = "36",
+ AndroidRuntime = "coreclr",
+ };
+
+ string OutputPath => Path.Combine (tempDir, "AndroidManifest.xml");
+
+ string WriteTemplate (string xml)
+ {
+ var path = Path.Combine (tempDir, "template.xml");
+ File.WriteAllText (path, xml);
+ return path;
+ }
+
+ static JavaPeerInfo CreatePeer (
+ string javaName,
+ ComponentInfo? component = null,
+ bool isAbstract = false,
+ string assemblyName = "TestApp")
+ {
+ return new JavaPeerInfo {
+ JavaName = javaName,
+ CompatJniName = javaName,
+ ManagedTypeName = javaName.Replace ('/', '.'),
+ ManagedTypeNamespace = javaName.Contains ('/') ? javaName.Substring (0, javaName.LastIndexOf ('/')).Replace ('/', '.') : "",
+ ManagedTypeShortName = javaName.Contains ('/') ? javaName.Substring (javaName.LastIndexOf ('/') + 1) : javaName,
+ AssemblyName = assemblyName,
+ IsAbstract = isAbstract,
+ ComponentAttribute = component,
+ };
+ }
+
+ XDocument GenerateAndLoad (
+ ManifestGenerator gen,
+ IReadOnlyList? peers = null,
+ AssemblyManifestInfo? assemblyInfo = null,
+ string? templatePath = null)
+ {
+ peers ??= [];
+ assemblyInfo ??= new AssemblyManifestInfo ();
+ gen.Generate (templatePath, peers, assemblyInfo, OutputPath);
+ return XDocument.Load (OutputPath);
+ }
+
+ [Fact]
+ public void Activity_MainLauncher ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MainActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ Properties = new Dictionary { ["MainLauncher"] = true },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var app = doc.Root?.Element ("application");
+ Assert.NotNull (app);
+ var activity = app?.Element ("activity");
+ Assert.NotNull (activity);
+
+ Assert.Equal ("com.example.app.MainActivity", (string?)activity?.Attribute (AttName));
+ Assert.Equal ("true", (string?)activity?.Attribute (AndroidNs + "exported"));
+
+ var filter = activity?.Element ("intent-filter");
+ Assert.NotNull (filter);
+ Assert.True (filter?.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.MAIN"));
+ Assert.True (filter?.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.LAUNCHER"));
+ }
+
+ [Fact]
+ public void Activity_WithProperties ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MyActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ Properties = new Dictionary {
+ ["Label"] = "My Activity",
+ ["Icon"] = "@drawable/icon",
+ ["Theme"] = "@style/MyTheme",
+ ["LaunchMode"] = 2, // singleTask
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var activity = doc.Root?.Element ("application")?.Element ("activity");
+
+ Assert.Equal ("My Activity", (string?)activity?.Attribute (AndroidNs + "label"));
+ Assert.Equal ("@drawable/icon", (string?)activity?.Attribute (AndroidNs + "icon"));
+ Assert.Equal ("@style/MyTheme", (string?)activity?.Attribute (AndroidNs + "theme"));
+ Assert.Equal ("singleTask", (string?)activity?.Attribute (AndroidNs + "launchMode"));
+ }
+
+ [Fact]
+ public void Activity_IntentFilter ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/ShareActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ IntentFilters = [
+ new IntentFilterInfo {
+ Actions = ["android.intent.action.SEND"],
+ Categories = ["android.intent.category.DEFAULT"],
+ Properties = new Dictionary {
+ ["DataMimeType"] = "text/plain",
+ },
+ },
+ ],
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var activity = doc.Root?.Element ("application")?.Element ("activity");
+
+ var filter = activity?.Element ("intent-filter");
+ Assert.NotNull (filter);
+ Assert.True (filter?.Elements ("action").Any (a => (string?)a.Attribute (AttName) == "android.intent.action.SEND"));
+ Assert.True (filter?.Elements ("category").Any (c => (string?)c.Attribute (AttName) == "android.intent.category.DEFAULT"));
+
+ var data = filter?.Element ("data");
+ Assert.NotNull (data);
+ Assert.Equal ("text/plain", (string?)data?.Attribute (AndroidNs + "mimeType"));
+ }
+
+ [Fact]
+ public void Activity_MetaData ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MetaActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ MetaData = [
+ new MetaDataInfo { Name = "com.example.key", Value = "my_value" },
+ new MetaDataInfo { Name = "com.example.res", Resource = "@xml/config" },
+ ],
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var activity = doc.Root?.Element ("application")?.Element ("activity");
+
+ var metaElements = activity?.Elements ("meta-data").ToList ();
+ Assert.Equal (2, metaElements?.Count);
+
+ var meta1 = metaElements?.FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.example.key");
+ Assert.NotNull (meta1);
+ Assert.Equal ("my_value", (string?)meta1?.Attribute (AndroidNs + "value"));
+
+ var meta2 = metaElements?.FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.example.res");
+ Assert.NotNull (meta2);
+ Assert.Equal ("@xml/config", (string?)meta2?.Attribute (AndroidNs + "resource"));
+ }
+
+ [Theory]
+ [InlineData (ComponentKind.Service, "service")]
+ [InlineData (ComponentKind.BroadcastReceiver, "receiver")]
+ public void Component_BasicProperties (ComponentKind kind, string elementName)
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MyComponent", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = kind,
+ Properties = new Dictionary {
+ ["Exported"] = true,
+ ["Label"] = "My Component",
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var element = doc.Root?.Element ("application")?.Element (elementName);
+ Assert.NotNull (element);
+
+ Assert.Equal ("com.example.app.MyComponent", (string?)element?.Attribute (AttName));
+ Assert.Equal ("true", (string?)element?.Attribute (AndroidNs + "exported"));
+ Assert.Equal ("My Component", (string?)element?.Attribute (AndroidNs + "label"));
+ }
+
+ [Fact]
+ public void ContentProvider_WithAuthorities ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MyProvider", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.ContentProvider,
+ Properties = new Dictionary {
+ ["Authorities"] = "com.example.app.provider",
+ ["Exported"] = false,
+ ["GrantUriPermissions"] = true,
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var provider = doc.Root?.Element ("application")?.Element ("provider");
+ Assert.NotNull (provider);
+
+ Assert.Equal ("com.example.app.MyProvider", (string?)provider?.Attribute (AttName));
+ Assert.Equal ("com.example.app.provider", (string?)provider?.Attribute (AndroidNs + "authorities"));
+ Assert.Equal ("false", (string?)provider?.Attribute (AndroidNs + "exported"));
+ Assert.Equal ("true", (string?)provider?.Attribute (AndroidNs + "grantUriPermissions"));
+ }
+
+ [Fact]
+ public void Application_TypeLevel ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MyApp", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Application,
+ Properties = new Dictionary {
+ ["Label"] = "Custom App",
+ ["AllowBackup"] = false,
+ ["LargeHeap"] = true,
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var app = doc.Root?.Element ("application");
+ Assert.NotNull (app);
+
+ Assert.Equal ("com.example.app.MyApp", (string?)app?.Attribute (AttName));
+ Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup"));
+ Assert.Equal ("true", (string?)app?.Attribute (AndroidNs + "largeHeap"));
+ }
+
+ [Fact]
+ public void Instrumentation_GoesToManifest ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Instrumentation,
+ Properties = new Dictionary {
+ ["Label"] = "My Test",
+ ["TargetPackage"] = "com.example.target",
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+
+ // Instrumentation should be under , not
+ var instrumentation = doc.Root?.Element ("instrumentation");
+ Assert.NotNull (instrumentation);
+
+ Assert.Equal ("com.example.app.MyInstrumentation", (string?)instrumentation?.Attribute (AttName));
+ Assert.Equal ("My Test", (string?)instrumentation?.Attribute (AndroidNs + "label"));
+ Assert.Equal ("com.example.target", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage"));
+
+ // Should NOT be inside
+ var appInstrumentation = doc.Root?.Element ("application")?.Element ("instrumentation");
+ Assert.Null (appInstrumentation);
+ }
+
+ [Fact]
+ public void RuntimeProvider_Added ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var doc = GenerateAndLoad (gen);
+ var app = doc.Root?.Element ("application");
+
+ var providers = app?.Elements ("provider").ToList ();
+ Assert.True (providers?.Count > 0);
+
+ var runtimeProvider = providers?.FirstOrDefault (p =>
+ ((string?)p.Attribute (AndroidNs + "name"))?.Contains ("MonoRuntimeProvider") == true);
+ Assert.NotNull (runtimeProvider);
+
+ var authorities = (string?)runtimeProvider?.Attribute (AndroidNs + "authorities");
+ Assert.True (authorities?.Contains ("com.example.app") == true, "authorities should contain package name");
+ Assert.True (authorities?.Contains ("__mono_init__") == true, "authorities should contain __mono_init__");
+ Assert.Equal ("false", (string?)runtimeProvider?.Attribute (AndroidNs + "exported"));
+ }
+
+ [Fact]
+ public void TemplateManifest_Preserved ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var template = WriteTemplate (
+ """
+
+
+
+
+
+ """);
+
+ var doc = GenerateAndLoad (gen, templatePath: template);
+ var app = doc.Root?.Element ("application");
+
+ Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup"));
+ Assert.Equal ("@mipmap/ic_launcher", (string?)app?.Attribute (AndroidNs + "icon"));
+ }
+
+ [Theory]
+ [InlineData ("", "", "1", "1.0")]
+ [InlineData ("42", "2.5", "42", "2.5")]
+ public void VersionDefaults (string versionCode, string versionName, string expectedCode, string expectedName)
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.VersionCode = versionCode;
+ gen.VersionName = versionName;
+
+ var doc = GenerateAndLoad (gen);
+ Assert.Equal (expectedCode, (string?)doc.Root?.Attribute (AndroidNs + "versionCode"));
+ Assert.Equal (expectedName, (string?)doc.Root?.Attribute (AndroidNs + "versionName"));
+ }
+
+ [Fact]
+ public void UsesSdk_Added ()
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.MinSdkVersion = "24";
+ gen.TargetSdkVersion = "34";
+
+ var doc = GenerateAndLoad (gen);
+ var usesSdk = doc.Root?.Element ("uses-sdk");
+ Assert.NotNull (usesSdk);
+
+ Assert.Equal ("24", (string?)usesSdk?.Attribute (AndroidNs + "minSdkVersion"));
+ Assert.Equal ("34", (string?)usesSdk?.Attribute (AndroidNs + "targetSdkVersion"));
+ }
+
+ [Theory]
+ [InlineData (true, false, false, "debuggable", "true")]
+ [InlineData (false, true, false, "debuggable", "true")]
+ [InlineData (false, false, true, "extractNativeLibs", "true")]
+ public void ApplicationFlags (bool debug, bool forceDebuggable, bool forceExtractNativeLibs, string attrName, string expected)
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.Debug = debug;
+ gen.ForceDebuggable = forceDebuggable;
+ gen.ForceExtractNativeLibs = forceExtractNativeLibs;
+
+ var doc = GenerateAndLoad (gen);
+ var app = doc.Root?.Element ("application");
+ Assert.Equal (expected, (string?)app?.Attribute (AndroidNs + attrName));
+ }
+
+ [Fact]
+ public void InternetPermission_WhenDebug ()
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.Debug = true;
+
+ var doc = GenerateAndLoad (gen);
+ var internetPerm = doc.Root?.Elements ("uses-permission")
+ .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "android.permission.INTERNET");
+ Assert.NotNull (internetPerm);
+ }
+
+ [Fact]
+ public void ManifestPlaceholders_Replaced ()
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.ManifestPlaceholders = "myAuthority=com.example.auth;myKey=12345";
+
+ var template = WriteTemplate (
+ """
+
+
+
+
+
+
+
+ """);
+
+ var doc = GenerateAndLoad (gen, templatePath: template);
+ var provider = doc.Root?.Element ("application")?.Elements ("provider")
+ .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "com.example.MyProvider");
+ Assert.Equal ("com.example.auth", (string?)provider?.Attribute (AndroidNs + "authorities"));
+
+ var meta = doc.Root?.Element ("application")?.Elements ("meta-data")
+ .FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "api_key");
+ Assert.Equal ("12345", (string?)meta?.Attribute (AndroidNs + "value"));
+ }
+
+ [Fact]
+ public void ApplicationJavaClass_Set ()
+ {
+ var gen = CreateDefaultGenerator ();
+ gen.ApplicationJavaClass = "com.example.app.CustomApplication";
+
+ var doc = GenerateAndLoad (gen);
+ var app = doc.Root?.Element ("application");
+ Assert.Equal ("com.example.app.CustomApplication", (string?)app?.Attribute (AttName));
+ }
+
+ [Fact]
+ public void AbstractTypes_Skipped ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var peer = CreatePeer ("com/example/app/AbstractActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ Properties = new Dictionary { ["Label"] = "Abstract" },
+ }, isAbstract: true);
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var activity = doc.Root?.Element ("application")?.Element ("activity");
+ Assert.Null (activity);
+ }
+
+ [Fact]
+ public void ExistingType_NotDuplicated ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var template = WriteTemplate (
+ """
+
+
+
+
+
+
+ """);
+
+ var peer = CreatePeer ("com/example/app/ExistingActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ Properties = new Dictionary { ["Label"] = "New Label" },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer], templatePath: template);
+ var activities = doc.Root?.Element ("application")?.Elements ("activity")
+ .Where (a => (string?)a.Attribute (AttName) == "com.example.app.ExistingActivity")
+ .ToList ();
+
+ Assert.Equal (1, activities?.Count);
+ // Original label preserved
+ Assert.Equal ("Existing", (string?)activities? [0].Attribute (AndroidNs + "label"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_UsesPermission ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo ();
+ info.UsesPermissions.Add (new UsesPermissionInfo { Name = "android.permission.CAMERA" });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var perm = doc.Root?.Elements ("uses-permission")
+ .FirstOrDefault (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA");
+ Assert.NotNull (perm);
+ }
+
+ [Fact]
+ public void AssemblyLevel_UsesFeature ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo ();
+ info.UsesFeatures.Add (new UsesFeatureInfo { Name = "android.hardware.camera", Required = false });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var feature = doc.Root?.Elements ("uses-feature")
+ .FirstOrDefault (f => (string?)f.Attribute (AttName) == "android.hardware.camera");
+ Assert.NotNull (feature);
+ Assert.Equal ("false", (string?)feature?.Attribute (AndroidNs + "required"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_UsesLibrary ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo ();
+ info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy", Required = false });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var lib = doc.Root?.Element ("application")?.Elements ("uses-library")
+ .FirstOrDefault (l => (string?)l.Attribute (AttName) == "org.apache.http.legacy");
+ Assert.NotNull (lib);
+ Assert.Equal ("false", (string?)lib?.Attribute (AndroidNs + "required"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_Permission ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo ();
+ info.Permissions.Add (new PermissionInfo {
+ Name = "com.example.MY_PERMISSION",
+ Properties = new Dictionary {
+ ["Label"] = "My Permission",
+ ["Description"] = "A custom permission",
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var perm = doc.Root?.Elements ("permission")
+ .FirstOrDefault (p => (string?)p.Attribute (AttName) == "com.example.MY_PERMISSION");
+ Assert.NotNull (perm);
+ Assert.Equal ("My Permission", (string?)perm?.Attribute (AndroidNs + "label"));
+ Assert.Equal ("A custom permission", (string?)perm?.Attribute (AndroidNs + "description"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_MetaData ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo ();
+ info.MetaData.Add (new MetaDataInfo { Name = "com.google.android.gms.version", Value = "12345" });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var meta = doc.Root?.Element ("application")?.Elements ("meta-data")
+ .FirstOrDefault (m => (string?)m.Attribute (AndroidNs + "name") == "com.google.android.gms.version");
+ Assert.NotNull (meta);
+ Assert.Equal ("12345", (string?)meta?.Attribute (AndroidNs + "value"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_Application ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var info = new AssemblyManifestInfo {
+ ApplicationProperties = new Dictionary {
+ ["Theme"] = "@style/AppTheme",
+ ["SupportsRtl"] = true,
+ },
+ };
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info);
+ var app = doc.Root?.Element ("application");
+ Assert.Equal ("@style/AppTheme", (string?)app?.Attribute (AndroidNs + "theme"));
+ Assert.Equal ("true", (string?)app?.Attribute (AndroidNs + "supportsRtl"));
+ }
+
+ [Fact]
+ public void AssemblyLevel_Deduplication ()
+ {
+ var gen = CreateDefaultGenerator ();
+ var template = WriteTemplate (
+ """
+
+
+
+
+
+
+
+
+ """);
+
+ var info = new AssemblyManifestInfo ();
+ info.UsesPermissions.Add (new UsesPermissionInfo { Name = "android.permission.CAMERA" });
+ info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy" });
+ info.MetaData.Add (new MetaDataInfo { Name = "existing.key", Value = "new_value" });
+
+ var doc = GenerateAndLoad (gen, assemblyInfo: info, templatePath: template);
+
+ var cameraPerms = doc.Root?.Elements ("uses-permission")
+ .Where (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA")
+ .ToList ();
+ Assert.Equal (1, cameraPerms?.Count);
+
+ var libs = doc.Root?.Element ("application")?.Elements ("uses-library")
+ .Where (l => (string?)l.Attribute (AttName) == "org.apache.http.legacy")
+ .ToList ();
+ Assert.Equal (1, libs?.Count);
+
+ var metas = doc.Root?.Element ("application")?.Elements ("meta-data")
+ .Where (m => (string?)m.Attribute (AndroidNs + "name") == "existing.key")
+ .ToList ();
+ Assert.Equal (1, metas?.Count);
+ }
+
+ [Fact]
+ public void ConfigChanges_EnumConversion ()
+ {
+ var gen = CreateDefaultGenerator ();
+ // orientation (0x0080) | keyboardHidden (0x0020) | screenSize (0x0400)
+ int configChanges = 0x0080 | 0x0020 | 0x0400;
+ var peer = CreatePeer ("com/example/app/ConfigActivity", new ComponentInfo { HasPublicDefaultConstructor = true,
+ Kind = ComponentKind.Activity,
+ Properties = new Dictionary {
+ ["ConfigurationChanges"] = configChanges,
+ },
+ });
+
+ var doc = GenerateAndLoad (gen, [peer]);
+ var activity = doc.Root?.Element ("application")?.Element ("activity");
+
+ var configValue = (string?)activity?.Attribute (AndroidNs + "configChanges");
+
+ // The value should be pipe-separated and contain all three flags
+ var parts = configValue?.Split ('|') ?? [];
+ Assert.True (parts.Contains ("orientation"), "configChanges should contain 'orientation'");
+ Assert.True (parts.Contains ("keyboardHidden"), "configChanges should contain 'keyboardHidden'");
+ Assert.True (parts.Contains ("screenSize"), "configChanges should contain 'screenSize'");
+ Assert.Equal (3, parts.Length);
+ }
+}