Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,7 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r
}

foreach (var className in classes) {
Assert.IsTrue (DexUtils.ContainsClassWithMethod (className, "<init>", "()V", dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
Assert.IsTrue (DexUtils.ContainsClass (className, dexFile, AndroidSdkPath), $"`{dexFile}` should include `{className}`!");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using NUnit.Framework;
using Xamarin.ProjectTools;

namespace Xamarin.Android.Build.Tests;

[TestFixture]
public class DexUtilsTests
{
[Test]
public void ContainsClassWithMethodScopesSignatureToMethod ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'",
" name : 'target'",
" type : '(I)V'",
" name : 'other'",
" type : '()V'",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "target", "(I)V", dexDump));
Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "target", "()V", dexDump));
}

[Test]
public void ContainsClassWithMethodMatchesExactNames ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/PeerExtension;'",
" name : 'getValueExtension'",
" type : '()I'",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "getValue", "()I", dexDump));
}

[TestCase ("()Ljava/lang/Object;")]
[TestCase ("()Ljava/lang/String;")]
public void ContainsClassWithMethodMatchesOverloads (string signature)
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Derived;'",
" name : 'getValue'",
" type : '()Ljava/lang/Object;'",
" name : 'getValue'",
" type : '()Ljava/lang/String;'",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod ("Lexample/Derived;", "getValue", signature, dexDump));
}

[Test]
public void ContainsClassWithMethodRejectsMissingSignature ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'\r",
" name : '$default$getValue'\r",
" access : 0x0009 (PUBLIC STATIC)\r",
" type : '()I'\r",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "$default$getValue", "()I", dexDump));
}

[Test]
public void ContainsClassWithMethodHandlesWhitespaceAndCrLf ()
{
var dexDump = new [] {
"\tClass descriptor : 'Lexample/Peer$-CC;' \r",
" name : '$default$getValue'\r",
" type : '(Lexample/Peer;)I'\r",
};

Assert.IsTrue (DexUtils.ContainsClassWithMethod (
"Lexample/Peer$-CC;",
"$default$getValue",
"(Lexample/Peer;)I",
dexDump));
}

[Test]
public void ContainsClassWithMethodResetsAtRepeatedClassDescriptor ()
{
var dexDump = new [] {
" Class descriptor : 'Lexample/Peer;'",
" name : 'getValue'",
" Class descriptor : 'Lexample/Other;'",
" name : 'getValue'",
" type : '()I'",
};

Assert.IsFalse (DexUtils.ContainsClassWithMethod ("Lexample/Peer;", "getValue", "()I", dexDump));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ protected static string RunAdbCommand (string command, bool ignoreErrors = true,
return RunProcess (adb, $"{adbTarget} {command}", timeout);
}

protected static (int code, string stdOutput, string stdError) RunAdbCommandWithExitCode (params string [] command)
{
string ext = Environment.OSVersion.Platform != PlatformID.Unix ? ".exe" : "";
string adb = Path.Combine (AndroidSdkPath, "platform-tools", "adb" + ext);
var arguments = new List<string> ();
string adbTarget = Environment.GetEnvironmentVariable ("ADB_TARGET");
if (!string.IsNullOrWhiteSpace (adbTarget)) {
if (adbTarget == "-d" || adbTarget == "-e") {
arguments.Add (adbTarget);
} else if (adbTarget.StartsWith ("-s", StringComparison.Ordinal)) {
string serial = adbTarget.Substring (2).Trim ();
if (serial.Length == 0) {
throw new InvalidOperationException ("ADB_TARGET must include a device serial after '-s'.");
}
arguments.Add ("-s");
arguments.Add (serial);
} else {
throw new InvalidOperationException ($"Unsupported ADB_TARGET value '{adbTarget}'.");
}
}
arguments.AddRange (command);
TestContext.Out.WriteLine ($"{nameof (RunAdbCommandWithExitCode)}: {adb} {string.Join (" ", arguments.Select (a => $"[{a}]"))}");

var info = Xamarin.Android.Tools.ProcessUtils.CreateProcessStartInfo (adb, arguments.ToArray ());
using var standardOutput = new StringWriter ();
using var standardError = new StringWriter ();
using var cancellationTokenSource = new CancellationTokenSource (TimeSpan.FromSeconds (30));
int exitCode;
try {
exitCode = Xamarin.Android.Tools.ProcessUtils.StartProcess (
info,
standardOutput,
standardError,
cancellationTokenSource.Token
).GetAwaiter ().GetResult ();
} catch (OperationCanceledException) {
exitCode = -1;
standardError.WriteLine ("adb timed out after 30 seconds.");
}
return (exitCode, standardOutput.ToString ().Trim (), standardError.ToString ().Trim ());
}

protected static (int code, string stdOutput, string stdError) RunApkDiffCommand (string args, string logFilePath)
{
var executableName = OperatingSystem.IsWindows () ? "apkdiff.exe" : "apkdiff";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ static class ResourceData
static Lazy<byte []> javaSourceTestInterface = new Lazy<byte []> (() => GetResourceData ("JavaSourceTestInterface.java"));
static Lazy<byte []> remapActivityJava = new Lazy<byte []> (() => GetResourceData ("RemapActivity.java"));
static Lazy<byte []> remapActivityXml = new Lazy<byte []> (() => GetResourceData ("RemapActivity.xml"));
static Lazy<byte []> idmStaticMethodsInterface = new Lazy<byte []> (() => GetResourceData ("StaticMethodsInterface.java"));
static Lazy<byte []> interfaceMethods = new Lazy<byte []> (() => GetResourceData ("InterfaceMethods.java"));
static Lazy<byte []> interfaceMethodPeer = new Lazy<byte []> (() => GetResourceData ("InterfaceMethodPeer.java"));
static Lazy<byte []> concreteInterfaceMethodPeer = new Lazy<byte []> (() => GetResourceData ("ConcreteInterfaceMethodPeer.java"));
static Lazy<byte []> interfaceMethodBridgeInvoker = new Lazy<byte []> (() => GetResourceData ("InterfaceMethodBridgeInvoker.java"));
static Lazy<byte []> covariantInterfaceMethods = new Lazy<byte []> (() => GetResourceData ("CovariantInterfaceMethods.java"));

static Lazy<byte []> rtxt = new Lazy<byte []> (() => GetResourceData ("R.txt"));

Expand All @@ -40,7 +44,11 @@ static class ResourceData
public static string RemapActivityXml => Encoding.UTF8.GetString (remapActivityXml.Value);
public static string RemapMamJson => Encoding.UTF8.GetString (GetResourceData ("mam.json"));
public static string RemapMamXml => Encoding.UTF8.GetString (GetResourceData ("mam.xml"));
public static string IdmStaticMethodsInterface => Encoding.UTF8.GetString (idmStaticMethodsInterface.Value);
public static string InterfaceMethods => Encoding.UTF8.GetString (interfaceMethods.Value);
public static string InterfaceMethodPeer => Encoding.UTF8.GetString (interfaceMethodPeer.Value);
public static string ConcreteInterfaceMethodPeer => Encoding.UTF8.GetString (concreteInterfaceMethodPeer.Value);
public static string InterfaceMethodBridgeInvoker => Encoding.UTF8.GetString (interfaceMethodBridgeInvoker.Value);
public static string CovariantInterfaceMethods => Encoding.UTF8.GetString (covariantInterfaceMethods.Value);

public static string RTxt => Encoding.UTF8.GetString (rtxt.Value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public static bool ContainsClass (string className, string dexFile, string andro
return GetClassDescriptors (dexFile).Any (descriptor => descriptor.Contains (className, StringComparison.Ordinal));
}

public static bool ContainsClass (string className, IEnumerable<string> dexDump)
{
return dexDump.Any (line => line.Contains ("Class descriptor") && line.Contains (className));
}

public static IReadOnlyList<string> GetClassDescriptors (string dexFile)
{
using var stream = File.OpenRead (dexFile);
Expand Down Expand Up @@ -307,28 +312,56 @@ static uint ReadUnsignedLeb128 (BinaryReader reader)
/// <param name="method">A Java method name of the form 'foo'</param>
/// <param name="type">A Java method signature of the form '()V'</param>
public static bool ContainsClassWithMethod (string className, string method, string type, string dexFile, string androidSdkDirectory)
{
return ContainsClassWithMethod (className, method, type, GetDexDump (dexFile, androidSdkDirectory));
}

public static bool ContainsClassWithMethod (string className, string method, string type, IEnumerable<string> dexDump)
{
bool inClass = false;
bool hasName = false;
bool hasType = false;
DataReceivedEventHandler handler = (s, e) => {
if (e.Data != null) {
if (e.Data.Contains ("Class descriptor")) {
inClass = e.Data.Contains (className);
hasName = false;
} else if (inClass && e.Data.Contains ("name") && e.Data.Contains (method)) {
hasName = true;
} else if (hasName && e.Data.Contains ("type") && e.Data.Contains (type)) {
hasType = true;
foreach (var line in dexDump) {
if (HasDexDumpName (line, "Class descriptor")) {
inClass = ContainsDexDumpValue (line, "Class descriptor", className);
hasName = false;
} else if (inClass && HasDexDumpName (line, "name")) {
hasName = ContainsDexDumpValue (line, "name", method);
} else if (hasName) {
if (ContainsDexDumpValue (line, "type", type)) {
return true;
}
hasName = false;
}
};
DexDump (handler, dexFile, androidSdkDirectory);
return hasType;
}
return false;
}

static bool ContainsDexDumpValue (string line, string name, string value)
{
var separator = line.IndexOf (':');
return separator >= 0 && HasDexDumpName (line, name, separator) &&
line.Substring (separator + 1).Trim () == $"'{value}'";
}

static void DexDump (DataReceivedEventHandler handler, string dexFile, string androidSdkDirectory)
static bool HasDexDumpName (string line, string name)
{
return HasDexDumpName (line, name, line.IndexOf (':'));
}

static bool HasDexDumpName (string line, string name, int separator)
{
return separator >= 0 && line.Substring (0, separator).Trim () == name;
}

public static IReadOnlyList<string> GetDexDump (string dexFile, string androidSdkDirectory)
{
return DexDump (dexFile, androidSdkDirectory);
}

static IReadOnlyList<string> DexDump (string dexFile, string androidSdkDirectory)
{
var lines = new List<string> ();
var linesLock = new object ();
var androidSdk = new AndroidSdkInfo ((l, m) => {
Console.WriteLine ($"{l}: {m}");
if (l == TraceLevel.Error) {
Expand All @@ -340,28 +373,44 @@ static void DexDump (DataReceivedEventHandler handler, string dexFile, string an
throw new Exception ($"Unable to find build-tools in `{androidSdkDirectory}`!");
}

var dexFileName = Path.GetFileName (dexFile);
var psi = new ProcessStartInfo {
FileName = Path.Combine (buildToolsPath, "dexdump"),
Arguments = Path.GetFileName (dexFile),
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Path.GetDirectoryName (dexFile),
};
psi.ArgumentList.Add (dexFileName);
using (var p = new Process { StartInfo = psi }) {
p.ErrorDataReceived += handler;
p.OutputDataReceived += handler;
var errors = new List<string> ();
p.ErrorDataReceived += (s, e) => {
if (e.Data != null) {
errors.Add (e.Data);
}
};
p.OutputDataReceived += (s, e) => {
if (e.Data != null) {
lock (linesLock) {
lines.Add (e.Data);
}
}
};

p.Start ();
p.BeginErrorReadLine ();
p.BeginOutputReadLine ();
p.WaitForExit ();

if (p.ExitCode != 0)
throw new Exception ($"'{psi.FileName} {psi.Arguments}' exited with code: {p.ExitCode}");
throw new Exception (
$"'{psi.FileName} {dexFileName}' exited with code: {p.ExitCode}" +
$"{Environment.NewLine}stdout:{Environment.NewLine}{string.Join (Environment.NewLine, lines)}" +
$"{Environment.NewLine}stderr:{Environment.NewLine}{string.Join (Environment.NewLine, errors)}");
}
return lines;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<EmbeddedResource Include="Resources\RemapActivity*">
<LogicalName>%(FileName)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Resources\StaticMethodsInterface*">
<EmbeddedResource Include="Resources\*InterfaceMethod*.java">
<LogicalName>%(FileName)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package example;

public final class ConcreteInterfaceMethodPeer implements InterfaceMethods {
public ConcreteInterfaceMethodPeer() {
}

@Override
public int getDefaultValue() {
return InterfaceMethods.super.getDefaultValue() + 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package example;

public interface CovariantInterfaceMethods {
interface Base {
Object getCovariantValue();
}

interface Derived extends Base {
@Override
default String getCovariantValue() {
return "bridge";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example;

public final class InterfaceMethodBridgeInvoker {
private InterfaceMethodBridgeInvoker() {
}

private static final class CovariantPeer implements CovariantInterfaceMethods.Derived {
}

public static String invokeCovariantBridge() {
CovariantInterfaceMethods.Base peer = new CovariantPeer();
return (String) peer.getCovariantValue() + ":" + invokeStaticMethods();
}
Comment thread
simonrozsival marked this conversation as resolved.

// Keep Java call sites so the R8 retention gap tracked by dotnet/android#11774
// does not mask the JNI runtime behavior covered here.
public static int invokeStaticMethods() {
return InterfaceMethods.getStaticValue()
+ InterfaceMethods.Nested.getNestedStaticValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package example;

public final class InterfaceMethodPeer
implements InterfaceMethods, InterfaceMethods.Nested {
public InterfaceMethodPeer() {
}
}
21 changes: 21 additions & 0 deletions tests/MSBuildDeviceIntegration/Resources/InterfaceMethods.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package example;

public interface InterfaceMethods {
static int getStaticValue() {
return 11;
}

default int getDefaultValue() {
return 22;
}

interface Nested {
static int getNestedStaticValue() {
return 33;
}

default int getNestedDefaultValue() {
return 44;
}
}
}
Loading