diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index fc3627224f5..98786c468e6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -673,7 +673,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) string compatName = ns.Length == 0 ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + // 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 new file mode 100644 index 00000000000..c8e3d78a60f --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerFieldIntegrationTests.cs @@ -0,0 +1,137 @@ +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."); + 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..535bb526b6a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs @@ -65,6 +65,26 @@ public override void OnReceive (Context? context, Intent? intent) } } +namespace UserApp.Providers +{ + // 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; + + 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 +112,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")]