Skip to content
Draft
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 @@ -69,8 +69,10 @@ public sealed record JavaPeerInfo
/// Types with component attributes ([Activity], [Service], etc.),
/// custom views from layout XML, or manifest-declared components
/// are unconditionally preserved (not trimmable).
/// May be set after scanning when the manifest references a type
/// that the scanner did not mark as unconditional.
/// </summary>
public bool IsUnconditional { get; init; }
public bool IsUnconditional { get; set; }

/// <summary>
/// True for Application and Instrumentation types. These types cannot call
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

Expand All @@ -12,13 +13,15 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
public class TrimmableTypeMapGenerator
{
readonly Action<string> log;
readonly Action<string>? warn;

public TrimmableTypeMapGenerator (Action<string> log)
public TrimmableTypeMapGenerator (Action<string> log, Action<string>? warn = null)
{
if (log is null) {
throw new ArgumentNullException (nameof (log));
}
this.log = log;
this.warn = warn;
}

/// <summary>
Expand Down Expand Up @@ -62,6 +65,8 @@ public TrimmableTypeMapResult Execute (
return new TrimmableTypeMapResult ([], [], allPeers);
}

RootManifestReferencedTypes (allPeers, manifestTemplatePath);

var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory);

// Generate JCW .java files for user assemblies + framework Implementor types.
Expand Down Expand Up @@ -220,4 +225,74 @@ List<string> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers, string javaSou
log ($"Generated {files.Count} JCW Java source files.");
return files.ToList ();
}

void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, string? manifestTemplatePath)
{
if (manifestTemplatePath.IsNullOrEmpty () || !File.Exists (manifestTemplatePath)) {
return;
}

XDocument doc;
try {
doc = XDocument.Load (manifestTemplatePath);
} catch (Exception ex) {
warn?.Invoke ($"Failed to parse ManifestTemplate '{manifestTemplatePath}': {ex.Message}");
return;
}

RootManifestReferencedTypes (allPeers, doc);
}

internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocument doc)
{
var root = doc.Root;
if (root is null) {
return;
}

XNamespace androidNs = "http://schemas.android.com/apk/res/android";
XName attName = androidNs + "name";

var componentNames = new HashSet<string> (StringComparer.Ordinal);
foreach (var element in root.Descendants ()) {
switch (element.Name.LocalName) {
case "activity":
case "service":
case "receiver":
case "provider":
var name = (string?) element.Attribute (attName);
if (name is not null) {
componentNames.Add (name);
}
break;
}
}

if (componentNames.Count == 0) {
return;
}

var peersByDotName = new Dictionary<string, List<JavaPeerInfo>> (StringComparer.Ordinal);
foreach (var peer in allPeers) {
var dotName = peer.JavaName.Replace ('/', '.').Replace ('$', '.');
if (!peersByDotName.TryGetValue (dotName, out var list)) {
list = [];
peersByDotName [dotName] = list;
}
list.Add (peer);
}
Comment on lines +275 to +283
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

RootManifestReferencedTypes builds its lookup key by converting peer.JavaName with .Replace ('$', '.'), but Android manifest class names for nested types use $ (e.g., com.example.Outer$Inner), so those will never match and won't be rooted. Also, manifest android:name commonly supports relative names (e.g., .MyActivity or MyActivity), which won't match the current dictionary keys either. Normalize android:name values (resolve relative names using the manifest package attribute) and avoid converting $ to . for manifest matching.

Copilot uses AI. Check for mistakes.

foreach (var name in componentNames) {
if (peersByDotName.TryGetValue (name, out var peers)) {
foreach (var peer in peers) {
if (!peer.IsUnconditional) {
peer.IsUnconditional = true;
log ($"Rooting manifest-referenced type '{name}' ({peer.ManagedTypeName}) as unconditional.");
}
}
} else {
warn?.Invoke ($"Manifest-referenced type '{name}' was not found in any scanned assembly. It may be a framework type.");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ public override bool RunTask ()
ApplicationJavaClass: ApplicationJavaClass);
}

var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg));
var generator = new TrimmableTypeMapGenerator (
msg => Log.LogMessage (MessageImportance.Low, msg),
msg => Log.LogWarning (msg));
var result = generator.Execute (
assemblyPaths,
OutputDirectory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#nullable enable

using System;
using System.Collections.Generic;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,121 @@ TrimmableTypeMapGenerator CreateGenerator ()
return new TrimmableTypeMapGenerator (msg => logMessages.Add (msg));
}

TrimmableTypeMapGenerator CreateGenerator (List<string> warnings)
{
return new TrimmableTypeMapGenerator (msg => logMessages.Add (msg), msg => warnings.Add (msg));
}

[Fact]
public void RootManifestReferencedTypes_RootsMatchingPeers ()
{
var peers = new List<JavaPeerInfo> {
new JavaPeerInfo {
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
AssemblyName = "MyApp", IsUnconditional = false,
},
new JavaPeerInfo {
JavaName = "com/example/MyService", CompatJniName = "com.example.MyService",
ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService",
AssemblyName = "MyApp", IsUnconditional = false,
},
};

var doc = System.Xml.Linq.XDocument.Parse ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
<application>
<activity android:name="com.example.MyActivity" />
</application>
</manifest>
""");

var generator = CreateGenerator ();
generator.RootManifestReferencedTypes (peers, doc);

Assert.True (peers [0].IsUnconditional, "MyActivity should be rooted as unconditional.");
Assert.False (peers [1].IsUnconditional, "MyService should remain conditional.");
Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type"));
}

[Fact]
public void RootManifestReferencedTypes_WarnsForUnresolvedTypes ()
{
var peers = new List<JavaPeerInfo> {
new JavaPeerInfo {
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
AssemblyName = "MyApp",
},
};

var doc = System.Xml.Linq.XDocument.Parse ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
<application>
<service android:name="com.example.NonExistentService" />
</application>
</manifest>
""");

var warnings = new List<string> ();
var generator = CreateGenerator (warnings);
generator.RootManifestReferencedTypes (peers, doc);

Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService"));
}

[Fact]
public void RootManifestReferencedTypes_SkipsAlreadyUnconditional ()
{
var peers = new List<JavaPeerInfo> {
new JavaPeerInfo {
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
AssemblyName = "MyApp", IsUnconditional = true,
},
};

var doc = System.Xml.Linq.XDocument.Parse ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
<application>
<activity android:name="com.example.MyActivity" />
</application>
</manifest>
""");

var generator = CreateGenerator ();
generator.RootManifestReferencedTypes (peers, doc);

Assert.True (peers [0].IsUnconditional);
Assert.DoesNotContain (logMessages, m => m.Contains ("Rooting manifest-referenced type"));
}

[Fact]
public void RootManifestReferencedTypes_EmptyManifest_NoChanges ()
{
var peers = new List<JavaPeerInfo> {
new JavaPeerInfo {
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
AssemblyName = "MyApp",
},
};

var doc = System.Xml.Linq.XDocument.Parse ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
</manifest>
""");

var generator = CreateGenerator ();
generator.RootManifestReferencedTypes (peers, doc);

Assert.False (peers [0].IsUnconditional);
}

static string GetTestFixtureAssemblyPath ()
{
var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public void InterfacePropertyImpl_DetectedWithCorrectSignature ()
var peer = FindFixtureByJavaName ("my/app/ImplicitPropertyImpl");
var getName = peer.MarshalMethods.First (m => m.JniName == "getName");
Assert.Equal ("()Ljava/lang/String;", getName.JniSignature);
Assert.Equal ("GetGetNameHandler:Android.Views.IHasNameInvoker", getName.Connector);
Assert.Equal ("GetGetNameHandler:Android.Views.IHasNameInvoker, TestFixtures, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", getName.Connector);
Assert.Equal ("Android.Views.IHasNameInvoker", getName.DeclaringTypeName);
Assert.Equal ("TestFixtures", getName.DeclaringAssemblyName);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public interface IOnLongClickListener
[Register ("android/view/View$IHasName", "", "Android.Views.IHasNameInvoker")]
public interface IHasName
{
[Register ("getName", "()Ljava/lang/String;", "GetGetNameHandler:Android.Views.IHasNameInvoker")]
[Register ("getName", "()Ljava/lang/String;", "GetGetNameHandler:Android.Views.IHasNameInvoker, TestFixtures, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")]
string? Name { get; }
}

Expand Down