[ApiModel] No outlining from already outlined classes

Change-Id: If24fcde19918bafe9884a8f05b59ff5f90ae3348
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
index 5f65c95..71ce242 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/apimodel/ApiInvokeOutlinerDesugaring.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.desugar.apimodel;
 
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.isOutlinedAtSameOrLowerLevel;
 import static org.objectweb.asm.Opcodes.INVOKESTATIC;
 
 import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
@@ -122,6 +123,10 @@
       assert false : "When computed a known api level we should always have a library class";
       return appView.computedMinApiLevel();
     }
+    // Check if this is already outlined.
+    if (isOutlinedAtSameOrLowerLevel(context.getHolder(), referenceApiLevel)) {
+      return appView.computedMinApiLevel();
+    }
     // Check for protected or package private access flags before outlining.
     if (firstLibraryClass.isInterface()
         || instruction.isCheckCast()
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
index cd94f57..94abbe5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/api/InstanceInitializerOutliner.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.api;
 
+import static com.android.tools.r8.utils.AndroidApiLevelUtils.isOutlinedAtSameOrLowerLevel;
+
 import com.android.tools.r8.androidapi.ComputedApiLevel;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.graph.AppView;
@@ -98,6 +100,10 @@
       if (minApiLevel.isGreaterThanOrEqualTo(apiReferenceLevel)) {
         continue;
       }
+      // Check if this is already outlined.
+      if (isOutlinedAtSameOrLowerLevel(context.getHolder(), apiReferenceLevel)) {
+        continue;
+      }
       DexEncodedMethod synthesizedInstanceInitializer =
           createSynthesizedInstanceInitializer(
               invokeDirect.getInvokedMethod(),
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
index 3067de7..1c323cb 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevelUtils.java
@@ -296,4 +296,37 @@
     }
     return interfaces;
   }
+
+  /**
+   * A lot of functionality has already been outlined in androidx. The ordinary pattern for manual
+   * outlining is to create a class with the name ApiXXImpl where XX is the api level. This method
+   * will check the context to see if it matches this pattern in androidx and extract the api level
+   * for comparison with the computed api level.
+   */
+  public static boolean isOutlinedAtSameOrLowerLevel(
+      DexProgramClass clazz, ComputedApiLevel apiLevel) {
+    assert apiLevel.isKnownApiLevel();
+    if (!clazz.getType().getDescriptor().startsWith("Landroidx/")) {
+      return false;
+    }
+    String simpleName = clazz.getSimpleName();
+    int apiIndex = simpleName.indexOf("Api");
+    if (apiIndex < 0) {
+      return false;
+    }
+    int endApiIndex = apiIndex += 3;
+    int implIndex = simpleName.indexOf("Impl");
+    if (implIndex < 0 || implIndex < endApiIndex || (implIndex - endApiIndex) != 2) {
+      return false;
+    }
+    String apiLevelAsString = simpleName.substring(endApiIndex, implIndex);
+    if (!StringUtils.onlyContainsDigits(apiLevelAsString)) {
+      return false;
+    }
+    int apiLevelAsInt = Integer.parseInt(apiLevelAsString);
+    if (apiLevelAsInt < 10 || apiLevelAsInt > AndroidApiLevel.LATEST.getLevel()) {
+      return false;
+    }
+    return apiLevel.asKnownApiLevel().getApiLevel().getLevel() <= apiLevelAsInt;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelAndroidxApiImplTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelAndroidxApiImplTest.java
index ff1875d..0d87b10 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelAndroidxApiImplTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelAndroidxApiImplTest.java
@@ -9,6 +9,7 @@
 import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticApiOutlineClass;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assume.assumeTrue;
 
@@ -145,16 +146,15 @@
     if (parameters.isCfRuntime()) {
       assertThat(inspector.clazz(classReference), isPresent());
     } else {
-      assertThat(inspector.clazz(classReference), notIf(isPresent(), isR8));
+      assertThat(
+          inspector.clazz(classReference),
+          notIf(
+              isPresent(),
+              isR8 && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O)));
       assertThat(
           inspector.clazz(syntheticApiOutlineClass(classReference, 0)),
           notIf(isPresent(), parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.R)));
-      assertThat(
-          inspector.clazz(syntheticApiOutlineClass(classReference, 1)),
-          notIf(isPresent(), parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O)));
-      assertThat(
-          inspector.clazz(syntheticApiOutlineClass(classReference, 2)),
-          notIf(isPresent(), parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.M)));
+      assertThat(inspector.clazz(syntheticApiOutlineClass(classReference, 1)), not(isPresent()));
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java
index 39b1db2..9564922 100644
--- a/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelOutlineMethodProtectedTest.java
@@ -29,7 +29,7 @@
 
 // This is a reproduction of b/216136762.
 @RunWith(Parameterized.class)
-public class ApiModelOutlineMethodProtectedTest extends TestBase {
+class ApiModelOutlineMethodProtectedTest extends TestBase {
 
   private static final AndroidApiLevel classApiLevel = AndroidApiLevel.M;
   private static final AndroidApiLevel methodApiLevel = AndroidApiLevel.O_MR1;