diff --git a/Xamarin.Android.slnx b/Xamarin.Android.slnx
index 2c15cd1d963..9ec9f9fcdb6 100644
--- a/Xamarin.Android.slnx
+++ b/Xamarin.Android.slnx
@@ -62,6 +62,7 @@
+
diff --git a/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
index ea69f86dfeb..c71f5dd524e 100644
--- a/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
+++ b/src/Microsoft.Android.Build.BaseTasks/HexUtilities.cs
@@ -2,11 +2,12 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Text;
namespace Microsoft.Android.Build.Tasks
{
///
- /// Allocation-free helpers for rendering bytes as hexadecimal.
+ /// Allocation-free helpers for rendering values as hexadecimal.
///
///
/// This file is also linked into Microsoft.Android.Sdk.TrimmableTypeMap, which
@@ -65,6 +66,21 @@ public static void WriteHex (TextWriter writer, byte value, bool upperCase = tru
writer.Write (GetHexValue (value & 0x0f, upperCase));
}
+ ///
+ /// Append to as exactly four
+ /// hexadecimal digits, without allocating.
+ ///
+ public static void WriteHex (StringBuilder builder, ushort value, bool upperCase = true)
+ {
+ if (builder == null)
+ throw new ArgumentNullException (nameof (builder));
+
+ builder.Append (GetHexValue (value >> 12, upperCase));
+ builder.Append (GetHexValue ((value >> 8) & 0x0f, upperCase));
+ builder.Append (GetHexValue ((value >> 4) & 0x0f, upperCase));
+ builder.Append (GetHexValue (value & 0x0f, upperCase));
+ }
+
///
/// Convert to a hexadecimal string, without allocating
/// intermediate strings.
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
index 29e8d56742a..f65be27397d 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -65,6 +65,7 @@ public void Generate (JavaPeerInfo type, TextWriter writer, string? applicationJ
{
writer.NewLine = "\n";
WritePackageDeclaration (type, writer);
+ WriteAnnotations (type.Annotations, writer);
WriteClassDeclaration (type, writer, applicationJavaClass);
WriteStaticInitializer (type, writer);
WriteConstructors (type, writer);
@@ -176,6 +177,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams);
string args = FormatArgumentList (ctorParams);
+ WriteAnnotations (ctor.Annotations, writer);
writer.Write ($$"""
public {{simpleClassName}} ({{parameters}})
{
@@ -212,6 +214,7 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
static void WriteFields (JavaPeerInfo type, TextWriter writer)
{
foreach (var field in type.JavaFields) {
+ WriteAnnotations (field.Annotations, writer);
writer.Write ('\t');
writer.Write (field.Visibility);
writer.Write (' ');
@@ -257,8 +260,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
}
if (method.Connector != null && !method.IsExport) {
+ writer.WriteLine ();
+ WriteAnnotations (method.Annotations, writer);
writer.Write ($$"""
-
@Override
public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
{
@@ -270,8 +274,9 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
} else {
string access = method.IsExport && method.JavaAccess != null ? method.JavaAccess : "public";
string staticKeyword = method.IsStatic ? "static " : "";
+ writer.WriteLine ();
+ WriteAnnotations (method.Annotations, writer);
writer.Write ($$"""
-
{{access}} {{staticKeyword}}{{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
{
{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
@@ -283,6 +288,29 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
}
}
+ static void WriteAnnotations (IReadOnlyList annotations, TextWriter writer)
+ {
+ foreach (var annotation in annotations) {
+ writer.Write ('@');
+ writer.Write (annotation.Name);
+ if (annotation.Properties.Count > 0) {
+ writer.Write (" (");
+ bool first = true;
+ foreach (var property in annotation.Properties) {
+ if (!first) {
+ writer.Write (", ");
+ }
+ writer.Write (property.Key);
+ writer.Write (" = ");
+ writer.Write (property.Value);
+ first = false;
+ }
+ writer.Write (')');
+ }
+ writer.WriteLine ();
+ }
+ }
+
static void WriteGCUserPeerMethods (TextWriter writer)
{
writer.Write ("""
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
index f16e61eb2cc..7628ad984ac 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -678,6 +678,7 @@ sealed record ExportInfo
{
public IReadOnlyList? ThrownNames { get; init; }
public string? SuperArgumentsString { get; init; }
+ public bool IsField { get; init; }
public IReadOnlyList ParameterKinds { get; init; } = [];
public ExportParameterKindInfo ReturnKind { get; init; }
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs
new file mode 100644
index 00000000000..8ce19000586
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaAnnotationParser.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection.Metadata;
+using System.Text;
+using Microsoft.Android.Build.Tasks;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+sealed class JavaAnnotationParser
+{
+ sealed record AnnotationTypeInfo (string JavaName, IReadOnlyDictionary PropertyNames);
+
+ static readonly IReadOnlyList noAnnotations = [];
+
+ readonly IReadOnlyDictionary assemblies;
+ readonly Func resolveTypeName;
+ readonly Dictionary<(AssemblyIndex Index, EntityHandle Type), AnnotationTypeInfo?> annotationTypes = new ();
+
+ public JavaAnnotationParser (IReadOnlyDictionary assemblies, Func resolveTypeName)
+ {
+ this.assemblies = assemblies;
+ this.resolveTypeName = resolveTypeName;
+ }
+
+ public IReadOnlyList Parse (CustomAttributeHandleCollection attributes, AssemblyIndex index)
+ {
+ List? annotations = null;
+ foreach (var attributeHandle in attributes) {
+ var attribute = index.Reader.GetCustomAttribute (attributeHandle);
+ var annotationType = GetAnnotationType (attribute, index);
+ if (annotationType is null) {
+ continue;
+ }
+
+ annotations ??= [];
+ annotations.Add (new JavaAnnotationInfo {
+ Name = annotationType.JavaName,
+ Properties = GetProperties (attribute, index, annotationType),
+ });
+ }
+ return annotations ?? noAnnotations;
+ }
+
+ static string? GetJavaName (TypeDefinition attributeType, AssemblyIndex index)
+ {
+ foreach (var markerHandle in attributeType.GetCustomAttributes ()) {
+ var marker = index.Reader.GetCustomAttribute (markerHandle);
+ if (!AssemblyIndex.IsCustomAttributeMatch (marker, index.Reader, "Android.Runtime", "AnnotationAttribute")) {
+ continue;
+ }
+
+ var value = index.DecodeAttribute (marker);
+ return value.FixedArguments.Length > 0 ? value.FixedArguments [0].Value as string : null;
+ }
+ return null;
+ }
+
+ IReadOnlyList> GetProperties (
+ CustomAttribute attribute,
+ AssemblyIndex index,
+ AnnotationTypeInfo annotationType)
+ {
+ var properties = new List> ();
+ foreach (var property in index.DecodeAttribute (attribute).NamedArguments) {
+ if (property.Kind != CustomAttributeNamedArgumentKind.Property || property.Name is null) {
+ continue;
+ }
+ var propertyName = annotationType.PropertyNames.TryGetValue (property.Name, out var javaName)
+ ? javaName
+ : property.Name;
+ properties.Add (new KeyValuePair (
+ propertyName,
+ ManagedValueToJavaSource (property.Type, property.Value)
+ ));
+ }
+ return properties;
+ }
+
+ static IReadOnlyDictionary GetJavaPropertyNames (TypeDefinition attributeType, AssemblyIndex index)
+ {
+ var names = new Dictionary (StringComparer.Ordinal);
+ foreach (var propertyHandle in attributeType.GetProperties ()) {
+ var property = index.Reader.GetPropertyDefinition (propertyHandle);
+ var managedName = index.Reader.GetString (property.Name);
+ foreach (var attributeHandle in property.GetCustomAttributes ()) {
+ var attribute = index.Reader.GetCustomAttribute (attributeHandle);
+ if (!AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Android.Runtime", "RegisterAttribute")) {
+ continue;
+ }
+ var value = index.DecodeAttribute (attribute);
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string javaName) {
+ names [managedName] = javaName;
+ }
+ break;
+ }
+ }
+ return names;
+ }
+
+ AnnotationTypeInfo? GetAnnotationType (CustomAttribute attribute, AssemblyIndex index)
+ {
+ EntityHandle typeHandle = default;
+ if (attribute.Constructor.Kind == HandleKind.MethodDefinition) {
+ typeHandle = index.Reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType ();
+ } else if (attribute.Constructor.Kind == HandleKind.MemberReference) {
+ typeHandle = index.Reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor).Parent;
+ }
+
+ var key = (index, typeHandle);
+ if (typeHandle.IsNil || annotationTypes.TryGetValue (key, out var cached) && cached is null) {
+ return null;
+ }
+ if (cached is not null) {
+ return cached;
+ }
+
+ TypeDefinition attributeType;
+ AssemblyIndex attributeIndex;
+ if (typeHandle.Kind == HandleKind.TypeDefinition) {
+ attributeType = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)typeHandle);
+ attributeIndex = index;
+ } else if (typeHandle.Kind == HandleKind.TypeReference) {
+ var typeReference = MetadataTypeNameResolver.GetTypeRefFromReference (
+ index.Reader,
+ (TypeReferenceHandle)typeHandle,
+ index.AssemblyName,
+ rawTypeKind: 0
+ );
+ if (!assemblies.TryGetValue (typeReference.AssemblyName, out attributeIndex) ||
+ !attributeIndex.TypesByFullName.TryGetValue (typeReference.ManagedTypeName, out var resolvedHandle)) {
+ annotationTypes [key] = null;
+ return null;
+ }
+ attributeType = attributeIndex.Reader.GetTypeDefinition (resolvedHandle);
+ } else {
+ annotationTypes [key] = null;
+ return null;
+ }
+
+ var javaName = GetJavaName (attributeType, attributeIndex);
+ var result = javaName.IsNullOrEmpty ()
+ ? null
+ : new AnnotationTypeInfo (javaName, GetJavaPropertyNames (attributeType, attributeIndex));
+ annotationTypes [key] = result;
+ return result;
+ }
+
+ string ManagedValueToJavaSource (string managedType, object? value)
+ {
+ if (value is null) {
+ return "null";
+ }
+ if (managedType == "String" || managedType == "System.String") {
+ return ToJavaStringLiteral (value.ToString () ?? "");
+ }
+ if (managedType == "System.Type" && value is string typeName) {
+ var javaName = resolveTypeName (typeName);
+ if (javaName is not null) {
+ return JniSignatureHelper.JniNameToJavaName (javaName) + ".class";
+ }
+ throw new InvalidOperationException ($"Java annotation type value '{typeName}' does not resolve to a Java peer.");
+ }
+ if (value is bool boolean) {
+ return boolean ? "true" : "false";
+ }
+ if (value is IFormattable formattable) {
+ return formattable.ToString (null, CultureInfo.InvariantCulture) ?? "";
+ }
+ return value.ToString () ?? "";
+ }
+
+ static string ToJavaStringLiteral (string value)
+ {
+ var builder = new StringBuilder (value.Length + 2);
+ builder.Append ('"');
+ foreach (char c in value) {
+ switch (c) {
+ case '"':
+ builder.Append ("\\\"");
+ break;
+ case '\\':
+ builder.Append ("\\\\");
+ break;
+ case '\b':
+ builder.Append ("\\b");
+ break;
+ case '\t':
+ builder.Append ("\\t");
+ break;
+ case '\n':
+ builder.Append ("\\n");
+ break;
+ case '\f':
+ builder.Append ("\\f");
+ break;
+ case '\r':
+ builder.Append ("\\r");
+ break;
+ default:
+ if (char.IsControl (c)) {
+ builder.Append ("\\u");
+ HexUtilities.WriteHex (builder, c, upperCase: false);
+ } else {
+ builder.Append (c);
+ }
+ break;
+ }
+ }
+ builder.Append ('"');
+ return builder.ToString ();
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 5f591978de3..1dce5a4e48c 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -63,6 +63,12 @@ public sealed record JavaPeerInfo
///
public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty ();
+ ///
+ /// Java annotations forwarded from managed custom attributes decorated with
+ /// Android.Runtime.AnnotationAttribute.
+ ///
+ public IReadOnlyList Annotations { get; init; } = [];
+
public bool IsInterface { get; init; }
public bool IsAbstract { get; init; }
@@ -296,6 +302,20 @@ public sealed record MarshalMethodInfo
/// new virtual while reusing the same JNI name and signature.
///
public bool CallManagedMethodDirectly { get; init; }
+
+ ///
+ /// Java annotations forwarded from the managed method or constructor.
+ ///
+ public IReadOnlyList Annotations { get; init; } = [];
+}
+
+///
+/// Describes a Java annotation forwarded from a managed custom attribute.
+///
+public sealed record JavaAnnotationInfo
+{
+ public required string Name { get; init; }
+ public IReadOnlyList> Properties { get; init; } = [];
}
///
@@ -344,6 +364,11 @@ public sealed record JavaConstructorInfo
/// True when this Java constructor has a matching public managed constructor on the target type.
///
public bool HasMatchingManagedCtor { get; init; }
+
+ ///
+ /// Java annotations forwarded from the managed constructor.
+ ///
+ public IReadOnlyList Annotations { get; init; } = [];
}
///
@@ -376,6 +401,11 @@ public sealed record JavaFieldInfo
/// Whether the field is static.
///
public bool IsStatic { get; init; }
+
+ ///
+ /// Java annotations forwarded from the managed field initializer method.
+ ///
+ public IReadOnlyList Annotations { get; init; } = [];
}
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index a9b28686132..0376a54c98b 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -33,6 +33,7 @@ enum HashedPackageNamingPolicy {
readonly HashedPackageNamingPolicy packageNamingPolicy;
readonly HashSet frameworkAssemblyNames;
readonly bool errorOnCustomJavaObject;
+ readonly JavaAnnotationParser annotationParser;
public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = true)
{
@@ -40,6 +41,7 @@ public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLog
this.logger = logger;
this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet (StringComparer.OrdinalIgnoreCase);
this.errorOnCustomJavaObject = errorOnCustomJavaObject;
+ annotationParser = new JavaAnnotationParser (assemblyCache, ResolveTypeOfArgumentToJniName);
}
///
@@ -385,6 +387,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
IsFrameworkAssembly = frameworkAssemblyNames.Contains (index.AssemblyName),
BaseJavaName = baseJavaName,
ImplementedInterfaceJavaNames = implementedInterfaces,
+ Annotations = annotationParser.Parse (typeDef.GetCustomAttributes (), index),
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
@@ -818,7 +821,7 @@ void CollectBasePropertyOverrides (TypeDefinition typeDef, AssemblyIndex index,
continue;
}
- var baseRegistration = FindBaseRegisteredProperty (typeDef, index, getterName, getterDef);
+ var baseRegistration = FindBaseRegisteredProperty (typeDef, index, getterName, getterDef, index);
if (baseRegistration is not null) {
methods.Add (baseRegistration);
alreadyRegistered.Add (sigKey);
@@ -961,6 +964,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index,
ManagedMethodName = ".ctor",
NativeCallbackName = "n_ctor",
IsConstructor = true,
+ Annotations = annotationParser.Parse (baseCtor.Method.GetCustomAttributes (), baseCtor.Index),
});
alreadyRegisteredSignatures.Add (signature);
}
@@ -1394,6 +1398,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
DeclaringTypeName = result.Value.DeclaringType.ManagedTypeName,
DeclaringAssemblyName = result.Value.DeclaringType.AssemblyName,
DeclaringType = result.Value.DeclaringType,
+ Annotations = annotationParser.Parse (derivedMethod.GetCustomAttributes (), index),
};
}
@@ -1402,7 +1407,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
/// matches the given getter name and has a compatible signature.
///
MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index,
- string getterName, MethodDefinition derivedGetter, TypeRefData? currentTypeRef = null)
+ string getterName, MethodDefinition derivedGetter, AssemblyIndex derivedIndex, TypeRefData? currentTypeRef = null)
{
if (!TryResolveBaseType (typeDef, index, currentTypeRef, out var baseTypeDef, out _, out var baseIndex, out _, out _, out var baseTypeRef)) {
return null;
@@ -1446,13 +1451,14 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
DeclaringTypeName = baseTypeRef.ManagedTypeName,
DeclaringAssemblyName = baseTypeRef.AssemblyName,
DeclaringType = baseTypeRef,
+ Annotations = annotationParser.Parse (derivedGetter.GetCustomAttributes (), derivedIndex),
};
}
}
// Keep walking the full base hierarchy so property overrides can inherit
// [Register] metadata declared above an intermediate MCW base type.
- return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter, baseTypeRef);
+ return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter, derivedIndex, baseTypeRef);
}
///
@@ -1554,6 +1560,7 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf
ThrownNames = exportInfo?.ThrownNames,
SuperArgumentsString = exportInfo?.SuperArgumentsString,
CallManagedMethodDirectly = callManagedMethodDirectly,
+ Annotations = exportInfo?.IsField == true ? [] : annotationParser.Parse (methodDef.GetCustomAttributes (), index),
});
}
@@ -1878,7 +1885,7 @@ string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnly
return (
new RegisterInfo { JniName = managedName, Signature = jniSig, Connector = "__export__", DoNotGenerateAcw = false },
- new ExportInfo { ThrownNames = null, SuperArgumentsString = null }
+ new ExportInfo { ThrownNames = null, SuperArgumentsString = null, IsField = true }
);
}
@@ -2481,6 +2488,7 @@ List BuildJavaConstructors (List marshal
SuperArgumentsString = mm.SuperArgumentsString,
HasMatchingManagedCtor = managedParams != null,
ManagedParameterTypes = managedParams ?? [],
+ Annotations = mm.Annotations,
});
ctorIndex++;
}
@@ -2592,6 +2600,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List (() => HexUtilities.WriteHex (writer: null, value: 0x00));
}
+ [Test]
+ public void WriteHex_StringBuilder_UInt16 ()
+ {
+ var builder = new StringBuilder ("_");
+ HexUtilities.WriteHex (builder, 0xabcd);
+ HexUtilities.WriteHex (builder, 0x012f, upperCase: false);
+ Assert.AreEqual ("_ABCD012f", builder.ToString ());
+ }
+
+ [Test]
+ public void WriteHex_StringBuilder_NullThrows ()
+ {
+ Assert.Throws (() => HexUtilities.WriteHex (builder: null, value: 0x0000));
+ }
+
[TestCase (0)]
[TestCase (1)]
public void WriteHex_Span_TooShortThrows (int length)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
index d824733c2f2..9f4c2460155 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -22,15 +22,27 @@ private protected static string TestFixtureAssemblyPath {
}
}
+ static string TestAttributeFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)
+ ?? throw new InvalidOperationException ("Cannot determine test assembly directory");
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestAttributeFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestAttributeFixtures.dll not found at {fixtureAssembly}. Ensure the TestAttributeFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => {
using var scanner = new JavaPeerScanner ();
- var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
- var mdReader = peReader.GetMetadataReader ();
- var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name);
- var assemblies = new [] { (assemblyName, peReader) };
+ using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
+ using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath));
+ var assemblies = new [] {
+ GetAssemblyInput (peReader),
+ GetAssemblyInput (attributePeReader),
+ };
var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
- peReader.Dispose ();
return (peers, manifestInfo);
});
@@ -40,12 +52,20 @@ private protected static List ScanFixtures (string packageNamingPo
{
using var scanner = new JavaPeerScanner (packageNamingPolicy);
using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
- var mdReader = peReader.GetMetadataReader ();
- var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name);
- var assemblies = new [] { (assemblyName, peReader) };
+ using var attributePeReader = new PEReader (File.OpenRead (TestAttributeFixtureAssemblyPath));
+ var assemblies = new [] {
+ GetAssemblyInput (peReader),
+ GetAssemblyInput (attributePeReader),
+ };
return scanner.Scan (assemblies);
}
+ static (string Name, PEReader Reader) GetAssemblyInput (PEReader peReader)
+ {
+ var reader = peReader.GetMetadataReader ();
+ return (reader.GetString (reader.GetAssemblyDefinition ().Name), peReader);
+ }
+
private protected static AssemblyManifestInfo ScanAssemblyManifestInfo () => _cachedScanResult.Value.manifestInfo;
private protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
index 055b3c399e7..1e2351626f1 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using Xunit;
@@ -347,6 +348,53 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration ()
AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java);
}
+ [Fact]
+ public void Generate_MarshalMethod_ForwardsJavaAnnotation ()
+ {
+ var peer = FindFixtureByManagedName ("MyApp.MyWebViewHandler");
+ var java = GenerateToString (peer);
+
+ AssertContainsLine ("@android.webkit.JavascriptInterface\n\t@Override\n\tpublic void postMessage (java.lang.String p0)\n", java);
+ }
+
+ [Fact]
+ public void Generate_ForwardsAnnotationsOnJcwMembers ()
+ {
+ var typeJava = GenerateFixture ("my/app/MyHelper");
+ AssertContainsLine ("@com.example.Custom (text = \"say \\\"hi\\\"\\\\path\\n\", Enabled = true, Number = 1.5)\npublic class MyHelper\n", typeJava);
+
+ var constructorJava = GenerateFixture ("my/app/CustomView");
+ AssertContainsLine ("@com.example.Custom\n\tpublic CustomView ()\n", constructorJava);
+
+ var fieldJava = GenerateFixture ("my/app/ExportFieldExample");
+ AssertContainsLine ("@com.example.Custom\n\tpublic java.lang.String VALUE = GetValue ();\n", fieldJava);
+ Assert.Equal (1, fieldJava.Split ("@com.example.Custom").Length - 1);
+ }
+
+ [Fact]
+ public void Generate_AnnotationValues_UseInvariantCulture ()
+ {
+ var originalCulture = CultureInfo.CurrentCulture;
+ try {
+ CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo ("fr-FR");
+ var peer = FindFixtureByManagedName ("MyApp.MyHelper", "Crc64");
+ var java = GenerateToString (peer);
+
+ AssertContainsLine ("Number = 1.5", java);
+ } finally {
+ CultureInfo.CurrentCulture = originalCulture;
+ }
+ }
+
+ [Fact]
+ public void Generate_PropertyOverride_ForwardsGetterAnnotation ()
+ {
+ var peer = FindFixtureByManagedName ("MyApp.AnnotatedPropertyDerived");
+ var java = GenerateToString (peer);
+
+ AssertContainsLine ("@com.example.Custom\n\t@Override\n\tpublic int getValue ()\n", java);
+ }
+
[Fact]
public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub ()
{
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
index 212fa1be735..f42e38eb363 100644
--- 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
@@ -13,6 +13,7 @@
+
@@ -23,6 +24,9 @@
+
+ false
+
false
@@ -33,6 +37,7 @@
<_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" />
+ <_TestFixtureFiles Include="TestAttributeFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestAttributeFixtures.dll" />
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs
new file mode 100644
index 00000000000..ff3a6d6ddcd
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/Attributes.cs
@@ -0,0 +1,65 @@
+using System;
+
+namespace Java.Interop
+{
+ public interface IJniNameProviderAttribute
+ {
+ string Name { get; }
+ }
+}
+
+namespace Android.Runtime
+{
+ [AttributeUsage (AttributeTargets.Class)]
+ public sealed class AnnotationAttribute : Attribute
+ {
+ public string JavaName { get; }
+
+ public AnnotationAttribute (string javaName) => JavaName = javaName;
+ }
+
+ [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;
+ }
+ }
+}
+
+namespace Android.Webkit
+{
+ [Android.Runtime.Annotation ("android.webkit.JavascriptInterface")]
+ [AttributeUsage (AttributeTargets.Method)]
+ public sealed class JavascriptInterfaceAttribute : Attribute
+ {
+ }
+}
+
+namespace MyApp
+{
+ [Android.Runtime.Annotation ("com.example.Custom")]
+ [AttributeUsage (AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)]
+ public sealed class JavaAnnotationAttribute : Attribute
+ {
+ [Android.Runtime.Register ("text")]
+ public string? Text { get; set; }
+
+ public bool Enabled { get; set; }
+ public double Number { get; set; }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj
new file mode 100644
index 00000000000..b089adb3218
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestAttributeFixtures/TestAttributeFixtures.csproj
@@ -0,0 +1,10 @@
+
+
+
+ $(DotNetStableTargetFramework)
+ latest
+ enable
+ false
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
index 050741c3e20..5f8a694b111 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
@@ -1,37 +1,7 @@
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,
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj
index f7f4c72139b..c5f61f8238c 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj
@@ -10,4 +10,8 @@
true
+
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index ceedbc96fe6..c95ea7d5587 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -4,6 +4,7 @@
using Android.App;
using Android.Content;
using Android.Runtime;
+using Android.Webkit;
namespace Java.Lang
{
@@ -275,12 +276,41 @@ public MainActivity () { }
}
[Register ("my/app/MyHelper")]
+ [JavaAnnotation (Text = "say \"hi\"\\path\n", Enabled = true, Number = 1.5)]
public class MyHelper : Java.Lang.Object
{
[Register ("doSomething", "()V", "GetDoSomethingHandler")]
public virtual void DoSomething () { }
}
+ [Register ("my/app/WebViewHandlerBase")]
+ public abstract class WebViewHandlerBase : Java.Lang.Object
+ {
+ [Register ("postMessage", "(Ljava/lang/String;)V", "GetPostMessage_Ljava_lang_String_Handler")]
+ public abstract void PostMessage (string? message);
+ }
+
+ public class MyWebViewHandler : WebViewHandlerBase
+ {
+ [JavascriptInterface]
+ public override void PostMessage (string? message) { }
+ }
+
+ [Register ("my/app/AnnotatedPropertyBase", DoNotGenerateAcw = true)]
+ public class AnnotatedPropertyBase : Java.Lang.Object
+ {
+ [Register ("getValue", "()I", "GetGetValueHandler")]
+ public virtual int Value => 0;
+ }
+
+ public class AnnotatedPropertyDerived : AnnotatedPropertyBase
+ {
+ public override int Value {
+ [JavaAnnotation]
+ get => 1;
+ }
+ }
+
// Fixture for the trimmable typemap's [JniAddNativeMethodRegistrationAttribute] detection.
// The trimmable typemap deliberately does not support this attribute (XA4251).
[Register ("my/app/HandWrittenNativeRegistrationPeer", DoNotGenerateAcw = true)]
@@ -350,6 +380,7 @@ public class CustomView : Android.Views.View
protected CustomView (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
[Register ("", "()V", "")]
+ [JavaAnnotation]
public CustomView () : base (default, default) { }
[Register ("", "(Landroid/content/Context;)V", "")]
@@ -583,6 +614,7 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base
public static ExportFieldExample? GetInstance () => default;
[Java.Interop.ExportField ("VALUE")]
+ [JavaAnnotation]
public string GetValue () => "";
}