diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index d1edbe95c97..d5554ea849a 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -59,6 +59,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.ProjectTools", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Build.Tests", "src\Xamarin.Android.Build.Tasks\Tests\Xamarin.Android.Build.Tests\Xamarin.Android.Build.Tests.csproj", "{53E4ABF0-1085-45F9-B964-DCAE4B819998}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap", "src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj", "{507759AE-93DF-411B-8645-31F680319F5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.Tests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj", "{F9CD012E-67AC-4A4E-B2A7-252387F91256}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}" @@ -231,6 +237,18 @@ Global {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Debug|AnyCPU.Build.0 = Debug|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.ActiveCfg = Release|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.Build.0 = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.Build.0 = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.Build.0 = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU @@ -398,6 +416,8 @@ Global {645E1718-C8C4-4C23-8A49-5A37E4ECF7ED} = {04E3E11E-B47D-4599-8AFC-50515A95E715} {2DD1EE75-6D8D-4653-A800-0A24367F7F38} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058} {7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058} {8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 6d3d6738ad2..48544d84d7a 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -77,6 +77,21 @@ steps: testRunTitle: Microsoft.Android.Sdk.Analysis.Tests continueOnError: true +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj + arguments: -c $(XA.Build.Configuration) --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.Tests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.Tests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests + - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index d75d4d7df1e..bf04c5efde8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index cf8bd977a71..4da7b3d752d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -105,6 +105,13 @@ void Build () applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); } + } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { + // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) + var name = TryGetNameProperty (ca); + if (name is not null) { + attrInfo = new TypeAttributeInfo (attrName); + attrInfo.JniName = name.Replace ('.', '/'); + } } } @@ -129,6 +136,36 @@ static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName); + /// + /// Checks whether a custom attribute's type implements Java.Interop.IJniNameProviderAttribute. + /// Only works for attributes defined in the assembly being scanned (MethodDefinition constructors). + /// + bool ImplementsJniNameProviderAttribute (CustomAttribute ca) + { + if (ca.Constructor.Kind != HandleKind.MethodDefinition) { + return false; + } + var methodDef = Reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); + var typeDef = Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { + var impl = Reader.GetInterfaceImplementation (implHandle); + if (impl.Interface.Kind == HandleKind.TypeReference) { + var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); + if (Reader.GetString (typeRef.Name) == "IJniNameProviderAttribute" && + Reader.GetString (typeRef.Namespace) == "Java.Interop") { + return true; + } + } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { + var ifaceDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); + if (Reader.GetString (ifaceDef.Name) == "IJniNameProviderAttribute" && + Reader.GetString (ifaceDef.Namespace) == "Java.Interop") { + return true; + } + } + } + return false; + } + internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) { if (ca.Constructor.Kind == HandleKind.MemberReference) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj new file mode 100644 index 00000000000..6370a77e680 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.Tests + + + + + + + + + + + + + + + + + + false + + + + + + + <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs new file mode 100644 index 00000000000..81aabe9ad74 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var peers = ScanFixtures (); + var method = FindByJavaName (peers, javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var peers = ScanFixtures (); + + var ctors = FindByJavaName (peers, "my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindByJavaName (peers, "android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); + } + + [Theory] + [InlineData ("android/app/Activity", "Android.App.Activity")] + [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] + [InlineData ("my/app/MyButton", "MyApp.MyButton")] + public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType) + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, javaName); + Assert.NotNull (peer.ActivationCtor); + Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName); + } + + [Theory] + [InlineData ("java/lang/Object", null)] + [InlineData ("android/app/Activity", "java/lang/Object")] + [InlineData ("my/app/MainActivity", "android/app/Activity")] + [InlineData ("java/lang/Throwable", "java/lang/Object")] + [InlineData ("java/lang/Exception", "java/lang/Throwable")] + [InlineData ("my/app/MyButton", "android/widget/Button")] + public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase) + { + var peers = ScanFixtures (); + Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + + Assert.Contains ("android/view/View$OnClickListener", + FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames); + Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); + } + + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + var peers = ScanFixtures (); + Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + Assert.Equal ("com/example/CustomWidget", + FindByManagedName (peers, "MyApp.CustomWidget").JavaName); + } + + [Theory] + [InlineData ("my/app/Outer$Inner", "MyApp.Outer+Inner")] + [InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")] + public void Scan_NestedType_IsDiscovered (string javaName, string managedName) + { + var peers = ScanFixtures (); + Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs new file mode 100644 index 00000000000..ec368ed6a44 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Fact] + public void Scan_GenericTypes_ResolveViaTypeSpecification () + { + var peers = ScanFixtures (); + Assert.Equal ("my/app/GenericBase", + FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName); + Assert.Contains ("my/app/IGenericCallback", + FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () + { + var peers = ScanFixtures (); + + var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + Assert.True (baseType.IsUnconditional); + Assert.Equal ("android/app/Activity", baseType.BaseJavaName); + + var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase"); + Assert.StartsWith ("crc64", derived.JavaName); + } + + [Theory] + [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "my/app/RegisteredParent_UnregisteredChild")] + [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")] + public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName) + { + var peers = ScanFixtures (); + Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_EmptyNamespace_Handled () + { + var peers = ScanFixtures (); + Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); + } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnnamedActivity")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) + { + var peers = ScanFixtures (); + Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var peers = ScanFixtures (); + var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs new file mode 100644 index 00000000000..bc2b6195f22 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + JavaPeerInfo FindByJavaName (List peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + JavaPeerInfo FindByManagedName (List peers, string managedName) + { + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + [Fact] + public void Scan_FindsAllJavaPeerTypes () + { + var peers = ScanFixtures (); + Assert.NotEmpty (peers); + Assert.Contains (peers, p => p.JavaName == "java/lang/Object"); + Assert.Contains (peers, p => p.JavaName == "android/app/Activity"); + Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity"); + } + + [Theory] + [InlineData ("android/app/Activity", true)] + [InlineData ("android/widget/Button", true)] + [InlineData ("my/app/MainActivity", false)] + public void Scan_DoNotGenerateAcw (string javaName, bool expected) + { + var peers = ScanFixtures (); + Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw); + } + + [Theory] + [InlineData ("my/app/MainActivity", true)] + [InlineData ("my/app/MyService", true)] + [InlineData ("my/app/MyReceiver", true)] + [InlineData ("my/app/MyProvider", true)] + [InlineData ("my/app/MyApplication", true)] + [InlineData ("my/app/MyInstrumentation", true)] + [InlineData ("my/app/MyBackupAgent", true)] + [InlineData ("my/app/MyManageSpaceActivity", true)] + [InlineData ("my/app/MyHelper", false)] + [InlineData ("android/app/Activity", false)] + public void Scan_IsUnconditional (string javaName, bool expected) + { + var peers = ScanFixtures (); + Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional); + } + + [Fact] + public void Scan_TypeMetadata_IsCorrect () + { + var peers = ScanFixtures (); + Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract); + Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface); + Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw); + + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + } + + [Fact] + public void Scan_InvokerAndInterface_ShareJavaName () + { + var peers = ScanFixtures (); + var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickListenerPeers.Count); + Assert.Contains (clickListenerPeers, p => p.IsInterface); + Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + } + + [Fact] + public void Scan_AllTypes_HaveAssemblyName () + { + var peers = ScanFixtures (); + Assert.All (peers, peer => + Assert.False (string.IsNullOrEmpty (peer.AssemblyName), + $"Type {peer.ManagedTypeName} should have assembly name")); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs new file mode 100644 index 00000000000..36c7587eb28 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -0,0 +1,119 @@ +using System; + +namespace Java.Interop +{ + public interface IJniNameProviderAttribute + { + string Name { get; } + } +} + +namespace Android.Runtime +{ + [AttributeUsage ( + AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | + AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, + AllowMultiple = false)] + public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public string? Signature { get; set; } + public string? Connector { get; set; } + public bool DoNotGenerateAcw { get; set; } + public int ApiSince { get; set; } + + public RegisterAttribute (string name) => Name = name; + + public RegisterAttribute (string name, string signature, string connector) + { + Name = name; + Signature = signature; + Connector = connector; + } + } + + public enum JniHandleOwnership + { + DoNotTransfer = 0, + TransferLocalRef = 1, + TransferGlobalRef = 2, + } +} + +namespace Android.App +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class ActivityAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public bool MainLauncher { get; set; } + public string? Label { get; set; } + public string? Icon { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ServiceAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class InstrumentationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ApplicationAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public Type? BackupAgent { get; set; } + public Type? ManageSpaceActivity { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } +} + +namespace Android.Content +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class BroadcastReceiverAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string []? Authorities { get; set; } + public string? Name { get; set; } + string Java.Interop.IJniNameProviderAttribute.Name => Name ?? ""; + + public ContentProviderAttribute (string [] authorities) => Authorities = authorities; + } +} + +namespace Java.Interop +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportAttribute : Attribute + { + public string? Name { get; set; } + + public ExportAttribute () { } + public ExportAttribute (string name) => Name = name; + } +} + +namespace MyApp +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class CustomJniNameAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public CustomJniNameAttribute (string name) => Name = name; + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj new file mode 100644 index 00000000000..f7f4c72139b --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + Microsoft.Android.Sdk.TrimmableTypeMap.Tests.TestFixtures + + false + true + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs new file mode 100644 index 00000000000..35987f36f93 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -0,0 +1,341 @@ +using System; +using Android.App; +using Android.Content; +using Android.Runtime; + +namespace Java.Lang +{ + [Register ("java/lang/Object", DoNotGenerateAcw = true)] + public class Object + { + public Object () { } + protected Object (IntPtr handle, JniHandleOwnership transfer) { } + } + + [Register ("java/lang/Throwable", DoNotGenerateAcw = true)] + public class Throwable : Object + { + protected Throwable (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")] + public virtual string? Message { get; } + } + + [Register ("java/lang/Exception", DoNotGenerateAcw = true)] + public class Exception : Throwable + { + protected Exception (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.App +{ + [Register ("android/app/Activity", DoNotGenerateAcw = true)] + public class Activity : Java.Lang.Object + { + public Activity () { } + protected Activity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual void OnCreate (object? savedInstanceState) { } + + [Register ("onStart", "()V", "")] + protected virtual void OnStart () { } + } + + [Register ("android/app/Service", DoNotGenerateAcw = true)] + public class Service : Java.Lang.Object + { + protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.App.Backup +{ + [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] + public class BackupAgent : Java.Lang.Object + { + protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.Content +{ + [Register ("android/content/Context", DoNotGenerateAcw = true)] + public class Context : Java.Lang.Object + { + protected Context (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace Android.Views +{ + [Register ("android/view/View", DoNotGenerateAcw = true)] + public class View : Java.Lang.Object + { + protected View (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] + public interface IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] + void OnClick (View v); + } + + [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] + internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + public void OnClick (View v) { } + } + + [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] + public interface IOnLongClickListener + { + [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] + bool OnLongClick (View v); + } +} + +namespace Android.Widget +{ + [Register ("android/widget/Button", DoNotGenerateAcw = true)] + public class Button : Android.Views.View + { + protected Button (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/widget/TextView", DoNotGenerateAcw = true)] + public class TextView : Android.Views.View + { + protected TextView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +namespace MyApp +{ + [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] + public class MainActivity : Android.App.Activity + { + public MainActivity () { } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected override void OnCreate (object? savedInstanceState) => base.OnCreate (savedInstanceState); + } + + [Register ("my/app/MyHelper")] + public class MyHelper : Java.Lang.Object + { + [Register ("doSomething", "()V", "GetDoSomethingHandler")] + public virtual void DoSomething () { } + } + + [Service (Name = "my.app.MyService")] + public class MyService : Android.App.Service + { + protected MyService (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [BroadcastReceiver (Name = "my.app.MyReceiver")] + public class MyReceiver : Java.Lang.Object { } + + [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider")] + public class MyProvider : Java.Lang.Object { } + + [Register ("my/app/AbstractBase")] + public abstract class AbstractBase : Java.Lang.Object + { + protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("doWork", "()V", "")] + public abstract void DoWork (); + } + + [Register ("my/app/SimpleActivity")] + public class SimpleActivity : Android.App.Activity { } + + [Register ("my/app/ClickableView")] + public class ClickableView : Android.Views.View, Android.Views.IOnClickListener + { + protected ClickableView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + } + + [Register ("my/app/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("", "()V", "")] + public CustomView () : base (default!, default) { } + + [Register ("", "(Landroid/content/Context;)V", "")] + public CustomView (Context context) : base (default!, default) { } + } + + [Register ("my/app/Outer")] + public class Outer : Java.Lang.Object + { + [Register ("my/app/Outer$Inner")] + public class Inner : Java.Lang.Object + { + protected Inner (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + } + + [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")] + public interface ICallback + { + [Register ("my/app/ICallback$Result")] + public class Result : Java.Lang.Object + { + protected Result (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + } + + [Register ("my/app/TouchHandler")] + public class TouchHandler : Java.Lang.Object + { + [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")] + public virtual bool OnTouch (Android.Views.View v, int action) => false; + + [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")] + public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) { } + + [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")] + public virtual void OnScroll (int x, float y, long timestamp, double velocity) { } + + [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")] + public virtual string? GetText () => null; + + [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")] + public virtual void SetItems (string[]? items) { } + } + + [Register ("my/app/ExportExample")] + public class ExportExample : Java.Lang.Object + { + [Java.Interop.Export ("myExportedMethod")] + public void MyExportedMethod () { } + } + + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] + public class MyApplication : Java.Lang.Object { } + + [Instrumentation (Name = "my.app.MyInstrumentation")] + public class MyInstrumentation : Java.Lang.Object { } + + [Register ("my/app/MyBackupAgent")] + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/MyManageSpaceActivity")] + public class MyManageSpaceActivity : Android.App.Activity + { + protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + public class UnregisteredHelper : Java.Lang.Object { } + + [Register ("my/app/MyButton")] + public class MyButton : Android.Widget.Button + { + protected MyButton (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/MultiInterfaceView")] + public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener + { + protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + + [Register ("onLongClick", "(Landroid/view/View;)Z", "")] + public bool OnLongClick (Android.Views.View v) => false; + } + + [CustomJniName ("com.example.CustomWidget")] + public class CustomWidget : Java.Lang.Object { } + + [Activity (Name = "my.app.BaseActivityNoRegister")] + public class BaseActivityNoRegister : Android.App.Activity { } + + public class DerivedFromComponentBase : BaseActivityNoRegister { } + + [Register ("my/app/RegisteredParent")] + public class RegisteredParent : Java.Lang.Object + { + public class UnregisteredChild : Java.Lang.Object { } + } + + [Register ("my/app/DeepOuter")] + public class DeepOuter : Java.Lang.Object + { + public class Middle : Java.Lang.Object + { + public class DeepInner : Java.Lang.Object { } + } + } + + public class PlainActivitySubclass : Android.App.Activity { } + + [Activity (Label = "Unnamed")] + public class UnnamedActivity : Android.App.Activity { } + + public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + } + + public class UnregisteredExporter : Java.Lang.Object + { + [Java.Interop.Export ("doExportedWork")] + public void DoExportedWork () { } + } +} + +namespace MyApp.Generic +{ + [Register ("my/app/GenericHolder")] + public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + { + [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] + public virtual T? GetItem () => default; + } + + [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] + public class GenericBase : Java.Lang.Object where T : class + { + protected GenericBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/ConcreteFromGeneric")] + public class ConcreteFromGeneric : GenericBase + { + protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("my/app/IGenericCallback", "", "")] + public interface IGenericCallback { } + + [Register ("my/app/GenericCallbackImpl")] + public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback + { + protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } +} + +[Register ("my/app/GlobalType")] +public class GlobalType : Java.Lang.Object +{ + protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } +} + +public class GlobalUnregisteredType : Java.Lang.Object { }