Skip to content

Commit 5926858

Browse files
Address PR review: fix manifest name matching and null guard
- Fix RootManifestReferencedTypes to resolve relative android:name values (.MyActivity, MyActivity) using manifest package attribute - Keep $ separator in peer lookup keys so nested types (Outer$Inner) match correctly against manifest class names - Guard Path.GetDirectoryName against null return for acw-map path - Fix pre-existing compilation error: load XDocument from template path before passing to ManifestGenerator.Generate - Add tests for relative name resolution and nested type matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0b6416a commit 5926858

2 files changed

Lines changed: 91 additions & 4 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ public TrimmableTypeMapResult Execute (
6262

6363
// Write merged acw-map.txt if requested
6464
if (!acwMapOutputPath.IsNullOrEmpty ()) {
65-
Directory.CreateDirectory (Path.GetDirectoryName (acwMapOutputPath));
65+
var acwDirectory = Path.GetDirectoryName (acwMapOutputPath);
66+
if (!acwDirectory.IsNullOrEmpty ()) {
67+
Directory.CreateDirectory (acwDirectory);
68+
}
6669
using (var writer = new StreamWriter (acwMapOutputPath)) {
6770
AcwMapWriter.Write (writer, allPeers);
6871
}
@@ -104,7 +107,12 @@ IList<string> GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifestInf
104107
ApplicationJavaClass = config.ApplicationJavaClass,
105108
};
106109

107-
return generator.Generate (manifestTemplatePath, allPeers, assemblyManifestInfo, mergedManifestOutputPath);
110+
XDocument? manifestTemplateDoc = null;
111+
if (!manifestTemplatePath.IsNullOrEmpty () && File.Exists (manifestTemplatePath)) {
112+
manifestTemplateDoc = XDocument.Load (manifestTemplatePath);
113+
}
114+
115+
return generator.Generate (manifestTemplateDoc, allPeers, assemblyManifestInfo, mergedManifestOutputPath);
108116
}
109117

110118
(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
@@ -176,6 +184,7 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
176184

177185
XNamespace androidNs = "http://schemas.android.com/apk/res/android";
178186
XName attName = androidNs + "name";
187+
var packageName = (string?) root.Attribute ("package") ?? "";
179188

180189
var componentNames = new HashSet<string> (StringComparer.Ordinal);
181190
foreach (var element in root.Descendants ()) {
@@ -186,7 +195,7 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
186195
case "provider":
187196
var name = (string?) element.Attribute (attName);
188197
if (name is not null) {
189-
componentNames.Add (name);
198+
componentNames.Add (ResolveManifestClassName (name, packageName));
190199
}
191200
break;
192201
}
@@ -196,9 +205,10 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
196205
return;
197206
}
198207

208+
// Build lookup by dot-name, keeping '$' for nested types (manifests use '$' too).
199209
var peersByDotName = new Dictionary<string, List<JavaPeerInfo>> (StringComparer.Ordinal);
200210
foreach (var peer in allPeers) {
201-
var dotName = peer.JavaName.Replace ('/', '.').Replace ('$', '.');
211+
var dotName = peer.JavaName.Replace ('/', '.');
202212
if (!peersByDotName.TryGetValue (dotName, out var list)) {
203213
list = [];
204214
peersByDotName [dotName] = list;
@@ -219,4 +229,22 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
219229
}
220230
}
221231
}
232+
233+
/// <summary>
234+
/// Resolves an android:name value to a fully-qualified class name.
235+
/// Names starting with '.' are relative to the package. Names with no '.' at all
236+
/// are also treated as relative (Android tooling convention).
237+
/// </summary>
238+
static string ResolveManifestClassName (string name, string packageName)
239+
{
240+
if (name.StartsWith (".", StringComparison.Ordinal)) {
241+
return packageName + name;
242+
}
243+
244+
if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) {
245+
return packageName + "." + name;
246+
}
247+
248+
return name;
249+
}
222250
}

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,65 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges ()
194194
Assert.False (peers [0].IsUnconditional);
195195
}
196196

197+
[Fact]
198+
public void RootManifestReferencedTypes_ResolvesRelativeNames ()
199+
{
200+
var peers = new List<JavaPeerInfo> {
201+
new JavaPeerInfo {
202+
JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity",
203+
ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity",
204+
AssemblyName = "MyApp", IsUnconditional = false,
205+
},
206+
new JavaPeerInfo {
207+
JavaName = "com/example/MyService", CompatJniName = "com.example.MyService",
208+
ManagedTypeName = "MyApp.MyService", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyService",
209+
AssemblyName = "MyApp", IsUnconditional = false,
210+
},
211+
};
212+
213+
var doc = System.Xml.Linq.XDocument.Parse ("""
214+
<?xml version="1.0" encoding="utf-8"?>
215+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
216+
<application>
217+
<activity android:name=".MyActivity" />
218+
<service android:name="MyService" />
219+
</application>
220+
</manifest>
221+
""");
222+
223+
var generator = CreateGenerator ();
224+
generator.RootManifestReferencedTypes (peers, doc);
225+
226+
Assert.True (peers [0].IsUnconditional, "Dot-relative name '.MyActivity' should resolve to com.example.MyActivity.");
227+
Assert.True (peers [1].IsUnconditional, "Simple name 'MyService' should resolve to com.example.MyService.");
228+
}
229+
230+
[Fact]
231+
public void RootManifestReferencedTypes_MatchesNestedTypes ()
232+
{
233+
var peers = new List<JavaPeerInfo> {
234+
new JavaPeerInfo {
235+
JavaName = "com/example/Outer$Inner", CompatJniName = "com.example.Outer$Inner",
236+
ManagedTypeName = "MyApp.Outer.Inner", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "Inner",
237+
AssemblyName = "MyApp", IsUnconditional = false,
238+
},
239+
};
240+
241+
var doc = System.Xml.Linq.XDocument.Parse ("""
242+
<?xml version="1.0" encoding="utf-8"?>
243+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
244+
<application>
245+
<activity android:name="com.example.Outer$Inner" />
246+
</application>
247+
</manifest>
248+
""");
249+
250+
var generator = CreateGenerator ();
251+
generator.RootManifestReferencedTypes (peers, doc);
252+
253+
Assert.True (peers [0].IsUnconditional, "Nested type 'Outer$Inner' should be matched using '$' separator.");
254+
}
255+
197256
static PEReader CreateTestFixturePEReader ()
198257
{
199258
var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)

0 commit comments

Comments
 (0)