Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ static class AndroidEnumConverter
_ => null,
};

public static string? RotationAnimationToString (int value) => value switch {
0 => "rotate",
1 => "crossfade",
2 => "jumpcut",
3 => "seamless",
_ => null,
};
Comment on lines +20 to +26

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I think the only issue with these, are that new Android API levels could introduce new values.

Is there a way to be based off Mono.Android.dll? Or do ToString().ToLowerInvariant()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far I've been trying to avoid the dependency of the typemap generator on Mono.Android. I will need to think this through. Please feel free to open an issue.


public static string? ScreenOrientationToString (int value, int targetSdkVersion = 0) => value switch {
-1 => "unspecified",
0 => "landscape",
Expand Down Expand Up @@ -126,8 +134,8 @@ static class AndroidEnumConverter

public static string? ActivityPersistableModeToString (int value) => value switch {
0 => "persistRootOnly",
1 => "persistAcrossReboots",
2 => "persistNever",
1 => "persistNever",
2 => "persistAcrossReboots",
_ => null,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ static class ComponentElementBuilder
static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
static readonly XName AttName = ManifestConstants.AttName;

internal static XElement? CreateComponentElement (JavaPeerInfo peer, string jniName, int targetSdkVersion = 0)
internal static XElement? CreateComponentElement (JavaPeerInfo peer, string jniName, int targetSdkVersion = 0, IReadOnlyDictionary<string, string>? managedToManifestNames = null)
{
var component = peer.ComponentAttribute;
if (component is null) {
Expand All @@ -34,11 +34,24 @@ static class ComponentElementBuilder
// Map known properties to android: attributes
PropertyMapper.MapComponentProperties (element, component, targetSdkVersion);

// android:parentActivityName comes from a [Activity (ParentActivity = typeof (...))] and is
// captured as the managed type name. Resolve it to the parent's Java/manifest name, matching
// the legacy ManifestDocument behavior (JavaNativeTypeManager.ToJniName).
ResolveParentActivityName (element, managedToManifestNames);

// Add intent filters
foreach (var intentFilter in component.IntentFilters) {
element.Add (CreateIntentFilterElement (intentFilter));
}

// Add <layout> element from a [Layout] attribute, if present
if (component.LayoutProperties is not null) {
var layout = CreateLayoutElement (component.LayoutProperties);
if (layout is not null) {
element.Add (layout);
}
}

// Handle MainLauncher for activities
if (component.Kind == ComponentKind.Activity && component.Properties.TryGetValue ("MainLauncher", out var ml) && ml is bool b && b) {
AddLauncherIntentFilter (element);
Expand All @@ -49,9 +62,54 @@ static class ComponentElementBuilder
element.Add (CreateMetaDataElement (meta));
}

// The legacy ManifestDocumentElement.ToElement sorts attributes alphabetically
// (specified.OrderBy (e => e)). Match that ordering so the generated manifest is
// byte-compatible with the legacy path when AndroidManifestMerger='legacy' (the
// manifestmerger.jar path re-sorts attributes itself, so this is also safe there).
SortAttributesAlphabetically (element);

return element;
}

// Reorders an element's attributes alphabetically by local name (case-insensitive),
// matching the legacy manifest generator's attribute ordering.
static void SortAttributesAlphabetically (XElement element)
{
var sorted = element.Attributes ()
.OrderBy (a => a.Name.LocalName, StringComparer.OrdinalIgnoreCase)
.ToList ();
if (sorted.Count < 2) {
return;
}
foreach (var attr in element.Attributes ().ToList ()) {
attr.Remove ();
}
foreach (var attr in sorted) {
element.Add (attr);
}
}

static void ResolveParentActivityName (XElement element, IReadOnlyDictionary<string, string>? managedToManifestNames)
{
if (managedToManifestNames is null) {
return;
}

var attr = element.Attribute (AndroidNs + "parentActivityName");
if (attr is null) {
return;
}

// The value may be assembly-qualified ("Foo.Bar, Asm [Version=...]"); use the type name part.
var value = attr.Value;
int comma = value.IndexOf (',');
var typeName = (comma < 0 ? value : value.Substring (0, comma)).Trim ();

if (managedToManifestNames.TryGetValue (typeName, out var manifestName)) {
attr.Value = manifestName;
}
}

internal static void AddLauncherIntentFilter (XElement activity)
{
// Check if there's already a launcher intent filter
Expand Down Expand Up @@ -154,6 +212,30 @@ internal static XElement CreateMetaDataElement (MetaDataInfo meta)
return element;
}

// Maps [Layout] attribute properties to the <layout> element's android: attributes.
static readonly (string Property, string Attribute) [] LayoutMappings = [
("DefaultHeight", "defaultHeight"),
("DefaultWidth", "defaultWidth"),
("Gravity", "gravity"),
("MinHeight", "minHeight"),
("MinWidth", "minWidth"),
];

internal static XElement? CreateLayoutElement (IReadOnlyDictionary<string, object?> layoutProperties)
{
var element = new XElement ("layout");
bool hasAttribute = false;

foreach (var (property, attribute) in LayoutMappings) {
if (layoutProperties.TryGetValue (property, out var value) && value is string s && !s.IsNullOrEmpty ()) {
element.SetAttributeValue (AndroidNs + attribute, s);
hasAttribute = true;
}
}
Comment thread
Copilot marked this conversation as resolved.

return hasAttribute ? element : null;
}

internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer, int targetSdkVersion = 0)
{
string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

Expand Down Expand Up @@ -41,6 +42,27 @@ class ManifestGenerator
public string? ManifestPlaceholders { get; set; }
public string? ApplicationJavaClass { get; set; }
public Action<string>? Warn { get; set; }
public Action<string>? WarnInvalidPlaceholder { get; set; }

/// <summary>
/// Absolute paths to extracted library (.aar) manifests that must be merged into the
/// application manifest. Only used by the legacy manifest merger; manifestmerger.jar handles
/// this downstream in the _ManifestMerger target. Mirrors the legacy ManifestDocument merge.
/// </summary>
public IReadOnlyList<string> LibraryManifests { get; set; } = [];

static readonly XNamespace ToolsNs = "http://schemas.android.com/tools";

// Attributes whose values are component names that must be qualified with the library's
// own package when they are relative (start with '.'). Mirrors ManifestDocument.ManifestAttributeFixups.
static readonly Dictionary<string, string []> ManifestAttributeFixups = new (StringComparer.Ordinal) {
{ "activity", ["name"] },
{ "application", ["backupAgent"] },
{ "instrumentation", ["name"] },
{ "provider", ["name"] },
{ "receiver", ["name"] },
{ "service", ["name"] },
};

/// <summary>
/// Generates the merged manifest from an optional pre-loaded template and writes it to <paramref name="outputPath"/>.
Expand Down Expand Up @@ -77,6 +99,15 @@ class ManifestGenerator
.OfType<string> (),
StringComparer.Ordinal);

// Map managed type names to their Java/manifest names so component properties that reference
// other types (e.g. [Activity (ParentActivity = typeof (...))]) can be resolved.
var managedToManifestNames = new Dictionary<string, string> (allPeers.Count, StringComparer.Ordinal);
foreach (var peer in allPeers) {
if (!string.IsNullOrEmpty (peer.ManagedTypeName)) {
managedToManifestNames [peer.ManagedTypeName] = JniSignatureHelper.JniNameToJavaName (peer.JavaName);
}
}

// Add components from scanned types
foreach (var peer in allPeers) {
if (peer.IsAbstract || peer.ComponentAttribute is null) {
Expand All @@ -99,7 +130,7 @@ class ManifestGenerator
continue;
}

var element = ComponentElementBuilder.CreateComponentElement (peer, jniName, targetSdkVersionValue);
var element = ComponentElementBuilder.CreateComponentElement (peer, jniName, targetSdkVersionValue, managedToManifestNames);
if (element is not null) {
app.Add (element);
}
Expand Down Expand Up @@ -133,12 +164,114 @@ class ManifestGenerator
AssemblyLevelElementBuilder.AddInternetPermission (manifest);
}

// Merge extracted library (.aar) manifests (legacy merger only — manifestmerger.jar does
// this downstream). Mirrors ManifestDocument: merge, then dedup, then strip node="remove",
// all before placeholder substitution so ${applicationId} resolves in merged content.
MergeLibraryManifests (manifest);
RemoveDuplicateElements (doc);
RemoveNodes (doc);

// Apply manifest placeholders
ApplyPlaceholders (doc, ManifestPlaceholders);
ApplyPlaceholders (doc, ManifestPlaceholders, PackageName, WarnInvalidPlaceholder);

return (doc, providerNames);
}

/// <summary>
/// Merges each library manifest's top-level elements into the application manifest, mirroring
/// ManifestDocument.MergeLibraryManifest: elements with a matching android:name append their
/// children to the existing element, otherwise the element is added; relative component names
/// are qualified with the library's own package.
/// </summary>
void MergeLibraryManifests (XElement manifest)
{
foreach (var path in LibraryManifests) {
if (path.IsNullOrEmpty () || !File.Exists (path)) {
continue;
}

XDocument libDoc;
try {
libDoc = XDocument.Load (path);
} catch (Exception ex) {
Warn?.Invoke ($"Unable to merge library manifest '{path}': {ex.Message}");
continue;
}

if (libDoc.Root is not { } libRoot) {
continue;
}

var package = (string?) libRoot.Attribute ("package") ?? "";
foreach (var top in libRoot.Elements ().ToList ()) {
var name = (string?) top.Attribute (AndroidNs + "name");
XElement? existing = name is not null
? manifest.Elements (top.Name).FirstOrDefault (e => (string?) e.Attribute (AndroidNs + "name") == name)
: manifest.Elements (top.Name).FirstOrDefault ();

if (existing is not null) {
// Append the library element's children to the matching element.
existing.Add (FixupNameElements (package, top.Nodes ()));
} else {
manifest.Add (FixupNameElements (package, [top]));
}
}
}
}

/// <summary>
/// Qualifies relative component names (those starting with '.') with the supplied package,
/// mirroring ManifestDocument.FixupNameElements.
/// </summary>
static IEnumerable<XNode> FixupNameElements (string packageName, IEnumerable<XNode> nodes)
{
var nodeList = nodes.ToList ();
foreach (var element in nodeList.OfType<XElement> ().Where (x => ManifestAttributeFixups.ContainsKey (x.Name.LocalName))) {
var attributes = ManifestAttributeFixups [element.Name.LocalName];
foreach (var attr in element.Attributes ().Where (x => attributes.Contains (x.Name.LocalName))) {
if (attr.Value.StartsWith (".", StringComparison.Ordinal)) {
attr.Value = packageName + attr.Value;
}
}
}
return nodeList;
}

/// <summary>
/// Removes structurally-identical duplicate elements, mirroring ManifestDocument.RemoveDuplicateElements.
/// </summary>
static void RemoveDuplicateElements (XDocument doc)
{
foreach (var duplicate in ResolveDuplicates (doc.Elements ()).ToList ()) {
duplicate.Remove ();
}
}

static IEnumerable<XElement> ResolveDuplicates (IEnumerable<XElement> elements)
{
var elementList = elements.ToList ();
foreach (var e in elementList) {
foreach (var d in ResolveDuplicates (e.Elements ())) {
yield return d;
}
}
foreach (var d in elementList.GroupBy (x => x.ToString (SaveOptions.DisableFormatting)).SelectMany (x => x.Skip (1))) {
yield return d;
}
}

/// <summary>
/// Removes elements marked with tools:node="remove", mirroring ManifestDocument.RemoveNodes.
/// </summary>
static void RemoveNodes (XDocument doc)
{
foreach (var node in doc.Descendants ().ToList ()) {
if (node.Attribute (ToolsNs + "node")?.Value == "remove") {
node.Remove ();
}
}
}

XDocument CreateDefaultManifest ()
{
return new XDocument (
Expand Down Expand Up @@ -204,7 +337,13 @@ void EnsureManifestAttributes (XElement manifest)
{
manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName);

if (string.IsNullOrEmpty ((string?)manifest.Attribute ("package"))) {
// Resolve the package: when the template uses a placeholder token such as "${PACKAGENAME}"
// (or has no package), write the resolved value provided by MSBuild ($(_AndroidPackage),
// produced by GetAndroidPackageName, which substitutes placeholders and canonicalizes the
// package). This matches the legacy GenerateMainAndroidManifest; a valid explicit package is
// preserved so compat-name resolution keeps using it.
var packageAttr = (string?) manifest.Attribute ("package") ?? "";
if ((packageAttr.Length == 0 || packageAttr.Contains ("${")) && !PackageName.IsNullOrEmpty ()) {
manifest.SetAttributeValue ("package", PackageName);
}

Expand Down Expand Up @@ -368,22 +507,34 @@ XElement CreateRuntimeProvider (string name, string? processName, int initOrder,
/// Replaces ${key} placeholders in all attribute values throughout the document.
/// Placeholder format: "key1=value1;key2=value2"
/// </summary>
internal static void ApplyPlaceholders (XDocument doc, string? placeholders)
internal static void ApplyPlaceholders (XDocument doc, string? placeholders, string? packageName = null, Action<string>? warnInvalidPlaceholder = null)
{
if (placeholders.IsNullOrEmpty ()) {
return;
}

var replacements = new Dictionary<string, string> (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 (!placeholders.IsNullOrEmpty ()) {
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;
} else if (eqIndex < 0) {
// An entry without '=' is not a valid key=value pair. Mirror the legacy
// ManifestDocument.ReplacePlaceholders behavior and warn (XA1010).
warnInvalidPlaceholder?.Invoke (placeholders);
}
}
}

// ${applicationId} is a built-in placeholder that always resolves to the application
// package name (mirrors ManifestDocument.Save, which does
// s.Replace ("${applicationId}", PackageName) before applying the user placeholders).
// It is set last so it wins over any user-supplied "applicationId" entry, matching the
// legacy ordering, and is what substitutes merged library-manifest values such as
// "${applicationId}.permission.C2D_MESSAGE".
if (!packageName.IsNullOrEmpty ()) {
replacements ["${applicationId}"] = packageName;
}

if (replacements.Count == 0) {
return;
}
Expand Down
Loading
Loading