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 { }