From d5c7ea95ce5bab001dc90b56d76680d4b1fac961 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:53:31 +0000 Subject: [PATCH 1/5] Initial plan From 68724e1eb761513b5fd068ab8f0d82d80053fd25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:34:50 +0000 Subject: [PATCH 2/5] Add scanner field integration coverage Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 2 +- .../ScannerFieldIntegrationTests.cs | 139 ++++++++++++++++++ .../UserTypesFixture/UserTypes.cs | 70 +++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fc3627224f5..62bc524c36a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -673,7 +673,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) string compatName = ns.Length == 0 ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + : $"{ns.ToLowerInvariant ()}/{typeName}"; return (jniName, compatName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs new file mode 100644 index 00000000000..6f63645a434 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; + +public partial class ScannerComparisonTests +{ + static readonly Lazy> monoAndroidPeers = new (() => ScanPeers (AllAssemblyPaths)); + static readonly Lazy> userFixturePeers = new (() => { + var paths = AllUserTypesAssemblyPaths + ?? throw new InvalidOperationException ("UserTypesFixture.dll not found."); + return ScanPeers (paths); + }); + + [Fact] + public void IsUnconditional_ComponentTypes () + { + Assert.True (FindUserPeerByManagedName ("UserApp.MainActivity").IsUnconditional); + Assert.True (FindUserPeerByManagedName ("UserApp.Services.MyBackgroundService").IsUnconditional); + Assert.True (FindUserPeerByManagedName ("UserApp.Receivers.BootReceiver").IsUnconditional); + Assert.True (FindUserPeerByManagedName ("UserApp.Providers.SettingsProvider").IsUnconditional); + + Assert.False (FindUserPeerByManagedName ("UserApp.PlainActivity").IsUnconditional); + Assert.False (FindUserPeerByManagedName ("UserApp.Listeners.MyClickListener").IsUnconditional); + + Assert.True (FindUserPeerByManagedName ("UserApp.MyBackupAgent").IsUnconditional); + } + + [Fact] + public void IsUnconditional_MonoAndroid () + { + var peers = ScanMonoAndroidPeers (); + + Assert.NotEmpty (peers); + Assert.Equal (0, peers.Count (p => p.IsUnconditional)); + Assert.All (peers.Where (p => p.DoNotGenerateAcw), peer => + Assert.False (peer.IsUnconditional, $"{peer.ManagedTypeName} should not be unconditional.")); + } + + [Fact] + public void InvokerTypeName_InterfacesAndAbstractTypes_MonoAndroid () + { + var peers = ScanMonoAndroidPeers (); + var managedNames = peers.Select (p => p.ManagedTypeName).ToHashSet (StringComparer.Ordinal); + var interfaces = peers.Where (p => p.IsInterface).ToList (); + + Assert.NotEmpty (interfaces); + Assert.All (interfaces, peer => { + var invokerTypeName = peer.InvokerTypeName; + Assert.False (string.IsNullOrEmpty (invokerTypeName), $"{peer.ManagedTypeName} should have an invoker."); + if (invokerTypeName is not null) { + Assert.Contains (invokerTypeName, managedNames); + } + }); + + var clickListener = interfaces.Single (p => p.JavaName == "android/view/View$OnClickListener"); + Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", clickListener.InvokerTypeName); + Assert.Contains ("Android.Views.View+IOnClickListenerInvoker", managedNames); + + var absListView = FindMonoAndroidPeerByManagedName ("Android.Widget.AbsListView"); + Assert.Equal ("Android.Widget.AbsListViewInvoker", absListView.InvokerTypeName); + Assert.Contains ("Android.Widget.AbsListViewInvoker", managedNames); + } + + [Fact] + public void InvokerTypeName_UserTypes () + { + Assert.Null (FindUserPeerByManagedName ("UserApp.MainActivity").InvokerTypeName); + Assert.Null (FindUserPeerByManagedName ("UserApp.Listeners.MyClickListener").InvokerTypeName); + + Assert.Equal ("UserApp.Interfaces.IWidgetListenerInvoker", + FindUserPeerByManagedName ("UserApp.Interfaces.IWidgetListener").InvokerTypeName); + Assert.Equal ("UserApp.AbstractWidgets.AbstractWidgetInvoker", + FindUserPeerByManagedName ("UserApp.AbstractWidgets.AbstractWidget").InvokerTypeName); + } + + [Fact] + public void CompatJniName_UserTypes () + { + var userModel = FindUserPeerByManagedName ("UserApp.Models.UserModel"); + Assert.StartsWith ("crc64", userModel.JavaName); + Assert.Equal ("userapp.models/UserModel", userModel.CompatJniName); + + var dataManager = FindUserPeerByManagedName ("UserApp.Models.DataManager"); + Assert.StartsWith ("crc64", dataManager.JavaName); + Assert.Equal ("userapp.models/DataManager", dataManager.CompatJniName); + } + + [Fact] + public void ManagedMethodName_MarshalMethods () + { + var peers = ScanMonoAndroidPeers (); + var marshalMethods = peers.SelectMany (p => p.MarshalMethods).ToList (); + + Assert.NotEmpty (marshalMethods); + Assert.All (marshalMethods, method => Assert.False (string.IsNullOrWhiteSpace (method.ManagedMethodName))); + + var activity = FindMonoAndroidPeerByJavaName ("android/app/Activity"); + Assert.Contains (activity.MarshalMethods, method => + method.ManagedMethodName == "OnCreate" && method.JniName == "onCreate"); + } + + static List ScanMonoAndroidPeers () => monoAndroidPeers.Value; + + static List ScanUserFixturePeers () => userFixturePeers.Value; + + static List ScanPeers (string[] assemblyPaths) + { + var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]); + using var scanner = new JavaPeerScanner (); + return scanner.Scan (assemblyPaths) + .Where (p => p.AssemblyName == primaryAssemblyName) + .ToList (); + } + + static JavaPeerInfo FindMonoAndroidPeerByJavaName (string javaName) + => FindPeerByJavaName (ScanMonoAndroidPeers (), javaName); + + static JavaPeerInfo FindMonoAndroidPeerByManagedName (string managedTypeName) + => FindPeerByManagedName (ScanMonoAndroidPeers (), managedTypeName); + + static JavaPeerInfo FindUserPeerByManagedName (string managedTypeName) + => FindPeerByManagedName (ScanUserFixturePeers (), managedTypeName); + + static JavaPeerInfo FindPeerByJavaName (IEnumerable peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + return peer ?? throw new InvalidOperationException ($"Peer with Java name '{javaName}' not found."); + } + + static JavaPeerInfo FindPeerByManagedName (IEnumerable peers, string managedTypeName) + { + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedTypeName); + return peer ?? throw new InvalidOperationException ($"Peer with managed name '{managedTypeName}' not found."); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 75586236a8e..5c6fbdb164e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -65,6 +65,25 @@ public override void OnReceive (Context? context, Intent? intent) } } +namespace UserApp.Providers +{ + [ContentProvider (Name = "com.example.userapp.SettingsProvider")] + public class SettingsProvider : ContentProvider + { + public override bool OnCreate () => true; + + public override int Delete (Android.Net.Uri? uri, string? selection, string[]? selectionArgs) => 0; + + public override string? GetType (Android.Net.Uri? uri) => null; + + public override Android.Net.Uri? Insert (Android.Net.Uri? uri, ContentValues? values) => null; + + public override Android.Database.ICursor? Query (Android.Net.Uri? uri, string[]? projection, string? selection, string[]? selectionArgs, string? sortOrder) => null; + + public override int Update (Android.Net.Uri? uri, ContentValues? values, string? selection, string[]? selectionArgs) => 0; + } +} + namespace UserApp { public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -92,6 +111,57 @@ public MyApp (IntPtr handle, JniHandleOwnership transfer) } } +namespace UserApp.Interfaces +{ + [Register ("com/example/userapp/IWidgetListener", "", "UserApp.Interfaces.IWidgetListenerInvoker")] + public interface IWidgetListener + { + [Register ("onWidgetChanged", "(Ljava/lang/String;)V", "GetOnWidgetChanged_Ljava_lang_String_Handler:UserApp.Interfaces.IWidgetListenerInvoker")] + void OnWidgetChanged (string? value); + } + + [Register ("com/example/userapp/IWidgetListener", DoNotGenerateAcw = true)] + internal sealed class IWidgetListenerInvoker : Java.Lang.Object, IWidgetListener + { + public IWidgetListenerInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + public void OnWidgetChanged (string? value) + { + } + } +} + +namespace UserApp.AbstractWidgets +{ + [Register ("com/example/userapp/AbstractWidget")] + public abstract class AbstractWidget : Java.Lang.Object + { + protected AbstractWidget (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("performAction", "()V", "GetPerformActionHandler")] + public abstract void PerformAction (); + } + + [Register ("com/example/userapp/AbstractWidget", DoNotGenerateAcw = true)] + internal sealed class AbstractWidgetInvoker : AbstractWidget + { + public AbstractWidgetInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + public override void PerformAction () + { + } + } +} + namespace UserApp.Nested { [Register ("com/example/userapp/OuterClass")] From 6ba4b5506a2176bc9489fe21a0e03a0d564cd489 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:39:27 +0000 Subject: [PATCH 3/5] Polish scanner field integration tests Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 1 + .../ScannerFieldIntegrationTests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 62bc524c36a..af149ba1912 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -673,6 +673,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) string compatName = ns.Length == 0 ? typeName + // Compat format: UserApp.Models -> userapp.models/TypeName (preserves dots, no slash replacement) : $"{ns.ToLowerInvariant ()}/{typeName}"; return (jniName, compatName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs index 6f63645a434..17261bbe70d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs @@ -8,8 +8,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests; public partial class ScannerComparisonTests { - static readonly Lazy> monoAndroidPeers = new (() => ScanPeers (AllAssemblyPaths)); - static readonly Lazy> userFixturePeers = new (() => { + static readonly Lazy> MonoAndroidPeers = new (() => ScanPeers (AllAssemblyPaths)); + static readonly Lazy> UserFixturePeers = new (() => { var paths = AllUserTypesAssemblyPaths ?? throw new InvalidOperationException ("UserTypesFixture.dll not found."); return ScanPeers (paths); @@ -103,9 +103,9 @@ public void ManagedMethodName_MarshalMethods () method.ManagedMethodName == "OnCreate" && method.JniName == "onCreate"); } - static List ScanMonoAndroidPeers () => monoAndroidPeers.Value; + static List ScanMonoAndroidPeers () => MonoAndroidPeers.Value; - static List ScanUserFixturePeers () => userFixturePeers.Value; + static List ScanUserFixturePeers () => UserFixturePeers.Value; static List ScanPeers (string[] assemblyPaths) { From e9900e097bf252729d1e1521db658e74e8f1bb0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 18:15:11 +0000 Subject: [PATCH 4/5] Fix UserTypesFixture content provider attribute Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../UserTypesFixture/UserTypes.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs index 5c6fbdb164e..535bb526b6a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -67,7 +67,8 @@ public override void OnReceive (Context? context, Intent? intent) namespace UserApp.Providers { - [ContentProvider (Name = "com.example.userapp.SettingsProvider")] + // Authorities use Android manifest naming conventions; Name is the generated JNI type name. + [ContentProvider (new [] { "com.example.userapp.settingsprovider" }, Name = "com.example.userapp.SettingsProvider")] public class SettingsProvider : ContentProvider { public override bool OnCreate () => true; From 1fa4c165f46ce94ab72e5d9899eaf5bf0221a31b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:13:38 +0000 Subject: [PATCH 5/5] Address review feedback on scanner tests Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 2 +- .../ScannerFieldIntegrationTests.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index af149ba1912..98786c468e6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -673,7 +673,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) string compatName = ns.Length == 0 ? typeName - // Compat format: UserApp.Models -> userapp.models/TypeName (preserves dots, no slash replacement) + // Compat format preserves namespace dots: UserApp.Models -> userapp.models/TypeName : $"{ns.ToLowerInvariant ()}/{typeName}"; return (jniName, compatName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs index 17261bbe70d..c8e3d78a60f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs @@ -51,9 +51,7 @@ public void InvokerTypeName_InterfacesAndAbstractTypes_MonoAndroid () Assert.All (interfaces, peer => { var invokerTypeName = peer.InvokerTypeName; Assert.False (string.IsNullOrEmpty (invokerTypeName), $"{peer.ManagedTypeName} should have an invoker."); - if (invokerTypeName is not null) { - Assert.Contains (invokerTypeName, managedNames); - } + Assert.Contains (invokerTypeName, managedNames); }); var clickListener = interfaces.Single (p => p.JavaName == "android/view/View$OnClickListener");