R8 Assistant: Support Proxy and ServiceLoader

Change-Id: Ifa630002516bd14c21a603f48f6cbadaa32e16d4
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
index 0aa1326..d0a7baf 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/EmptyReflectiveOperationReceiver.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
 
 @KeepForApi
 public class EmptyReflectiveOperationReceiver implements ReflectiveOperationReceiver {
@@ -63,4 +64,14 @@
   @Override
   public void onAtomicReferenceFieldUpdaterNewUpdater(
       Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {}
+
+  @Override
+  public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {}
+
+  @Override
+  public void onProxyNewProxyInstance(
+      Stack stack,
+      ClassLoader classLoader,
+      Class<?>[] interfaces,
+      InvocationHandler invocationHandler) {}
 }
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
index 5df728a..71f8b99 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationLogger.java
@@ -6,6 +6,8 @@
 
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
+import java.util.Arrays;
 
 @KeepForApi
 public class ReflectiveOperationLogger implements ReflectiveOperationReceiver {
@@ -107,6 +109,26 @@
             + name);
   }
 
+  @Override
+  public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+    System.out.println("Reflectively got ServiceLoader.load on " + clazz + " with " + classLoader);
+  }
+
+  @Override
+  public void onProxyNewProxyInstance(
+      Stack stack,
+      ClassLoader classLoader,
+      Class<?>[] interfaces,
+      InvocationHandler invocationHandler) {
+    System.out.println(
+        "Reflectively got Proxy.newProxyInstance on "
+            + Arrays.toString(interfaces)
+            + " with "
+            + classLoader
+            + " and "
+            + invocationHandler);
+  }
+
   public boolean requiresStackInformation() {
     return true;
   }
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
index 79ff0e5..69088f1 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationReceiver.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
 
 @KeepForApi
 public interface ReflectiveOperationReceiver {
@@ -48,6 +49,14 @@
   void onAtomicReferenceFieldUpdaterNewUpdater(
       Stack stack, Class<?> clazz, Class<?> fieldClass, String name);
 
+  void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader);
+
+  void onProxyNewProxyInstance(
+      Stack stack,
+      ClassLoader classLoader,
+      Class<?>[] interfaces,
+      InvocationHandler invocationHandler);
+
   @KeepForApi
   enum ClassFlag {
     ANNOTATION,
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 54d0eb9..aaacd87 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.ClassFlag;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver.NameLookupType;
 import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.lang.reflect.InvocationHandler;
 import java.util.Arrays;
 
 @KeepForApi
@@ -196,4 +197,23 @@
     getInstance()
         .onAtomicReferenceFieldUpdaterNewUpdater(Stack.createStack(), clazz, fieldClass, name);
   }
+
+  public static void onServiceLoaderLoad(Class<?> clazz) {
+    getInstance().onServiceLoaderLoad(Stack.createStack(), clazz, null);
+  }
+
+  public static void onServiceLoaderLoadWithClassLoader(Class<?> clazz, ClassLoader classLoader) {
+    getInstance().onServiceLoaderLoad(Stack.createStack(), clazz, classLoader);
+  }
+
+  public static void onServiceLoaderLoadInstalled(Class<?> clazz) {
+    getInstance()
+        .onServiceLoaderLoad(Stack.createStack(), clazz, ClassLoader.getSystemClassLoader());
+  }
+
+  public static void onProxyNewProxyInstance(
+      ClassLoader classLoader, Class<?>[] interfaces, InvocationHandler invocationHandler) {
+    getInstance()
+        .onProxyNewProxyInstance(Stack.createStack(), classLoader, interfaces, invocationHandler);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
index 928982f..75a08d7 100644
--- a/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
+++ b/src/main/java/com/android/tools/r8/assistant/InstrumentedReflectiveMethodList.java
@@ -144,6 +144,25 @@
             factory.classType,
             factory.stringType));
 
+    builder.put(
+        factory.serviceLoaderMethods.load,
+        getMethodReferenceWithClassParameter("onServiceLoaderLoad"));
+    builder.put(
+        factory.serviceLoaderMethods.loadWithClassLoader,
+        getMethodReferenceWithParameterTypes(
+            "onServiceLoaderLoadWithClassLoader", factory.classType, factory.classLoaderType));
+    builder.put(
+        factory.serviceLoaderMethods.loadInstalled,
+        getMethodReferenceWithClassParameter("onServiceLoaderLoadInstalled"));
+
+    builder.put(
+        factory.proxyMethods.newProxyInstance,
+        getMethodReferenceWithParameterTypes(
+            "onProxyNewProxyInstance",
+            factory.classLoaderType,
+            factory.classArrayType,
+            factory.invocationHandlerType));
+
     return builder.build();
   }
 
diff --git a/src/test/java/com/android/tools/r8/assistant/ProxyTest.java b/src/test/java/com/android/tools/r8/assistant/ProxyTest.java
new file mode 100644
index 0000000..6aabd15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ProxyTest.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import java.lang.reflect.InvocationHandler;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ProxyTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testInstrumentationWithCustomOracle() throws Exception {
+    testForAssistant()
+        .addProgramClassesAndInnerClasses(ProxyTestClass.class)
+        .addInstrumentationClasses(Instrumentation.class)
+        .setCustomReflectiveOperationReceiver(Instrumentation.class)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), ProxyTestClass.class)
+        .assertSuccessWithOutputLines(
+            "[interface com.android.tools.r8.assistant.ProxyTestClass$F]");
+  }
+
+  public static class Instrumentation extends EmptyReflectiveOperationReceiver {
+
+    @Override
+    public void onProxyNewProxyInstance(
+        Stack stack,
+        ClassLoader classLoader,
+        Class<?>[] interfaces,
+        InvocationHandler invocationHandler) {
+      System.out.println(Arrays.toString(interfaces));
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java b/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java
new file mode 100644
index 0000000..008f591
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ProxyTestClass.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+public class ProxyTestClass {
+
+  public static void main(String[] args) {
+    InvocationHandler handler =
+        new InvocationHandler() {
+          @Override
+          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+            return null;
+          }
+        };
+
+    Class<?>[] interfaces = new Class<?>[1];
+    interfaces[0] = F.class;
+    Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), interfaces, handler);
+  }
+
+  interface F {
+    int getInt();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
index e57caf9..b7c09d6 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractor.java
@@ -77,6 +77,9 @@
     if (factory.serviceLoaderMethods.isLoadMethod(method)) {
       return true;
     }
+    if (factory.proxyMethods.newProxyInstance.isIdenticalTo(method)) {
+      return true;
+    }
     DexType type = method.getHolderType();
     String name = method.getName().toString();
     if (type.isIdenticalTo(factory.classType)) {
@@ -91,12 +94,6 @@
       }
       return true;
     }
-    if (type.isIdenticalTo(factory.proxyType)) {
-      if (name.equals("getProxyClass") || name.equals("newProxyInstance")) {
-        return true;
-      }
-      return false;
-    }
     if (type.isIdenticalTo(factory.unsafeType)) {
       // We assume all offset based methods are called using the right method below to compute the
       // offset in the object, we do not support cases where programmers would compute the
@@ -119,9 +116,12 @@
     List<DexType> types = new ArrayList<>(methods.keySet());
     types.sort(Comparator.naturalOrder());
     for (DexType type : types) {
-      sb.append("+++ ").append(type).append(" +++").append("\n");
-      for (DexMethod dexMethod : methods.get(type)) {
-        sb.append(dexMethod).append("\n");
+      Collection<DexMethod> typeMethods = methods.get(type);
+      if (!typeMethods.isEmpty()) {
+        sb.append("+++ ").append(type).append(" +++").append("\n");
+        for (DexMethod dexMethod : typeMethods) {
+          sb.append(dexMethod).append("\n");
+        }
       }
     }
     return sb.toString();
diff --git a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
index 374407f..e7f88f0 100644
--- a/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
+++ b/src/test/java/com/android/tools/r8/assistant/ReflectiveCallExtractorTest.java
@@ -45,7 +45,7 @@
 
   @Test
   public void testGuava() throws Exception {
-    test(ToolHelper.GUAVA_JRE, 21, 17);
+    test(ToolHelper.GUAVA_JRE, 22, 16);
   }
 
   @Test
@@ -64,7 +64,7 @@
             "dump_app.zip");
     Path programArchive =
         CompilerDump.fromArchive(zip, temp.newFolder().toPath()).getProgramArchive();
-    test(programArchive, 26, 23);
+    test(programArchive, 28, 21);
   }
 
   private void test(Path jar, int success, int failure) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java
new file mode 100644
index 0000000..c5ad5a3d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTest.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ServiceLoaderTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testInstrumentationWithCustomOracle() throws Exception {
+    testForAssistant()
+        .addProgramClassesAndInnerClasses(ServiceLoaderTestClass.class)
+        .addInstrumentationClasses(Instrumentation.class)
+        .setCustomReflectiveOperationReceiver(Instrumentation.class)
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), ServiceLoaderTestClass.class)
+        .assertSuccessWithOutputLines(
+            "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService",
+            "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService",
+            "com.android.tools.r8.assistant.ServiceLoaderTestClass$NameService");
+  }
+
+  public static class Instrumentation extends EmptyReflectiveOperationReceiver {
+
+    @Override
+    public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+      System.out.println(clazz.getName());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java
new file mode 100644
index 0000000..8e5861d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/ServiceLoaderTestClass.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant;
+
+import java.util.ServiceLoader;
+
+public class ServiceLoaderTestClass {
+
+  volatile int i;
+  volatile long l;
+  volatile Object o;
+
+  public static void main(String[] args) {
+    ServiceLoader<NameService> loader = ServiceLoader.load(NameService.class);
+    for (NameService nameService : loader) {
+      System.out.println(nameService.getName());
+    }
+
+    ServiceLoader<NameService> loader2 = ServiceLoader.loadInstalled(NameService.class);
+    for (NameService nameService : loader2) {
+      System.out.println(nameService.getName());
+    }
+
+    ServiceLoader<NameService> loader3 =
+        ServiceLoader.load(NameService.class, ServiceLoaderTestClass.class.getClassLoader());
+    for (NameService nameService : loader3) {
+      System.out.println(nameService.getName());
+    }
+  }
+
+  public interface NameService {
+
+    String getName();
+  }
+
+  public class DummyService implements NameService {
+
+    @Override
+    public String getName() {
+      return "dummy";
+    }
+  }
+}