diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
new file mode 100644
index 00000000000..8a0d02b49ac
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates JCW (Java Callable Wrapper) .java source files from scanned records.
+/// Only processes ACW types (where is false).
+///
+///
+/// Each generated .java file looks like this (pseudo-Java):
+///
+/// package com.example;
+///
+/// public class MainActivity
+/// extends android.app.Activity
+/// implements
+/// mono.android.IGCUserPeer,
+/// android.view.View.OnClickListener
+/// {
+/// static {
+/// mono.android.Runtime.registerNatives (MainActivity.class);
+/// }
+///
+/// public MainActivity (android.content.Context p0)
+/// {
+/// super (p0);
+/// if (getClass () == MainActivity.class) nctor_0 (p0);
+/// }
+/// private native void nctor_0 (android.content.Context p0);
+///
+/// @Override
+/// public void onCreate (android.os.Bundle p0)
+/// {
+/// n_onCreate (p0);
+/// }
+/// public native void n_onCreate (android.os.Bundle p0);
+/// }
+///
+///
+sealed class JcwJavaSourceGenerator
+{
+ ///
+ /// Generates .java source files for all ACW types and writes them to the output directory.
+ /// Returns the list of generated file paths.
+ ///
+ public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory)
+ {
+ if (types is null) {
+ throw new ArgumentNullException (nameof (types));
+ }
+ if (outputDirectory is null) {
+ throw new ArgumentNullException (nameof (outputDirectory));
+ }
+
+ var generatedFiles = new List ();
+
+ foreach (var type in types) {
+ if (type.DoNotGenerateAcw || type.IsInterface) {
+ continue;
+ }
+
+ string filePath = GetOutputFilePath (type, outputDirectory);
+ string? dir = Path.GetDirectoryName (filePath);
+ if (dir != null) {
+ Directory.CreateDirectory (dir);
+ }
+
+ using var writer = new StreamWriter (filePath);
+ Generate (type, writer);
+ generatedFiles.Add (filePath);
+ }
+
+ return generatedFiles;
+ }
+
+ ///
+ /// Generates a single .java source file for the given type.
+ ///
+ internal void Generate (JavaPeerInfo type, TextWriter writer)
+ {
+ writer.NewLine = "\n";
+ WritePackageDeclaration (type, writer);
+ WriteClassDeclaration (type, writer);
+ WriteStaticInitializer (type, writer);
+ WriteConstructors (type, writer);
+ WriteMethods (type, writer);
+ WriteGCUserPeerMethods (writer);
+ WriteClassClose (writer);
+ }
+
+ static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
+ {
+ JniSignatureHelper.ValidateJniName (type.JavaName);
+ string relativePath = type.JavaName + ".java";
+ return Path.Combine (outputDirectory, relativePath);
+ }
+
+ ///
+ /// Validates that the JNI name is well-formed: non-empty, each segment separated by '/'
+ /// contains only valid Java identifier characters (letters, digits, '_', '$').
+ /// This also prevents path traversal (e.g., ".." segments, rooted paths, backslashes).
+ ///
+ static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ string? package = JniSignatureHelper.GetJavaPackageName (type.JavaName);
+ if (package != null) {
+ writer.Write ("package ");
+ writer.Write (package);
+ writer.WriteLine (';');
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ string abstractModifier = type.IsAbstract && !type.IsInterface ? "abstract " : "";
+ string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+
+ writer.Write ($"public {abstractModifier}class {className}\n");
+
+ // extends clause
+ if (type.BaseJavaName != null) {
+ writer.WriteLine ($"\textends {JniSignatureHelper.JniNameToJavaName (type.BaseJavaName)}");
+ }
+
+ // implements clause — always includes IGCUserPeer, plus any implemented interfaces
+ writer.Write ("\timplements\n\t\tmono.android.IGCUserPeer");
+
+ foreach (var iface in type.ImplementedInterfaceJavaNames) {
+ writer.Write ($",\n\t\t{JniSignatureHelper.JniNameToJavaName (iface)}");
+ }
+
+ writer.WriteLine ();
+ writer.WriteLine ('{');
+ }
+
+ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
+ {
+ string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+ writer.Write ($$"""
+ static {
+ mono.android.Runtime.registerNatives ({{className}}.class);
+ }
+
+
+""");
+ }
+
+ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
+ {
+ string simpleClassName = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+
+ foreach (var ctor in type.JavaConstructors) {
+ string parameters = FormatParameterList (ctor.Parameters);
+ string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctor.Parameters);
+ string args = FormatArgumentList (ctor.Parameters);
+
+ writer.Write ($$"""
+ public {{simpleClassName}} ({{parameters}})
+ {
+ super ({{superArgs}});
+ if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}});
+ }
+
+
+""");
+ }
+
+ // Write native constructor declarations
+ foreach (var ctor in type.JavaConstructors) {
+ string parameters = FormatParameterList (ctor.Parameters);
+ writer.WriteLine ($"\tprivate native void nctor_{ctor.ConstructorIndex} ({parameters});");
+ }
+
+ if (type.JavaConstructors.Count > 0) {
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
+ {
+ foreach (var method in type.MarshalMethods) {
+ if (method.IsConstructor) {
+ continue;
+ }
+
+ string javaReturnType = JniSignatureHelper.JniTypeToJava (method.JniReturnType);
+ bool isVoid = method.JniReturnType == "V";
+ string parameters = FormatParameterList (method.Parameters);
+ string args = FormatArgumentList (method.Parameters);
+ string returnPrefix = isVoid ? "" : "return ";
+
+ // throws clause for [Export] methods
+ string throwsClause = "";
+ if (method.ThrownNames != null && method.ThrownNames.Count > 0) {
+ throwsClause = $"\n\t\tthrows {string.Join (", ", method.ThrownNames)}";
+ }
+
+ if (method.Connector != null) {
+ writer.Write ($$"""
+
+ @Override
+ public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
+ {
+ {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
+ }
+ public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
+
+""");
+ } else {
+ writer.Write ($$"""
+
+ public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
+ {
+ {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
+ }
+ public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
+
+""");
+ }
+ }
+ }
+
+ static void WriteGCUserPeerMethods (TextWriter writer)
+ {
+ writer.Write ("""
+
+ private java.util.ArrayList refList;
+ public void monodroidAddReference (java.lang.Object obj)
+ {
+ if (refList == null)
+ refList = new java.util.ArrayList ();
+ refList.add (obj);
+ }
+
+ public void monodroidClearReferences ()
+ {
+ if (refList != null)
+ refList.clear ();
+ }
+
+""");
+ }
+
+ static void WriteClassClose (TextWriter writer)
+ {
+ writer.WriteLine ('}');
+ }
+
+ static string FormatParameterList (IReadOnlyList parameters)
+ {
+ if (parameters.Count == 0) {
+ return "";
+ }
+
+ var sb = new System.Text.StringBuilder ();
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ sb.Append (", ");
+ }
+ sb.Append (JniSignatureHelper.JniTypeToJava (parameters [i].JniType));
+ sb.Append (" p");
+ sb.Append (i);
+ }
+ return sb.ToString ();
+ }
+
+ static string FormatArgumentList (IReadOnlyList parameters)
+ {
+ if (parameters.Count == 0) {
+ return "";
+ }
+
+ var sb = new System.Text.StringBuilder ();
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ sb.Append (", ");
+ }
+ sb.Append ('p');
+ sb.Append (i);
+ }
+ return sb.ToString ();
+ }
+
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
index 0e2b867c4f9..0e9ac52f93e 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -123,4 +123,109 @@ public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kin
default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type");
}
}
+
+ ///
+ /// Validates that a JNI type name has the expected structure (e.g., "com/example/MyClass").
+ ///
+ internal static void ValidateJniName (string jniName)
+ {
+ if (string.IsNullOrEmpty (jniName)) {
+ throw new ArgumentException ("JNI name must not be null or empty.", nameof (jniName));
+ }
+
+ int segmentStart = 0;
+ for (int i = 0; i <= jniName.Length; i++) {
+ if (i == jniName.Length || jniName [i] == '/') {
+ if (i == segmentStart) {
+ throw new ArgumentException ($"JNI name '{jniName}' has an empty segment.", nameof (jniName));
+ }
+
+ // First char of a segment must not be a digit
+ char first = jniName [segmentStart];
+ if (first >= '0' && first <= '9') {
+ throw new ArgumentException ($"JNI name '{jniName}' has a segment starting with a digit.", nameof (jniName));
+ }
+
+ // All chars in the segment must be valid Java identifier chars
+ for (int j = segmentStart; j < i; j++) {
+ char c = jniName [j];
+ bool valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '_' || c == '$';
+ if (!valid) {
+ throw new ArgumentException ($"JNI name '{jniName}' contains invalid character '{c}'.", nameof (jniName));
+ }
+ }
+
+ segmentStart = i + 1;
+ }
+ }
+ }
+
+ ///
+ /// Converts a JNI type name to a Java source type name.
+ /// e.g., "android/app/Activity" \u2192 "android.app.Activity"
+ ///
+ internal static string JniNameToJavaName (string jniName)
+ {
+ return jniName.Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the Java package name from a JNI type name.
+ /// e.g., "com/example/MainActivity" \u2192 "com.example"
+ /// Returns null for types without a package.
+ ///
+ internal static string? GetJavaPackageName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ if (lastSlash < 0) {
+ return null;
+ }
+ return jniName.Substring (0, lastSlash).Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the simple Java class name from a JNI type name.
+ /// e.g., "com/example/MainActivity" \u2192 "MainActivity"
+ /// e.g., "com/example/Outer$Inner" \u2192 "Outer$Inner" (preserves nesting separator)
+ ///
+ internal static string GetJavaSimpleName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName;
+ }
+
+ ///
+ /// Converts a JNI type descriptor to a Java source type.
+ /// e.g., "V" \u2192 "void", "I" \u2192 "int", "Landroid/os/Bundle;" \u2192 "android.os.Bundle"
+ ///
+ internal static string JniTypeToJava (string jniType)
+ {
+ if (jniType.Length == 1) {
+ return jniType [0] switch {
+ 'V' => "void",
+ 'Z' => "boolean",
+ 'B' => "byte",
+ 'C' => "char",
+ 'S' => "short",
+ 'I' => "int",
+ 'J' => "long",
+ 'F' => "float",
+ 'D' => "double",
+ _ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"),
+ };
+ }
+
+ // Array types: "[I" \u2192 "int[]", "[Ljava/lang/String;" \u2192 "java.lang.String[]"
+ if (jniType [0] == '[') {
+ return JniTypeToJava (jniType.Substring (1)) + "[]";
+ }
+
+ // Object types: "Landroid/os/Bundle;" \u2192 "android.os.Bundle"
+ if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') {
+ return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2));
+ }
+
+ throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}");
+ }
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index e76c3810ea7..e8d0c5d6ba3 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -43,6 +43,19 @@ sealed record JavaPeerInfo
///
public required string AssemblyName { get; init; }
+ ///
+ /// JNI name of the base Java type, e.g., "android/app/Activity" for a type
+ /// that extends Activity. Null for java/lang/Object or types without a Java base.
+ /// Needed by JCW Java source generation ("extends" clause).
+ ///
+ public string? BaseJavaName { get; init; }
+
+ ///
+ /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"].
+ /// Needed by JCW Java source generation ("implements" clause).
+ ///
+ public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty ();
+
public bool IsInterface { get; init; }
public bool IsAbstract { get; init; }
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index ca9057ed747..eabc73f7ee9 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -197,6 +197,12 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
var isUnconditional = attrInfo is not null;
string? invokerTypeName = null;
+ // Resolve base Java type name
+ var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
+
+ // Resolve implemented Java interface names
+ var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index);
+
// Collect marshal methods (including constructors) in a single pass over methods
var marshalMethods = CollectMarshalMethods (typeDef, index);
@@ -215,6 +221,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
ManagedTypeNamespace = ExtractNamespace (fullName),
ManagedTypeShortName = ExtractShortName (fullName),
AssemblyName = index.AssemblyName,
+ BaseJavaName = baseJavaName,
+ ImplementedInterfaceJavaNames = implementedInterfaces,
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
@@ -283,6 +291,51 @@ static void AddMarshalMethod (List methods, RegisterInfo regi
});
}
+ string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
+ {
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo is null) {
+ return null;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ // First try [Register] attribute
+ var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
+ if (registerJniName is not null) {
+ return registerJniName;
+ }
+
+ // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
+ if (results.TryGetValue (baseTypeName, out var basePeer)) {
+ return basePeer.JavaName;
+ }
+
+ return null;
+ }
+
+ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var result = new List ();
+ var interfaceImpls = typeDef.GetInterfaceImplementations ();
+
+ foreach (var implHandle in interfaceImpls) {
+ var impl = index.Reader.GetInterfaceImplementation (implHandle);
+ var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index);
+ if (ifaceJniName is not null) {
+ result.Add (ifaceJniName);
+ }
+ }
+
+ return result;
+ }
+
+ string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
+ {
+ var resolved = ResolveEntityHandle (interfaceHandle, index);
+ return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
+ }
+
static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
{
exportInfo = null;
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
index 4a168b636a9..201db530238 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -33,10 +33,16 @@ protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
return peer;
}
- protected static void CleanUpDir (string path)
+ protected static string CreateTempDir ()
{
- var dir = Path.GetDirectoryName (path);
- if (dir != null && Directory.Exists (dir))
+ var dir = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}");
+ Directory.CreateDirectory (dir);
+ return dir;
+ }
+
+ protected static void DeleteTempDir (string dir)
+ {
+ if (Directory.Exists (dir))
try { Directory.Delete (dir, true); } catch { }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
new file mode 100644
index 00000000000..049f4dbbeed
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class JcwJavaSourceGeneratorTests : FixtureTestBase
+{
+ static string GenerateToString (JavaPeerInfo type)
+ {
+ var generator = new JcwJavaSourceGenerator ();
+ using var writer = new StringWriter ();
+ generator.Generate (type, writer);
+ return writer.ToString ();
+ }
+
+ static string GenerateFixture (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ return GenerateToString (peer);
+ }
+
+
+ public class JniNameConversion
+ {
+
+ [Theory]
+ [InlineData ("android/app/Activity", "android.app.Activity")]
+ [InlineData ("java/lang/Object", "java.lang.Object")]
+ [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")]
+ public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.JniNameToJavaName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity", "com.example")]
+ [InlineData ("java/lang/Object", "java.lang")]
+ [InlineData ("TopLevelClass", null)]
+ public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.GetJavaPackageName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("V", "void")]
+ [InlineData ("Z", "boolean")]
+ [InlineData ("B", "byte")]
+ [InlineData ("I", "int")]
+ [InlineData ("J", "long")]
+ [InlineData ("F", "float")]
+ [InlineData ("D", "double")]
+ [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")]
+ [InlineData ("[I", "int[]")]
+ [InlineData ("[Ljava/lang/String;", "java.lang.String[]")]
+ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.JniTypeToJava (jniType));
+ }
+
+ }
+
+ public class Filtering : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_SkipsMcwTypes ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var files = generator.Generate (peers, _outputDir);
+ Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java"));
+ Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java"));
+ Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java"));
+ }
+
+ }
+
+ public class ClassDeclaration
+ {
+
+ [Fact]
+ public void Generate_MainActivity_HasClassDeclaration ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("public class MainActivity\n", java);
+ Assert.Contains ("\textends android.app.Activity\n", java);
+ Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java);
+ }
+
+ [Fact]
+ public void Generate_MainActivity_HasIGCUserPeerMethods ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("private java.util.ArrayList refList;", java);
+ Assert.Contains ("public void monodroidAddReference (java.lang.Object obj)", java);
+ Assert.Contains ("public void monodroidClearReferences ()", java);
+ }
+
+ [Fact]
+ public void Generate_AbstractType_HasAbstractModifier ()
+ {
+ var java = GenerateFixture ("my/app/AbstractBase");
+ Assert.Contains ("public abstract class AbstractBase\n", java);
+ }
+
+ }
+
+ public class StaticInitializer
+ {
+
+ [Fact]
+ public void Generate_AcwType_HasRegisterNativesStaticBlock ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("static {\n", java);
+ Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java);
+ }
+
+ }
+
+ public class Constructor
+ {
+
+ [Fact]
+ public void Generate_CustomView_HasExpectedConstructorElements ()
+ {
+ var java = GenerateFixture ("my/app/CustomView");
+ Assert.Contains ("public CustomView ()\n", java);
+ Assert.Contains ("public CustomView (android.content.Context p0)\n", java);
+ Assert.Contains ("private native void nctor_0 ();\n", java);
+ Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java);
+ Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs ()
+ {
+ // [Export] constructors with SuperArgumentsString should use it in super() call
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/CustomService",
+ ManagedTypeName = "MyApp.CustomService",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "CustomService",
+ AssemblyName = "App",
+ BaseJavaName = "android/app/Service",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;I)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "I" },
+ },
+ SuperArgumentsString = "p0",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super (p0);", java);
+ Assert.DoesNotContain ("super (p0, p1);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper ()
+ {
+ // Empty string means super() with no arguments
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyWidget",
+ ManagedTypeName = "MyApp.MyWidget",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyWidget",
+ AssemblyName = "App",
+ BaseJavaName = "android/appwidget/AppWidgetProvider",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ },
+ SuperArgumentsString = "",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super ();", java);
+ Assert.DoesNotContain ("super (p0);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams ()
+ {
+ // null SuperArgumentsString means forward all params (default behavior)
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyView",
+ ManagedTypeName = "MyApp.MyView",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyView",
+ AssemblyName = "App",
+ BaseJavaName = "android/view/View",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" },
+ },
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super (p0, p1);", java);
+ }
+
+ }
+
+ public class Method
+ {
+
+ [Fact]
+ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("@Override\n", java);
+ Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java);
+ Assert.Contains ("n_OnCreate (p0);\n", java);
+ Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java);
+ }
+
+ }
+
+ public class NestedType
+ {
+
+ [Fact]
+ public void Generate_NestedType_HasCorrectPackageAndClassName ()
+ {
+ var java = GenerateFixture ("my/app/Outer$Inner");
+ Assert.Contains ("package my.app;\n", java);
+ Assert.Contains ("public class Outer$Inner\n", java);
+ }
+
+ }
+
+ public class OutputFilePath : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_CreatesCorrectFileStructure ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var files = generator.Generate (peers, _outputDir);
+ Assert.NotEmpty (files);
+
+ foreach (var file in files) {
+ Assert.StartsWith (_outputDir, file);
+ Assert.True (File.Exists (file), $"Generated file should exist: {file}");
+ Assert.EndsWith (".java", file);
+ }
+ }
+
+ [Theory]
+ [InlineData ("")]
+ [InlineData ("com//Example")]
+ [InlineData ("/com/Example")]
+ [InlineData ("com/Example/")]
+ [InlineData ("com/1Invalid")]
+ [InlineData ("com/../etc/passwd")]
+ [InlineData ("com\\..\\.\\secret")]
+ [InlineData ("C:\\Windows\\System32")]
+ [InlineData ("com/Ex:ample")]
+ [InlineData ("/absolute/path")]
+ public void Generate_InvalidJniName_Throws (string badJniName)
+ {
+ var peer = MakeAcwPeer (badJniName, "Test.Bad", "TestApp");
+ var generator = new JcwJavaSourceGenerator ();
+ Assert.Throws (() => generator.Generate (new [] { peer }, _outputDir));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity")]
+ [InlineData ("my/app/Outer$Inner")]
+ [InlineData ("SingleSegment")]
+ [InlineData ("com/example/_Private")]
+ [InlineData ("com/example/$Generated")]
+ public void Generate_ValidJniName_DoesNotThrow (string validJniName)
+ {
+ var peer = MakeAcwPeer (validJniName, "Test.Valid", "TestApp");
+ var generator = new JcwJavaSourceGenerator ();
+ generator.Generate (new [] { peer }, _outputDir);
+ }
+
+ }
+
+ public class ExportWithThrowsClause
+ {
+
+ [Fact]
+ public void Generate_ExportWithThrows_HasThrowsClause ()
+ {
+ var java = GenerateFixture ("my/app/ExportWithThrows");
+ Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java);
+ }
+
+ }
+
+ public class MethodReturnTypesAndParams
+ {
+
+ [Fact]
+ public void Generate_TouchHandler_HasExpectedMethodSignatures ()
+ {
+ var java = GenerateFixture ("my/app/TouchHandler");
+ Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java);
+ Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java);
+ Assert.Contains ("public java.lang.String getText ()\n", java);
+ Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index b2282243273..cbb65cc2af2 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -127,65 +127,65 @@ public void Generate_ProxyType_HasCtorAndCreateInstance ()
}
-public class AcwProxy : IDisposable
-{
-readonly string _outputDir = CreateTempDir ();
-public void Dispose () => DeleteTempDir (_outputDir);
+ public class AcwProxy : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
-[Fact]
-public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
-{
-var peers = ScanFixtures ();
-var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
-var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "AcwTest");
-var (pe, reader) = OpenAssembly (path);
-using (pe) {
-var proxy = reader.TypeDefinitions
-.Select (h => reader.GetTypeDefinition (h))
-.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
-
-var methods = proxy.GetMethods ()
-.Select (h => reader.GetMethodDefinition (h))
-.Select (m => reader.GetString (m.Name))
-.ToList ();
-
-Assert.Contains ("RegisterNatives", methods);
-Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0"));
-}
-}
-
-[Fact]
-public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute ()
-{
-var peers = ScanFixtures ();
-var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
-var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "UcoTest");
-var (pe, reader) = OpenAssembly (path);
-using (pe) {
-var proxy = reader.TypeDefinitions
-.Select (h => reader.GetTypeDefinition (h))
-.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
-
-var ucoMethod = proxy.GetMethods ()
-.Select (h => reader.GetMethodDefinition (h))
-.First (m => reader.GetString (m.Name).Contains ("_uco_"));
-
-var attrNames = ucoMethod.GetCustomAttributes ()
-.Select (h => reader.GetCustomAttribute (h))
-.Select (a => {
-var ctorHandle = (MemberReferenceHandle) a.Constructor;
-var ctor = reader.GetMemberReference (ctorHandle);
-var typeRef = reader.GetTypeReference ((TypeReferenceHandle) ctor.Parent);
-return $"{reader.GetString (typeRef.Namespace)}.{reader.GetString (typeRef.Name)}";
-})
-.ToList ();
-Assert.Contains ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", attrNames);
-}
-}
-
-}
-
-public class IgnoresAccessChecksTo : IDisposable
+ [Fact]
+ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
+ {
+ var peers = ScanFixtures ();
+ var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+ var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "AcwTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ Assert.Contains ("RegisterNatives", methods);
+ Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0"));
+ }
+ }
+
+ [Fact]
+ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+ var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "UcoTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+ var ucoMethod = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("_uco_"));
+
+ var attrNames = ucoMethod.GetCustomAttributes ()
+ .Select (h => reader.GetCustomAttribute (h))
+ .Select (a => {
+ var ctorHandle = (MemberReferenceHandle) a.Constructor;
+ var ctor = reader.GetMemberReference (ctorHandle);
+ var typeRef = reader.GetTypeReference ((TypeReferenceHandle) ctor.Parent);
+ return $"{reader.GetString (typeRef.Namespace)}.{reader.GetString (typeRef.Name)}";
+ })
+ .ToList ();
+ Assert.Contains ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", attrNames);
+ }
+ }
+
+ }
+
+ public class IgnoresAccessChecksTo : IDisposable
{
readonly string _outputDir = CreateTempDir ();
public void Dispose () => DeleteTempDir (_outputDir);
@@ -286,68 +286,68 @@ public void Generate_EmptyPeerList_ProducesValidAssembly ()
}
-public class JniSignatureHelperTests
-{
+ public class JniSignatureHelperTests
+ {
-[Theory]
-[InlineData ("()V", 0)]
-[InlineData ("(I)V", 1)]
-[InlineData ("(Landroid/os/Bundle;)V", 1)]
-[InlineData ("(IFJ)V", 3)]
-[InlineData ("(ZLandroid/view/View;I)Z", 3)]
-[InlineData ("([Ljava/lang/String;)V", 1)]
-public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount)
-{
-var actual = JniSignatureHelper.ParseParameterTypes (signature);
-Assert.Equal (expectedCount, actual.Count);
-}
-
-[Theory]
-[InlineData ("(Z)V", JniParamKind.Boolean)]
-[InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)]
-public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind)
-{
-var types = JniSignatureHelper.ParseParameterTypes (signature);
-Assert.Single (types);
-Assert.Equal (expectedKind, types [0]);
-}
-
-[Theory]
-[InlineData ("()V", JniParamKind.Void)]
-[InlineData ("()I", JniParamKind.Int)]
-[InlineData ("()Z", JniParamKind.Boolean)]
-[InlineData ("()Ljava/lang/String;", JniParamKind.Object)]
-public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind)
-{
-Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnType (signature));
-}
+ [Theory]
+ [InlineData ("()V", 0)]
+ [InlineData ("(I)V", 1)]
+ [InlineData ("(Landroid/os/Bundle;)V", 1)]
+ [InlineData ("(IFJ)V", 3)]
+ [InlineData ("(ZLandroid/view/View;I)Z", 3)]
+ [InlineData ("([Ljava/lang/String;)V", 1)]
+ public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount)
+ {
+ var actual = JniSignatureHelper.ParseParameterTypes (signature);
+ Assert.Equal (expectedCount, actual.Count);
+ }
-}
+ [Theory]
+ [InlineData ("(Z)V", JniParamKind.Boolean)]
+ [InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)]
+ public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind)
+ {
+ var types = JniSignatureHelper.ParseParameterTypes (signature);
+ Assert.Single (types);
+ Assert.Equal (expectedKind, types [0]);
+ }
-public class NegativeEdgeCase
-{
+ [Theory]
+ [InlineData ("()V", JniParamKind.Void)]
+ [InlineData ("()I", JniParamKind.Int)]
+ [InlineData ("()Z", JniParamKind.Boolean)]
+ [InlineData ("()Ljava/lang/String;", JniParamKind.Object)]
+ public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind)
+ {
+ Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnType (signature));
+ }
-[Fact]
-public void ParseParameterTypes_EmptyString_ReturnsEmptyList ()
-{
-Assert.Empty (JniSignatureHelper.ParseParameterTypes (""));
-}
+ }
-[Fact]
-public void ParseParameterTypes_InvalidSignature_Throws ()
-{
-Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterTypes ("not-a-sig"));
-}
+ public class NegativeEdgeCase
+ {
-[Fact]
-public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList ()
-{
-Assert.Empty (JniSignatureHelper.ParseParameterTypes ("("));
-}
+ [Fact]
+ public void ParseParameterTypes_EmptyString_ReturnsEmptyList ()
+ {
+ Assert.Empty (JniSignatureHelper.ParseParameterTypes (""));
+ }
-}
+ [Fact]
+ public void ParseParameterTypes_InvalidSignature_Throws ()
+ {
+ Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterTypes ("not-a-sig"));
+ }
+
+ [Fact]
+ public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList ()
+ {
+ Assert.Empty (JniSignatureHelper.ParseParameterTypes ("("));
+ }
+
+ }
-public class CreateInstancePaths : IDisposable
+ public class CreateInstancePaths : IDisposable
{
readonly string _outputDir = CreateTempDir ();
public void Dispose () => DeleteTempDir (_outputDir);