Basic tooling to desugar select core libraries
RELNOTES: None.

PiperOrigin-RevId: 184619885
diff --git a/src/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java b/src/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
new file mode 100644
index 0000000..d7fcad4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/desugar/CoreLibrarySupportTest.java
@@ -0,0 +1,232 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CoreLibrarySupportTest {
+
+  @Test
+  public void testIsRenamedCoreLibrary() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of());
+    assertThat(support.isRenamedCoreLibrary("java/time/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("java/time/y/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("java/io/X")).isFalse();
+    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+  }
+
+  @Test
+  public void testIsRenamedCoreLibrary_prefixedLoader() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter("__/"),
+            null,
+            ImmutableList.of("java/time/"),
+            ImmutableList.of());
+    assertThat(support.isRenamedCoreLibrary("__/java/time/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("__/java/time/y/X")).isTrue();
+    assertThat(support.isRenamedCoreLibrary("__/java/io/X")).isFalse();
+    assertThat(support.isRenamedCoreLibrary("com/google/X")).isFalse();
+  }
+  @Test
+  public void testRenameCoreLibrary() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""), null, ImmutableList.of(), ImmutableList.of());
+    assertThat(support.renameCoreLibrary("java/time/X")).isEqualTo("j$/time/X");
+    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+  }
+
+  @Test
+  public void testRenameCoreLibrary_prefixedLoader() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter("__/"), null, ImmutableList.of(), ImmutableList.of());
+    assertThat(support.renameCoreLibrary("__/java/time/X")).isEqualTo("j$/time/X");
+    assertThat(support.renameCoreLibrary("com/google/X")).isEqualTo("com/google/X");
+  }
+
+  @Test
+  public void testIsEmulatedCoreLibraryInvocation() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"));
+    assertThat(
+            support.isEmulatedCoreLibraryInvocation(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isTrue(); // true for default method
+    assertThat(
+            support.isEmulatedCoreLibraryInvocation(
+                Opcodes.INVOKEINTERFACE, "java/util/Collection", "size", "()I", true))
+        .isFalse(); // false for abstract method
+  }
+
+  @Test
+  public void testGetEmulatedCoreLibraryInvocationTarget_defaultMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"));
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isEqualTo(Collection.class);
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                false))
+        .isEqualTo(Collection.class);
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "com/google/common/collect/ImmutableList",
+                "removeIf",
+                "(Ljava/util/function/Predicate;)Z",
+                true))
+        .isNull();
+  }
+
+  @Test
+  public void testGetEmulatedCoreLibraryInvocationTarget_abstractMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Collection"));
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Collection",
+                "size",
+                "()I",
+                true))
+        .isNull();
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEVIRTUAL,
+                "java/util/ArrayList",
+                "size",
+                "()I",
+                false))
+        .isNull();
+  }
+
+  @Test
+  public void testGetEmulatedCoreLibraryInvocationTarget_defaultOverride() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Map"));
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Map",
+                "putIfAbsent",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "putIfAbsent",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isNull(); // putIfAbsent is default in Map but abstract in ConcurrentMap
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(ConcurrentMap.class); // conversely, getOrDefault is overridden as default method
+  }
+
+  @Test
+  public void testGetEmulatedCoreLibraryInvocationTarget_staticInterfaceMethod() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of(),
+            ImmutableList.of("java/util/Comparator"));
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKESTATIC,
+                "java/util/Comparator",
+                "reverseOrder",
+                "()Ljava/util/Comparator;",
+                true))
+        .isEqualTo(Comparator.class);
+  }
+
+  @Test
+  public void testGetEmulatedCoreLibraryInvocationTarget_ignoreRenamed() throws Exception {
+    CoreLibrarySupport support =
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""),
+            Thread.currentThread().getContextClassLoader(),
+            ImmutableList.of("java/util/concurrent/"),  // should return null for these
+            ImmutableList.of("java/util/Map"));
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/Map",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isEqualTo(Map.class);
+    assertThat(
+            support.getEmulatedCoreLibraryInvocationTarget(
+                Opcodes.INVOKEINTERFACE,
+                "java/util/concurrent/ConcurrentMap",
+                "getOrDefault",
+                "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+                true))
+        .isNull();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java b/src/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
new file mode 100644
index 0000000..0626b46
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/desugar/CorePackageRenamerTest.java
@@ -0,0 +1,90 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(JUnit4.class)
+public class CorePackageRenamerTest {
+
+  @Test
+  public void testSymbolRewrite() throws Exception {
+    MockClassVisitor out = new MockClassVisitor();
+    CorePackageRenamer renamer = new CorePackageRenamer(
+        out,
+        new CoreLibrarySupport(
+            new CoreLibraryRewriter(""), null, ImmutableList.of("java/time/"), ImmutableList.of()));
+    MethodVisitor mv = renamer.visitMethod(0, "test", "()V", null, null);
+
+    mv.visitMethodInsn(
+        Opcodes.INVOKESTATIC, "java/time/Instant", "now", "()Ljava/time/Instant;", false);
+    assertThat(out.mv.owner).isEqualTo("j$/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+    mv.visitMethodInsn(
+        Opcodes.INVOKESTATIC, "other/time/Instant", "now", "()Ljava/time/Instant;", false);
+    assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("()Lj$/time/Instant;");
+
+    mv.visitFieldInsn(
+        Opcodes.GETFIELD, "other/time/Instant", "now", "Ljava/time/Instant;");
+    assertThat(out.mv.owner).isEqualTo("other/time/Instant");
+    assertThat(out.mv.desc).isEqualTo("Lj$/time/Instant;");
+  }
+
+  private static class MockClassVisitor extends ClassVisitor {
+
+    final MockMethodVisitor mv = new MockMethodVisitor();
+
+    public MockClassVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public MethodVisitor visitMethod(
+        int access, String name, String desc, String signature, String[] exceptions) {
+      return mv;
+    }
+  }
+
+  private static class MockMethodVisitor extends MethodVisitor {
+
+    String owner;
+    String desc;
+
+    public MockMethodVisitor() {
+      super(Opcodes.ASM6);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      this.owner = owner;
+      this.desc = desc;
+    }
+
+    @Override
+    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
+      this.owner = owner;
+      this.desc = desc;
+    }
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
new file mode 100644
index 0000000..417248b
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryInvocationRewriter.java
@@ -0,0 +1,81 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+/**
+ * Rewriter of default and static interface methods defined in some core libraries.
+ *
+ * <p>This is conceptually similar to call site rewriting in {@link InterfaceDesugaring} but here
+ * we're doing it for certain bootclasspath methods and in particular for invokeinterface and
+ * invokevirtual, which are ignored in regular {@link InterfaceDesugaring}.
+ */
+public class CoreLibraryInvocationRewriter extends ClassVisitor {
+
+  private final CoreLibrarySupport support;
+
+  public CoreLibraryInvocationRewriter(ClassVisitor cv, CoreLibrarySupport support) {
+    super(Opcodes.ASM6, cv);
+    this.support = support;
+  }
+
+  @Override
+  public MethodVisitor visitMethod(
+      int access, String name, String desc, String signature, String[] exceptions) {
+    MethodVisitor result = super.visitMethod(access, name, desc, signature, exceptions);
+    return result != null ? new CoreLibraryMethodInvocationRewriter(result) : null;
+  }
+
+  private class CoreLibraryMethodInvocationRewriter extends MethodVisitor {
+    public CoreLibraryMethodInvocationRewriter(MethodVisitor mv) {
+      super(Opcodes.ASM6, mv);
+    }
+
+    @Override
+    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
+      Class<?> coreInterface =
+          support.getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf);
+      if (coreInterface != null) {
+        String coreInterfaceName = coreInterface.getName().replace('.', '/');
+        name =
+            InterfaceDesugaring.normalizeInterfaceMethodName(
+                name, name.startsWith("lambda$"), opcode == Opcodes.INVOKESTATIC);
+        if (opcode == Opcodes.INVOKESTATIC) {
+          checkState(owner.equals(coreInterfaceName));
+        } else {
+          desc =
+              InterfaceDesugaring.companionDefaultMethodDescriptor(
+                  opcode == Opcodes.INVOKESPECIAL ? owner : coreInterfaceName, desc);
+        }
+
+        if (opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
+          checkArgument(itf, "Expected interface to rewrite %s.%s : %s", owner, name, desc);
+          owner = InterfaceDesugaring.getCompanionClassName(owner);
+        } else {
+          // TODO(kmb): Simulate dynamic dispatch instead of calling most general default method
+          owner = InterfaceDesugaring.getCompanionClassName(coreInterfaceName);
+        }
+        opcode = Opcodes.INVOKESTATIC;
+        itf = false;
+      }
+      super.visitMethodInsn(opcode, owner, name, desc, itf);
+    }
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
index 7f1591b..456fdb5 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibraryRewriter.java
@@ -85,6 +85,10 @@
     return false;
   }
 
+  public String getPrefix() {
+    return prefix;
+  }
+
   /** Removes prefix from class names */
   public String unprefix(String typeName) {
     if (prefix.isEmpty() || !typeName.startsWith(prefix)) {
@@ -159,7 +163,7 @@
       if (!prefix.isEmpty()) {
         this.cv =
             new ClassRemapper(
-                this.cv,
+                this.writer,
                 new Remapper() {
                   @Override
                   public String map(String typeName) {
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
new file mode 100644
index 0000000..56e5f18
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/CoreLibrarySupport.java
@@ -0,0 +1,152 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.ImmutableList;
+import java.lang.reflect.Method;
+import java.util.LinkedHashSet;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+/**
+ * Helper that keeps track of which core library classes and methods we want to rewrite.
+ */
+class CoreLibrarySupport {
+
+  private final CoreLibraryRewriter rewriter;
+  private final ClassLoader targetLoader;
+  /** Internal name prefixes that we want to move to a custom package. */
+  private final ImmutableList<String> renamedPrefixes;
+  /** Internal names of interfaces whose default and static interface methods we'll emulate. */
+  private final ImmutableList<Class<?>> emulatedInterfaces;
+
+  public CoreLibrarySupport(CoreLibraryRewriter rewriter, ClassLoader targetLoader,
+      ImmutableList<String> renamedPrefixes, ImmutableList<String> emulatedInterfaces)
+      throws ClassNotFoundException {
+    this.rewriter = rewriter;
+    this.targetLoader = targetLoader;
+    checkArgument(
+        renamedPrefixes.stream().allMatch(prefix -> prefix.startsWith("java/")), renamedPrefixes);
+    this.renamedPrefixes = renamedPrefixes;
+    ImmutableList.Builder<Class<?>> classBuilder = ImmutableList.builder();
+    for (String itf : emulatedInterfaces) {
+      checkArgument(itf.startsWith("java/util/"), itf);
+      Class<?> clazz = targetLoader.loadClass((rewriter.getPrefix() + itf).replace('/', '.'));
+      checkArgument(clazz.isInterface(), itf);
+      classBuilder.add(clazz);
+    }
+    this.emulatedInterfaces = classBuilder.build();
+  }
+
+  public boolean isRenamedCoreLibrary(String internalName) {
+    String unprefixedName = rewriter.unprefix(internalName);
+    return renamedPrefixes.stream().anyMatch(prefix -> unprefixedName.startsWith(prefix));
+  }
+
+  public String renameCoreLibrary(String internalName) {
+    internalName = rewriter.unprefix(internalName);
+    return (internalName.startsWith("java/"))
+        ? "j$/" + internalName.substring(/* cut away "java/" prefix */ 5)
+        : internalName;
+  }
+
+  public boolean isEmulatedCoreLibraryInvocation(
+      int opcode, String owner, String name, String desc, boolean itf) {
+    return getEmulatedCoreLibraryInvocationTarget(opcode, owner, name, desc, itf) != null;
+  }
+
+  @Nullable
+  public Class<?> getEmulatedCoreLibraryInvocationTarget(
+      int opcode, String owner, String name, String desc, boolean itf) {
+    if (owner.contains("$$Lambda$") || owner.endsWith("$$CC")) {
+      return null;  // regular desugaring handles invocations on generated classes, no emulation
+    }
+    Class<?> clazz = getEmulatedCoreClassOrInterface(owner);
+    if (clazz == null) {
+      return null;
+    }
+
+    if (itf && opcode == Opcodes.INVOKESTATIC) {
+      return clazz; // static interface method
+    }
+
+    Method callee = findInterfaceMethod(clazz, name, desc);
+    if (callee != null && callee.isDefault()) {
+      return callee.getDeclaringClass();
+    }
+    return null;
+  }
+
+  private Class<?> getEmulatedCoreClassOrInterface(String internalName) {
+    {
+      String unprefixedOwner = rewriter.unprefix(internalName);
+      if (!unprefixedOwner.startsWith("java/util/") || isRenamedCoreLibrary(unprefixedOwner)) {
+        return null;
+      }
+    }
+
+    Class<?> clazz;
+    try {
+      clazz = targetLoader.loadClass(internalName.replace('/', '.'));
+    } catch (ClassNotFoundException e) {
+      throw (NoClassDefFoundError) new NoClassDefFoundError().initCause(e);
+    }
+
+    if (emulatedInterfaces.stream().anyMatch(itf -> itf.isAssignableFrom(clazz))) {
+      return clazz;
+    }
+    return null;
+  }
+
+  private static Method findInterfaceMethod(Class<?> clazz, String name, String desc) {
+    return collectImplementedInterfaces(clazz, new LinkedHashSet<>())
+        .stream()
+        // search more subtypes before supertypes
+        .sorted(DefaultMethodClassFixer.InterfaceComparator.INSTANCE)
+        .map(itf -> findMethod(itf, name, desc))
+        .filter(Objects::nonNull)
+        .findFirst()
+        .orElse((Method) null);
+  }
+
+
+  private static Method findMethod(Class<?> clazz, String name, String desc) {
+    for (Method m : clazz.getMethods()) {
+      if (m.getName().equals(name) && Type.getMethodDescriptor(m).equals(desc)) {
+        return m;
+      }
+    }
+    return null;
+  }
+
+  private static Set<Class<?>> collectImplementedInterfaces(Class<?> clazz, Set<Class<?>> dest) {
+    if (clazz.isInterface()) {
+      if (!dest.add(clazz)) {
+        return dest;
+      }
+    } else if (clazz.getSuperclass() != null) {
+      collectImplementedInterfaces(clazz.getSuperclass(), dest);
+    }
+
+    for (Class<?> itf : clazz.getInterfaces()) {
+      collectImplementedInterfaces(itf, dest);
+    }
+    return dest;
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java b/src/tools/android/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
new file mode 100644
index 0000000..3d58ef6
--- /dev/null
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/CorePackageRenamer.java
@@ -0,0 +1,54 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.android.desugar;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.commons.ClassRemapper;
+import org.objectweb.asm.commons.Remapper;
+
+/**
+ * A visitor that renames some type names and only when the owner is also renamed.
+ */
+class CorePackageRenamer extends ClassRemapper {
+
+  public CorePackageRenamer(ClassVisitor cv, CoreLibrarySupport support) {
+    super(cv, new CorePackageRemapper(support));
+  }
+
+  private static final class CorePackageRemapper extends Remapper {
+    private final CoreLibrarySupport support;
+
+    private CorePackageRemapper(CoreLibrarySupport support) {
+      this.support = support;
+    }
+
+    public boolean isRenamed(String owner) {
+      return support.isRenamedCoreLibrary(owner);
+    }
+
+    @Override
+    public String map(String typeName) {
+      return isRenamed(typeName) ? support.renameCoreLibrary(typeName) : typeName;
+    }
+
+    @Override
+    public Object mapValue(Object value) {
+      if (value instanceof Handle && !isRenamed(((Handle) value).getOwner())) {
+        return value;
+      }
+      return super.mapValue(value);
+    }
+  }
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
index 5d3df4a..c86b406 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/Desugar.java
@@ -248,6 +248,28 @@
     )
     public boolean coreLibrary;
 
+    /** Type prefixes that we'll move to a custom package. */
+    @Option(
+      name = "rewrite_core_library_prefix",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Assume the given java.* prefixes are desugared."
+    )
+    public List<String> rewriteCoreLibraryPrefixes;
+
+    /** Interfaces whose default and static interface methods we'll emulate. */
+    @Option(
+      name = "emulate_core_library_interface",
+      defaultValue = "", // ignored
+      allowMultiple = true,
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = {OptionEffectTag.UNKNOWN},
+      help = "Assume the given java.* interfaces are emulated."
+    )
+    public List<String> emulateCoreLibraryInterfaces;
+
     /** Set to work around b/62623509 with JaCoCo versions prior to 0.7.9. */
     // TODO(kmb): Remove when Android Studio doesn't need it anymore (see b/37116789)
     @Option(
@@ -265,7 +287,7 @@
   private final DesugarOptions options;
   private final CoreLibraryRewriter rewriter;
   private final LambdaClassMaker lambdas;
-  private final GeneratedClassStore store;
+  private final GeneratedClassStore store = new GeneratedClassStore();
   private final Set<String> visitedExceptionTypes = new HashSet<>();
   /** The counter to record the times of try-with-resources desugaring is invoked. */
   private final AtomicInteger numOfTryWithResourcesInvoked = new AtomicInteger();
@@ -282,7 +304,6 @@
     this.options = options;
     this.rewriter = new CoreLibraryRewriter(options.coreLibrary ? "__desugar__/" : "");
     this.lambdas = new LambdaClassMaker(dumpDirectory);
-    this.store = new GeneratedClassStore();
     this.outputJava7 = options.minSdkVersion < 24;
     this.allowDefaultMethods =
         options.desugarInterfaceMethodBodiesIfNeeded || options.minSdkVersion >= 24;
@@ -358,6 +379,16 @@
 
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector = ImmutableSet.builder();
       ClassVsInterface interfaceCache = new ClassVsInterface(classpathReader);
+      CoreLibrarySupport coreLibrarySupport =
+          options.rewriteCoreLibraryPrefixes.isEmpty()
+                  && options.emulateCoreLibraryInterfaces.isEmpty()
+              ? null
+              : new CoreLibrarySupport(
+                  rewriter,
+                  loader,
+                  ImmutableList.copyOf(options.rewriteCoreLibraryPrefixes),
+                  ImmutableList.copyOf(options.emulateCoreLibraryInterfaces));
+
       desugarClassesInInput(
           inputFiles,
           outputFileProvider,
@@ -365,6 +396,7 @@
           classpathReader,
           depsCollector,
           bootclasspathReader,
+          coreLibrarySupport,
           interfaceCache,
           interfaceLambdaMethodCollector);
 
@@ -374,11 +406,12 @@
           classpathReader,
           depsCollector,
           bootclasspathReader,
+          coreLibrarySupport,
           interfaceCache,
           interfaceLambdaMethodCollector.build(),
           bridgeMethodReader);
 
-      desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader);
+      desugarAndWriteGeneratedClasses(outputFileProvider, bootclasspathReader, coreLibrarySupport);
       copyThrowableExtensionClass(outputFileProvider);
 
       byte[] depsInfo = depsCollector.toByteArray();
@@ -445,6 +478,7 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector)
       throws IOException {
@@ -466,6 +500,7 @@
                   classpathReader,
                   depsCollector,
                   bootclasspathReader,
+                  coreLibrarySupport,
                   interfaceCache,
                   interfaceLambdaMethodCollector,
                   writer,
@@ -494,6 +529,7 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader)
@@ -525,6 +561,7 @@
                 classpathReader,
                 depsCollector,
                 bootclasspathReader,
+                coreLibrarySupport,
                 interfaceCache,
                 interfaceLambdaMethods,
                 bridgeMethodReader,
@@ -540,7 +577,9 @@
   }
 
   private void desugarAndWriteGeneratedClasses(
-      OutputFileProvider outputFileProvider, ClassReaderFactory bootclasspathReader)
+      OutputFileProvider outputFileProvider,
+      ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport)
       throws IOException {
     // Write out any classes we generated along the way
     ImmutableMap<String, ClassNode> generatedClasses = store.drain();
@@ -552,8 +591,13 @@
       UnprefixingClassWriter writer = rewriter.writer(ClassWriter.COMPUTE_MAXS);
       // checkState above implies that we want Java 7 .class files, so send through that visitor.
       // Don't need a ClassReaderFactory b/c static interface methods should've been moved.
-      ClassVisitor visitor =
-          new Java7Compatibility(writer, (ClassReaderFactory) null, bootclasspathReader);
+      ClassVisitor visitor = writer;
+      if (coreLibrarySupport != null) {
+        visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+        visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+      }
+
+      visitor = new Java7Compatibility(visitor, (ClassReaderFactory) null, bootclasspathReader);
       generated.getValue().accept(visitor);
       String filename = rewriter.unprefix(generated.getKey()) + ".class";
       outputFileProvider.write(filename, writer.toByteArray());
@@ -569,6 +613,7 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet<String> interfaceLambdaMethods,
       @Nullable ClassReaderFactory bridgeMethodReader,
@@ -576,6 +621,12 @@
       UnprefixingClassWriter writer,
       ClassReader input) {
     ClassVisitor visitor = checkNotNull(writer);
+
+    if (coreLibrarySupport != null) {
+      visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+      visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+    }
+
     if (!allowTryWithResources) {
       CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
       input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -612,6 +663,7 @@
                 options.legacyJacocoFix);
       }
     }
+
     visitor =
         new LambdaClassFixer(
             visitor,
@@ -639,11 +691,18 @@
       @Nullable ClassReaderFactory classpathReader,
       DependencyCollector depsCollector,
       ClassReaderFactory bootclasspathReader,
+      @Nullable CoreLibrarySupport coreLibrarySupport,
       ClassVsInterface interfaceCache,
       ImmutableSet.Builder<String> interfaceLambdaMethodCollector,
       UnprefixingClassWriter writer,
       ClassReader input) {
     ClassVisitor visitor = checkNotNull(writer);
+
+    if (coreLibrarySupport != null) {
+      visitor = new CorePackageRenamer(visitor, coreLibrarySupport);
+      visitor = new CoreLibraryInvocationRewriter(visitor, coreLibrarySupport);
+    }
+
     if (!allowTryWithResources) {
       CloseResourceMethodScanner closeResourceMethodScanner = new CloseResourceMethodScanner();
       input.accept(closeResourceMethodScanner, ClassReader.SKIP_DEBUG);
@@ -678,6 +737,7 @@
                   options.legacyJacocoFix);
         }
       }
+
       // LambdaDesugaring is relatively expensive, so check first whether we need it.  Additionally,
       // we need to collect lambda methods referenced by invokedynamic instructions up-front anyway.
       // TODO(kmb): Scan constant pool instead of visiting the class to find bootstrap methods etc.
diff --git a/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
index b8b3ead..3524fae 100644
--- a/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
+++ b/src/tools/android/java/com/google/devtools/build/android/desugar/InterfaceDesugaring.java
@@ -265,8 +265,7 @@
     return "<clinit>".equals(methodName);
   }
 
-  private static String normalizeInterfaceMethodName(
-      String name, boolean isLambda, boolean isStatic) {
+  static String normalizeInterfaceMethodName(String name, boolean isLambda, boolean isStatic) {
     if (isLambda) {
       // Rename lambda method to reflect the new owner.  Not doing so confuses LambdaDesugaring
       // if it's run over this class again. LambdaDesugaring has already renamed the method from