Optimize ServiceLoader.load() calls when no services are present

And adds checks for wrong subtype, non-public constructor, and missing
constructor.

The optimization is enabled only when the following rule is present:
-assumenosideeffects class java.util.ServiceLoader {
  *** load(...);
}

Bug: b/291923475
Change-Id: I3a3409a8b833f1c088e6c0ca7a52efb4b63e9bb5
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index d52e9ae..4151107 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -158,6 +158,10 @@
     return isSet(Constants.ACC_PUBLIC);
   }
 
+  public boolean wasPublic() {
+    return wasSet(Constants.ACC_PUBLIC);
+  }
+
   public void setPublic() {
     assert !isPrivate() && !isProtected();
     set(Constants.ACC_PUBLIC);
diff --git a/src/main/java/com/android/tools/r8/graph/AppServices.java b/src/main/java/com/android/tools/r8/graph/AppServices.java
index ea50b5a..da91743 100644
--- a/src/main/java/com/android/tools/r8/graph/AppServices.java
+++ b/src/main/java/com/android/tools/r8/graph/AppServices.java
@@ -76,10 +76,6 @@
     assert verifyRewrittenWithLens();
     Map<FeatureSplit, List<DexType>> featureSplitListMap = services.get(serviceType);
     if (featureSplitListMap == null) {
-      assert false
-          : "Unexpected attempt to get service implementations for non-service type `"
-              + serviceType.toSourceString()
-              + "`";
       return ImmutableList.of();
     }
     ImmutableList.Builder<DexType> builder = ImmutableList.builder();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 2ca09f2..8cc1360 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -129,10 +129,11 @@
       }
 
       ConstClass constClass = argument.getDefinition().asConstClass();
+      DexType serviceType = constClass.getType();
       if (invokedMethod.isNotIdenticalTo(serviceLoaderMethods.loadWithClassLoader)) {
         report(
             code.context(),
-            constClass.getType(),
+            serviceType,
             "Inlining is only supported for `java.util.ServiceLoader.load(java.lang.Class,"
                 + " java.lang.ClassLoader)`");
         continue;
@@ -143,7 +144,7 @@
               + " java.lang.ServiceLoader.iterator()`";
       Value serviceLoaderLoadOut = serviceLoaderLoad.outValue();
       if (!serviceLoaderLoadOut.hasSingleUniqueUser() || serviceLoaderLoadOut.hasPhiUsers()) {
-        report(code.context(), constClass.getType(), invalidUserMessage);
+        report(code.context(), serviceType, invalidUserMessage);
         continue;
       }
 
@@ -151,14 +152,13 @@
       InvokeVirtual singleUniqueUser = serviceLoaderLoadOut.singleUniqueUser().asInvokeVirtual();
       if (singleUniqueUser == null
           || singleUniqueUser.getInvokedMethod().isNotIdenticalTo(serviceLoaderMethods.iterator)) {
-        report(
-            code.context(), constClass.getType(), invalidUserMessage + ", but found other usages");
+        report(code.context(), serviceType, invalidUserMessage + ", but found other usages");
         continue;
       }
 
       // Check that the service is not kept.
       if (appView().appInfo().isPinnedWithDefinitionLookup(constClass.getValue())) {
-        report(code.context(), constClass.getType(), "The service loader type is kept");
+        report(code.context(), serviceType, "The service loader type is kept");
         continue;
       }
 
@@ -168,9 +168,9 @@
       if (classLoaderValue.isPhi()) {
         report(
             code.context(),
-            constClass.getType(),
+            serviceType,
             "The java.lang.ClassLoader argument must be defined locally as null or "
-                + constClass.getType()
+                + serviceType
                 + ".class.getClassLoader()");
         continue;
       }
@@ -186,45 +186,62 @@
                   .getDefinition()
                   .asConstClass()
                   .getValue()
-                  .isIdenticalTo(constClass.getType());
+                  .isIdenticalTo(serviceType);
       if (!isNullClassLoader && !isGetClassLoaderOnConstClass) {
         report(
             code.context(),
-            constClass.getType(),
+            serviceType,
             "The java.lang.ClassLoader argument must be defined locally as null or "
-                + constClass.getType()
+                + serviceType
                 + ".class.getClassLoader()");
         continue;
       }
 
       // Check that the service is configured in the META-INF/services.
       AppServices appServices = appView.appServices();
-      if (!appServices.allServiceTypes().contains(constClass.getValue())) {
-        report(code.context(), constClass.getType(), "No META-INF/services file found.");
-        continue;
-      }
-
-      // Check that we are not service loading anything from a feature into base.
-      if (hasServiceImplementationInDifferentFeature(
-          code, constClass.getType(), isNullClassLoader)) {
-        continue;
-      }
-
       List<DexType> dexTypes = appServices.serviceImplementationsFor(constClass.getValue());
       List<DexClass> classes = new ArrayList<>(dexTypes.size());
-      boolean seenNull = false;
       for (DexType serviceImpl : dexTypes) {
         DexClass serviceImplementation = appView.definitionFor(serviceImpl);
         if (serviceImplementation == null) {
           report(
               code.context(),
-              constClass.getType(),
+              serviceType,
               "Unable to find definition for service implementation " + serviceImpl.getTypeName());
-          seenNull = true;
+          break;
+        }
+        if (!appView().appInfo().isSubtype(serviceImpl, serviceType)) {
+          report(
+              code.context(),
+              serviceType,
+              "Implementation is not a subtype of the service: " + serviceImpl.getTypeName());
+          break;
+        }
+        DexEncodedMethod method = serviceImplementation.getDefaultInitializer();
+        if (method == null) {
+          report(
+              code.context(),
+              serviceType,
+              "Implementation has no default constructor: " + serviceImpl.getTypeName());
+          break;
+        }
+        if (!method.getAccessFlags().wasPublic()) {
+          // A non-public constructor causes a ServiceConfigurationError on APIs 24 & 25 (Nougat).
+          // Check the original access flag to not change app behavior if R8 publicized the method.
+          report(
+              code.context(),
+              serviceType,
+              "Implementation's default constructor is not public: " + serviceImpl.getTypeName());
+          break;
         }
         classes.add(serviceImplementation);
       }
-      if (seenNull) {
+      if (dexTypes.size() != classes.size()) {
+        continue;
+      }
+
+      // Check that we are not service loading anything from a feature into base.
+      if (hasServiceImplementationInDifferentFeature(code, serviceType, isNullClassLoader)) {
         continue;
       }
 
diff --git a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
index 09025e9..939e90b5 100644
--- a/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/serviceloader/ServiceLoaderRewritingTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.optimize.serviceloader;
 
+import static com.android.tools.r8.ToolHelper.DexVm.Version.V7_0_0;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
@@ -11,11 +12,11 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
 import java.io.IOException;
 import java.util.List;
 import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
@@ -49,6 +50,14 @@
     }
   }
 
+  public static class ServiceImplNoDefaultConstructor extends ServiceImpl {
+    public ServiceImplNoDefaultConstructor(int unused) {}
+  }
+
+  public static class ServiceImplNonPublicConstructor extends ServiceImpl {
+    ServiceImplNonPublicConstructor() {}
+  }
+
   public static class MainRunner {
 
     public static void main(String[] args) {
@@ -153,11 +162,10 @@
       throws IOException, CompilationFailedException, ExecutionException {
     serviceLoaderTest(null)
         .addKeepMainRule(MainRunner.class)
-        .allowDiagnosticInfoMessages()
-        .compileWithExpectedDiagnostics(REWRITER_DIAGNOSTICS)
+        .compile()
         .run(parameters.getRuntime(), MainRunner.class)
         .assertFailureWithErrorThatThrows(NoSuchElementException.class)
-        .inspectFailure(inspector -> assertEquals(1, getServiceLoaderLoads(inspector)));
+        .inspectFailure(inspector -> assertEquals(0, getServiceLoaderLoads(inspector)));
   }
 
   @Test
@@ -221,6 +229,44 @@
   }
 
   @Test
+  public void testDoNoRewriteNoDefaultConstructor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    serviceLoaderTest(Service.class, ServiceImplNoDefaultConstructor.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(REWRITER_DIAGNOSTICS)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertFailureWithErrorThatThrows(ServiceConfigurationError.class);
+  }
+
+  @Test
+  public void testDoNoRewriteNonSubclass()
+      throws IOException, CompilationFailedException, ExecutionException {
+    serviceLoaderTest(Service.class, MainRunner.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(REWRITER_DIAGNOSTICS)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .assertFailureWithErrorThatThrows(ServiceConfigurationError.class);
+  }
+
+  @Test
+  public void testDoNoRewriteNonPublicConstructor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    // This throws a ServiceConfigurationError only on Android 7.
+    serviceLoaderTest(Service.class, ServiceImplNonPublicConstructor.class)
+        .addKeepMainRule(MainRunner.class)
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(REWRITER_DIAGNOSTICS)
+        .run(parameters.getRuntime(), MainRunner.class)
+        .applyIf(
+            parameters.isCfRuntime() || parameters.getDexRuntimeVersion() != V7_0_0,
+            runResult -> runResult.assertSuccessWithOutput(EXPECTED_OUTPUT),
+            runResult ->
+                runResult.assertFailureWithErrorThatThrows(ServiceConfigurationError.class));
+  }
+
+  @Test
   public void testDoNoRewriteWhenEscaping()
       throws IOException, CompilationFailedException, ExecutionException {
     serviceLoaderTest(Service.class, ServiceImpl.class)
@@ -262,7 +308,7 @@
     // https://android-review.googlesource.com/c/platform/libcore/+/273135
     assumeTrue(
         parameters.getRuntime().isCf()
-            || !parameters.getRuntime().asDex().getVm().getVersion().equals(Version.V7_0_0));
+            || !parameters.getRuntime().asDex().getVm().getVersion().equals(V7_0_0));
     serviceLoaderTest(Service.class, ServiceImpl.class)
         .addKeepMainRule(MainRunner.class)
         .addKeepClassRules(Service.class)