Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Xamarin.Android.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
15 changes: 15 additions & 0 deletions build-tools/automation/yaml-templates/build-windows-steps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="System.IO.Hashing" Version="$(SystemIOHashingPackageVersion)" />
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataPackageVersion)" />
<InternalsVisibleTo Include="Microsoft.Android.Sdk.TrimmableTypeMap.Tests" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ('.', '/');
}
}
}

Expand All @@ -129,6 +136,36 @@ static TypeAttributeInfo CreateTypeAttributeInfo (string attrName)

static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName);

/// <summary>
/// Checks whether a custom attribute's type implements <c>Java.Interop.IJniNameProviderAttribute</c>.
/// Only works for attributes defined in the assembly being scanned (MethodDefinition constructors).
/// </summary>
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DotNetStableTargetFramework)</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Microsoft.Android.Sdk.TrimmableTypeMap.Tests</RootNamespace>
</PropertyGroup>

<!-- Exclude TestFixtures subdirectory — it's a separate project compiled independently -->
<ItemGroup>
<Compile Remove="TestFixtures\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj" />
<ProjectReference Include="TestFixtures\TestFixtures.csproj">
<!-- Don't add as a compile-time reference — we only need the built DLL as scanner test input -->
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>

<!-- Copy TestFixtures output to our output directory so tests can find it -->
<Target Name="_CopyTestFixturesOutput" AfterTargets="Build">
<ItemGroup>
<_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" />
</ItemGroup>
<Copy SourceFiles="@(_TestFixtureFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading