Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ static class ComponentElementBuilder
element.Add (CreateIntentFilterElement (intentFilter));
}

// Add <layout> element from a [Layout] attribute, if present
if (component.LayoutProperties is not null) {
if (component.Kind == ComponentKind.Activity && component.LayoutProperties is not null) {
var layout = CreateLayoutElement (component.LayoutProperties);
if (layout is not null) {
element.Add (layout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ internal static void ApplyPlaceholders (XDocument doc, string? placeholders, str
if (!placeholders.IsNullOrEmpty ()) {
foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) {
var eqIndex = entry.IndexOf ('=');
if (eqIndex > 0) {
if (eqIndex >= 0) {
Comment thread
simonrozsival marked this conversation as resolved.
var key = entry.Substring (0, eqIndex).Trim ();
var value = entry.Substring (eqIndex + 1).Trim ();
replacements ["${" + key + "}"] = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
string jniName = kvp.Key;
var peersForName = kvp.Value;

// Sort aliases by managed type name for deterministic proxy naming
if (peersForName.Count > 1) {
peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
peersForName.Sort (CompareAliasesForRuntimeResolution);
}

EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
Expand Down Expand Up @@ -141,6 +140,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
return model;
}

static int CompareAliasesForRuntimeResolution (JavaPeerInfo a, JavaPeerInfo b)
{
// Keep alias [0] aligned with the native java→managed map, which processes Mono.Android first.
bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal);
bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal);
if (aMonoAndroid != bMonoAndroid) {
return aMonoAndroid ? -1 : 1;
}

int result = StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName);
return result != 0 ? result : StringComparer.Ordinal.Compare (a.AssemblyName, b.AssemblyName);
}

static void EmitPeers (TypeMapAssemblyData model, string jniName,
List<JavaPeerInfo> peersForName, string assemblyName, HashSet<string> usedProxyNames)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

Expand Down Expand Up @@ -107,12 +109,28 @@ public void WritePE (Stream stream)
new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
new MetadataRootBuilder (Metadata),
ILBuilder,
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null);
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null,
// ManagedPEBuilder otherwise uses a time-based id, changing bytes on every regeneration.
deterministicIdProvider: DeterministicContentId);
Comment thread
simonrozsival marked this conversation as resolved.
var peBlob = new BlobBuilder ();
peBuilder.Serialize (peBlob);
peBlob.WriteContentTo (stream);
}

static BlobContentId DeterministicContentId (IEnumerable<Blob> content)
{
using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256);
foreach (var blob in content) {
var segment = blob.GetBytes ();
if (segment.Count == 0) {
continue;
}
Debug.Assert (segment.Array is not null);
hash.AppendData (segment.Array, segment.Offset, segment.Count);
}
return BlobContentId.FromHash (hash.GetHashAndReset ());
}

/// <summary>
/// Adds (or retrieves from cache) an assembly reference.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface ITrimmableTypeMapLogger
void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName);
void LogManifestReferencedTypeNotFoundWarning (string javaTypeName);
void LogLibraryManifestMergeWarning (string message);
void LogInvalidManifestPlaceholderWarning (string placeholders);
void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
// with the wrong AttributeName.
List<IntentFilterInfo>? intentFilters = null;
List<MetaDataInfo>? metaData = null;
Dictionary<string, object?>? layoutProperties = null;

foreach (var caHandle in typeDef.GetCustomAttributes ()) {
var ca = Reader.GetCustomAttribute (caHandle);
Expand Down Expand Up @@ -214,6 +215,8 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
metaData ??= new List<MetaDataInfo> ();
var (mdName, mdProps) = ParseNameAndProperties (ca);
metaData.Add (CreateMetaDataInfo (mdName, mdProps));
} else if (attrName == "LayoutAttribute") {
layoutProperties = ParseLayoutAttribute (ca);
} else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) {
// Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName])
var name = TryGetNameProperty (ca);
Expand All @@ -232,6 +235,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
if (metaData is not null) {
attrInfo.MetaData.AddRange (metaData);
}
if (layoutProperties is not null) {
attrInfo.LayoutProperties = layoutProperties;
}
}

return (registerInfo, attrInfo);
Expand Down Expand Up @@ -425,6 +431,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue<string> value)
return null;
}

Dictionary<string, object?> ParseLayoutAttribute (CustomAttribute ca)
{
var value = DecodeAttribute (ca);
var properties = new Dictionary<string, object?> (StringComparer.Ordinal);
foreach (var named in value.NamedArguments) {
if (named.Name is not null) {
properties [named.Name] = named.Value;
}
}
return properties;
}

IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca)
{
var value = DecodeAttribute (ca);
Expand Down Expand Up @@ -712,6 +730,12 @@ class TypeAttributeInfo (string attributeName)
/// Metadata entries declared on this type via [MetaData] attributes.
/// </summary>
public List<MetaDataInfo> MetaData { get; } = [];

/// <summary>
/// Named property values from a [Layout] attribute on this type, or null if none.
/// Maps to the &lt;layout&gt; child element of the component in the manifest.
/// </summary>
public Dictionary<string, object?>? LayoutProperties { get; set; }
}

sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string
}
}

// ManagedPeer depends on reflection-based registration; the trimmable path uses IAndroidCallableWrapper.
static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) =>
Comment thread
simonrozsival marked this conversation as resolved.
managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop";

void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results)
{
foreach (var typeHandle in index.Reader.TypeDefinitions) {
Expand All @@ -286,6 +290,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A

var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader);

if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) {
continue;
}

// Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate
// which scenarios fail later in the trimmable typemap pipeline.
// if (index.MayUseJniAddNativeMethodRegistrationAttribute &&
Expand Down Expand Up @@ -2528,6 +2536,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<J
Properties = attrInfo.Properties,
IntentFilters = attrInfo.IntentFilters,
MetaData = attrInfo.MetaData,
LayoutProperties = attrInfo.LayoutProperties,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
// Other codes (e.g. unresolvable type properties) are not yet assigned XA codes
// and are intentionally not surfaced here.
},
WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders),
LibraryManifests = config.LibraryManifests ?? [],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName);
public void LogLibraryManifestMergeWarning (string message) =>
log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message);
public void LogInvalidManifestPlaceholderWarning (string placeholders) =>
log.LogCodedWarning ("XA1010", Properties.Resources.XA1010, placeholders);
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ public void Placeholders_AllValid_DoesNotWarn ()
Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label"));
}

[Fact]
public void Placeholders_EmptyKey_ReplacesEmptyTokenWithoutWarning ()
{
var gen = CreateDefaultGenerator ();
var warnings = new List<string> ();
gen.WarnInvalidPlaceholder = warnings.Add;
gen.ManifestPlaceholders = "=val1";
var template = ParseTemplate ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
<uses-sdk />
<application android:label="${}" />
</manifest>
""");
var doc = GenerateAndLoad (gen, template: template);
Assert.Empty (warnings);
Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label"));
}

[Fact]
public void Package_PlaceholderToken_ReplacedWithResolvedPackageName ()
{
Expand Down Expand Up @@ -385,6 +404,24 @@ public void Activity_LayoutAttributeElement ()
Assert.Equal ("400dp", (string?)layout?.Attribute (AndroidNs + "minHeight"));
}

[Fact]
public void Service_LayoutAttributeElement_Ignored ()
{
var gen = CreateDefaultGenerator ();
var peer = CreatePeer ("com/example/app/MyService", new ComponentInfo {
Kind = ComponentKind.Service,
LayoutProperties = new Dictionary<string, object?> {
["DefaultWidth"] = "500dp",
},
});

var doc = GenerateAndLoad (gen, [peer]);
var service = doc.Root?.Element ("application")?.Element ("service");

Assert.NotNull (service);
Assert.Null (service?.Element ("layout"));
}

[Fact]
public void Activity_AllExtendedProperties ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type.");
public void LogLibraryManifestMergeWarning (string message) =>
warnings?.Add (message);
public void LogInvalidManifestPlaceholderWarning (string placeholders) =>
warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'.");
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,23 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries ()
Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count);
}

[Fact]
public void Build_AliasGroup_MonoAndroidPeerSortsFirst ()
{
// Java.Interop first proves Mono.Android still becomes alias [0], matching runtime lookup.
var peers = new List<JavaPeerInfo> {
MakeMcwPeer ("java/lang/Object", "Java.Interop.JavaObject", "Java.Interop") with { IsFromJniTypeSignature = true },
MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
};

var model = BuildModel (peers, "MonoAndroid");

Assert.Equal ("java/lang/Object[0]", model.Entries [0].MapKey);
Assert.Contains ("Java.Lang.Object", model.Entries [0].ProxyTypeReference);
Assert.Equal ("java/lang/Object[1]", model.Entries [1].MapKey);
Assert.Contains ("Java.Interop.JavaObject", model.Entries [1].ProxyTypeReference);
}

[Fact]
public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivation ()
{
Expand Down
Loading