Reintroduce legacy access modifier for testing

This partially reverts d2732c9c0b3812069ad0dd1592cc77cb16c13552 and a046083828eaf3a7878b9ff9db15ca28f0f9b78b.

Change-Id: I730b36a13269f196aeb6db22bb9b41f3d05c35cc
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 94109ba..10d9755 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -64,6 +64,7 @@
 import com.android.tools.r8.naming.ProguardMapMinifier;
 import com.android.tools.r8.naming.RecordRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
+import com.android.tools.r8.optimize.LegacyAccessModifier;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
 import com.android.tools.r8.optimize.MemberRebindingIdentityLensFactory;
@@ -452,6 +453,9 @@
       // to clear the cache, so that we will recompute the type lattice elements.
       appView.dexItemFactory().clearTypeElementsCache();
 
+      // TODO(b/132677331): Remove legacy access modifier.
+      LegacyAccessModifier.run(appViewWithLiveness, executorService, timing);
+
       // This pass attempts to reduce the number of nests and nest size to allow further passes, and
       // should therefore be run after the publicizer.
       new NestReducer(appViewWithLiveness).run(executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
new file mode 100644
index 0000000..bd0376b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberPoolCollection.java
@@ -0,0 +1,276 @@
+// Copyright (c) 2019, 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.ir.optimize;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
+
+// Per-class collection of member signatures.
+public abstract class MemberPoolCollection<R extends DexMember<?, R>> {
+
+  final Equivalence<R> equivalence;
+  final AppView<AppInfoWithLiveness> appView;
+  final SubtypingInfo subtypingInfo;
+  final Map<DexClass, MemberPool<R>> memberPools = new ConcurrentHashMap<>();
+
+  MemberPoolCollection(
+      AppView<AppInfoWithLiveness> appView,
+      Equivalence<R> equivalence,
+      SubtypingInfo subtypingInfo) {
+    this.appView = appView;
+    this.equivalence = equivalence;
+    this.subtypingInfo = subtypingInfo;
+  }
+
+  public void buildAll(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Building member pool collection");
+    try {
+      List<Future<?>> futures = new ArrayList<>();
+
+      // Generate a future for each class that will build the member pool collection for the
+      // corresponding class. Note that, we visit the classes using a top-down class hierarchy
+      // traversal, since this ensures that we do not visit library classes that are not
+      // reachable from any program class.
+      TopDownClassHierarchyTraversal.forAllClasses(appView)
+          .visit(appView.appInfo().classes(), clazz -> submit(clazz, futures, executorService));
+      ThreadUtils.awaitFutures(futures);
+    } finally {
+      timing.end();
+    }
+  }
+
+  public MemberPool<R> buildForHierarchy(
+      DexClass clazz, ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Building member pool collection");
+    try {
+      List<Future<?>> futures = new ArrayList<>();
+      submitAll(
+          getAllSuperTypesInclusive(clazz, memberPools::containsKey), futures, executorService);
+      submitAll(getAllSubTypesExclusive(clazz, memberPools::containsKey), futures, executorService);
+      ThreadUtils.awaitFutures(futures);
+    } finally {
+      timing.end();
+    }
+    return get(clazz);
+  }
+
+  public boolean hasPool(DexClass clazz) {
+    return memberPools.containsKey(clazz);
+  }
+
+  public MemberPool<R> get(DexClass clazz) {
+    assert hasPool(clazz);
+    return memberPools.get(clazz);
+  }
+
+  public boolean markIfNotSeen(DexClass clazz, R reference) {
+    MemberPool<R> memberPool = get(clazz);
+    Wrapper<R> key = equivalence.wrap(reference);
+    if (memberPool.hasSeen(key)) {
+      return true;
+    }
+    memberPool.seen(key);
+    return false;
+  }
+
+  private void submitAll(
+      Iterable<? extends DexClass> classes,
+      List<Future<?>> futures,
+      ExecutorService executorService) {
+    for (DexClass clazz : classes) {
+      submit(clazz, futures, executorService);
+    }
+  }
+
+  private void submit(DexClass clazz, List<Future<?>> futures, ExecutorService executorService) {
+    futures.add(executorService.submit(computeMemberPoolForClass(clazz)));
+  }
+
+  abstract Runnable computeMemberPoolForClass(DexClass clazz);
+
+  private Set<DexClass> getAllSuperTypesInclusive(
+      DexClass subject, Predicate<DexClass> stoppingCriterion) {
+    Set<DexClass> superTypes = new HashSet<>();
+    Deque<DexClass> worklist = new ArrayDeque<>();
+    worklist.add(subject);
+    while (!worklist.isEmpty()) {
+      DexClass clazz = worklist.pop();
+      if (stoppingCriterion.test(clazz)) {
+        continue;
+      }
+      if (superTypes.add(clazz)) {
+        if (clazz.superType != null) {
+          addNonNull(worklist, appView.definitionFor(clazz.superType));
+        }
+        for (DexType interfaceType : clazz.interfaces.values) {
+          addNonNull(worklist, appView.definitionFor(interfaceType));
+        }
+      }
+    }
+    return superTypes;
+  }
+
+  private Set<DexClass> getAllSubTypesExclusive(
+      DexClass subject, Predicate<DexClass> stoppingCriterion) {
+    Set<DexClass> subTypes = new HashSet<>();
+    Deque<DexClass> worklist = new ArrayDeque<>();
+    subtypingInfo.forAllImmediateExtendsSubtypes(
+        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+    subtypingInfo.forAllImmediateImplementsSubtypes(
+        subject.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+    while (!worklist.isEmpty()) {
+      DexClass clazz = worklist.pop();
+      if (stoppingCriterion.test(clazz)) {
+        continue;
+      }
+      if (subTypes.add(clazz)) {
+        subtypingInfo.forAllImmediateExtendsSubtypes(
+            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+        subtypingInfo.forAllImmediateImplementsSubtypes(
+            clazz.type, type -> addNonNull(worklist, appView.definitionFor(type)));
+      }
+    }
+    return subTypes;
+  }
+
+  public static class MemberPool<T> {
+
+    private final DexClass clazz;
+    private final Equivalence<T> equivalence;
+    private MemberPool<T> superType;
+    private final Set<MemberPool<T>> interfaces = new HashSet<>();
+    private final Set<MemberPool<T>> subTypes = new HashSet<>();
+    private final Set<Wrapper<T>> memberPool = new HashSet<>();
+
+    MemberPool(Equivalence<T> equivalence, DexClass clazz) {
+      this.equivalence = equivalence;
+      this.clazz = clazz;
+    }
+
+    synchronized void linkSupertype(MemberPool<T> superType) {
+      assert this.superType == null;
+      this.superType = superType;
+    }
+
+    synchronized void linkSubtype(MemberPool<T> subType) {
+      boolean added = subTypes.add(subType);
+      assert added;
+    }
+
+    synchronized void linkInterface(MemberPool<T> itf) {
+      boolean added = interfaces.add(itf);
+      assert added;
+    }
+
+    public void seen(T member) {
+      seen(equivalence.wrap(member));
+    }
+
+    public synchronized void seen(Wrapper<T> member) {
+      boolean added = memberPool.add(member);
+      assert added;
+    }
+
+    public boolean hasSeen(Wrapper<T> member) {
+      return fold(member, false, true, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenDirectly(Wrapper<T> member) {
+      return here(member, false, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenStrictlyAbove(Wrapper<T> member) {
+      return above(member, false, false, true, (t, ignored) -> true);
+    }
+
+    public boolean hasSeenStrictlyBelow(Wrapper<T> member) {
+      return below(member, false, true, (t, ignored) -> true);
+    }
+
+    private <S> S above(
+        Wrapper<T> member,
+        boolean inclusive,
+        S value,
+        S terminator,
+        BiFunction<DexClass, S, S> accumulator) {
+      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this);
+      while (workList.hasNext()) {
+        MemberPool<T> next = workList.next();
+        if (inclusive) {
+          value = next.here(member, value, accumulator);
+          if (value == terminator) {
+            return value;
+          }
+        }
+        inclusive = true;
+        if (next.superType != null) {
+          workList.addIfNotSeen(next.superType);
+        }
+        workList.addIfNotSeen(next.interfaces);
+      }
+      return value;
+    }
+
+    private <S> S here(Wrapper<T> member, S value, BiFunction<DexClass, S, S> accumulator) {
+      if (memberPool.contains(member)) {
+        return accumulator.apply(clazz, value);
+      }
+      return value;
+    }
+
+    public <S> S below(
+        Wrapper<T> member, S value, S terminator, BiFunction<DexClass, S, S> accumulator) {
+      WorkList<MemberPool<T>> workList = WorkList.newIdentityWorkList(this.subTypes);
+      while (workList.hasNext()) {
+        MemberPool<T> next = workList.next();
+        value = next.here(member, value, accumulator);
+        if (value == terminator) {
+          return value;
+        }
+        workList.addIfNotSeen(next.interfaces);
+        workList.addIfNotSeen(next.subTypes);
+      }
+      return value;
+    }
+
+    public <S> S fold(
+        Wrapper<T> member, S initialValue, S terminator, BiFunction<DexClass, S, S> accumulator) {
+      S value = above(member, true, initialValue, terminator, accumulator);
+      if (value == terminator) {
+        return value;
+      }
+      return below(member, initialValue, terminator, accumulator);
+    }
+  }
+
+  private static <T> void addNonNull(Collection<T> collection, T item) {
+    if (item != null) {
+      collection.add(item);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
new file mode 100644
index 0000000..78eb343
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MethodPoolCollection.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2018, 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.ir.optimize;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Predicates;
+import java.util.function.Predicate;
+
+// Per-class collection of method signatures.
+//
+// Example use cases:
+// *) in publicizer,
+//   to determine if a private method does not collide with methods in that class hierarchy.
+// *) in vertical class merger,
+//   before moving a default interface method to its subtype, check if it does not collide with one
+//   in the given class hierarchy.
+public class MethodPoolCollection extends MemberPoolCollection<DexMethod> {
+
+  private final Predicate<DexEncodedMethod> methodTester;
+
+  public MethodPoolCollection(AppView<AppInfoWithLiveness> appView, SubtypingInfo subtypingInfo) {
+    this(appView, subtypingInfo, Predicates.alwaysTrue());
+  }
+
+  public MethodPoolCollection(
+      AppView<AppInfoWithLiveness> appView,
+      SubtypingInfo subtypingInfo,
+      Predicate<DexEncodedMethod> methodTester) {
+    super(appView, MethodSignatureEquivalence.get(), subtypingInfo);
+    this.methodTester = methodTester;
+  }
+
+  public static boolean excludesPrivateInstanceMethod(DexEncodedMethod method) {
+    return !method.isPrivateMethod() || method.isStatic();
+  }
+
+  @Override
+  Runnable computeMemberPoolForClass(DexClass clazz) {
+    return () -> {
+      MemberPool<DexMethod> methodPool =
+          memberPools.computeIfAbsent(clazz, k -> new MemberPool<>(equivalence, k));
+      clazz.forEachMethod(
+          encodedMethod -> {
+            if (methodTester.test(encodedMethod)) {
+              methodPool.seen(equivalence.wrap(encodedMethod.getReference()));
+            }
+          });
+      if (clazz.superType != null) {
+        DexClass superClazz = appView.definitionFor(clazz.superType);
+        if (superClazz != null) {
+          MemberPool<DexMethod> superPool =
+              memberPools.computeIfAbsent(
+                  superClazz, k -> new MemberPool<>(equivalence, superClazz));
+          superPool.linkSubtype(methodPool);
+          methodPool.linkSupertype(superPool);
+        }
+      }
+      if (clazz.isInterface()) {
+        for (DexType subtype : subtypingInfo.allImmediateSubtypes(clazz.type)) {
+          DexClass subClazz = appView.definitionFor(subtype);
+          if (subClazz != null) {
+            MemberPool<DexMethod> childPool =
+                memberPools.computeIfAbsent(subClazz, k -> new MemberPool<>(equivalence, subClazz));
+            methodPool.linkSubtype(childPool);
+            childPool.linkInterface(methodPool);
+          }
+        }
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
new file mode 100644
index 0000000..c51b13c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/LegacyAccessModifier.java
@@ -0,0 +1,225 @@
+// Copyright (c) 2016, 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.optimize;
+
+import static com.android.tools.r8.dex.Constants.ACC_PRIVATE;
+import static com.android.tools.r8.dex.Constants.ACC_PROTECTED;
+import static com.android.tools.r8.dex.Constants.ACC_PUBLIC;
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ProgramDefinition;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
+import com.android.tools.r8.ir.optimize.MethodPoolCollection;
+import com.android.tools.r8.optimize.PublicizerLens.PublicizedLensBuilder;
+import com.android.tools.r8.optimize.accessmodification.AccessModifierOptions;
+import com.android.tools.r8.optimize.redundantbridgeremoval.RedundantBridgeRemover;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public final class LegacyAccessModifier {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final SubtypingInfo subtypingInfo;
+  private final MethodPoolCollection methodPoolCollection;
+
+  private final PublicizedLensBuilder lensBuilder = PublicizerLens.createBuilder();
+
+  private LegacyAccessModifier(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.subtypingInfo = appView.appInfo().computeSubtypingInfo();
+    this.methodPoolCollection =
+        // We will add private instance methods when we promote them.
+        new MethodPoolCollection(
+            appView, subtypingInfo, MethodPoolCollection::excludesPrivateInstanceMethod);
+  }
+
+  /**
+   * Marks all package private and protected methods and fields as public. Makes all private static
+   * methods public. Makes private instance methods public final instance methods, if possible.
+   *
+   * <p>This will destructively update the DexApplication passed in as argument.
+   */
+  public static void run(
+      AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
+    if (accessModifierOptions.isAccessModificationEnabled()
+        && accessModifierOptions.isLegacyAccessModifierEnabled()) {
+      timing.begin("Access modification");
+      new LegacyAccessModifier(appView).internalRun(executorService, timing);
+      timing.end();
+      if (appView.graphLens().isPublicizerLens()) {
+        // We can now remove redundant bridges. Note that we do not need to update the
+        // invoke-targets here, as the existing invokes will simply dispatch to the now
+        // visible super-method. MemberRebinding, if run, will then dispatch it correctly.
+        new RedundantBridgeRemover(appView.withLiveness()).run(executorService, timing);
+      }
+    }
+  }
+
+  private void internalRun(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    // Phase 1: Collect methods to check if private instance methods don't have conflicts.
+    methodPoolCollection.buildAll(executorService, timing);
+
+    // Phase 2: Visit classes and promote class/member to public if possible.
+    timing.begin("Phase 2: promoteToPublic");
+    appView.appInfo().forEachReachableInterface(clazz -> processType(clazz.getType()));
+    processType(appView.dexItemFactory().objectType);
+    timing.end();
+
+    PublicizerLens publicizerLens = lensBuilder.build(appView);
+    if (publicizerLens != null) {
+      appView.setGraphLens(publicizerLens);
+    }
+
+    appView.notifyOptimizationFinishedForTesting();
+  }
+
+  private void doPublicize(ProgramDefinition definition) {
+    definition.getAccessFlags().promoteToPublic();
+  }
+
+  private void processType(DexType type) {
+    DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type));
+    if (clazz != null) {
+      processClass(clazz);
+    }
+    subtypingInfo.forAllImmediateExtendsSubtypes(type, this::processType);
+  }
+
+  private void processClass(DexProgramClass clazz) {
+    if (appView.appInfo().isAccessModificationAllowed(clazz)) {
+      doPublicize(clazz);
+    }
+
+    // Publicize fields.
+    clazz.forEachProgramField(this::processField);
+
+    // Publicize methods.
+    Set<DexEncodedMethod> privateInstanceMethods = new LinkedHashSet<>();
+    clazz.forEachProgramMethod(
+        method -> {
+          if (publicizeMethod(method)) {
+            privateInstanceMethods.add(method.getDefinition());
+          }
+        });
+    if (!privateInstanceMethods.isEmpty()) {
+      clazz.virtualizeMethods(privateInstanceMethods);
+    }
+
+    // Publicize inner class attribute.
+    InnerClassAttribute attr = clazz.getInnerClassAttributeForThisClass();
+    if (attr != null) {
+      int accessFlags = ((attr.getAccess() | ACC_PUBLIC) & ~ACC_PRIVATE) & ~ACC_PROTECTED;
+      clazz.replaceInnerClassAttributeForThisClass(
+          new InnerClassAttribute(
+              accessFlags, attr.getInner(), attr.getOuter(), attr.getInnerName()));
+    }
+  }
+
+  private void processField(ProgramField field) {
+    if (appView.appInfo().isAccessModificationAllowed(field)) {
+      publicizeField(field);
+    }
+  }
+
+  private void publicizeField(ProgramField field) {
+    FieldAccessFlags flags = field.getAccessFlags();
+    if (!flags.isPublic()) {
+      flags.promoteToPublic();
+    }
+  }
+
+  private boolean publicizeMethod(ProgramMethod method) {
+    MethodAccessFlags accessFlags = method.getAccessFlags();
+    if (accessFlags.isPublic()) {
+      return false;
+    }
+    // If this method is mentioned in keep rules, do not transform (rule applications changed).
+    DexEncodedMethod definition = method.getDefinition();
+    if (!appView.appInfo().isAccessModificationAllowed(method)) {
+      // TODO(b/131130038): Also do not publicize package-private and protected methods that are
+      //  kept.
+      if (definition.isPrivate()) {
+        return false;
+      }
+    }
+
+    if (method.getDefinition().isInstanceInitializer() || accessFlags.isProtected()) {
+      doPublicize(method);
+      return false;
+    }
+
+    if (accessFlags.isPackagePrivate()) {
+      // If we publicize a package private method we have to ensure there is no overrides of it. We
+      // could potentially publicize a method if it only has package-private overrides.
+      // TODO(b/182136236): See if we can break the hierarchy for clusters.
+      MemberPool<DexMethod> memberPool = methodPoolCollection.get(method.getHolder());
+      Wrapper<DexMethod> methodKey = MethodSignatureEquivalence.get().wrap(method.getReference());
+      if (memberPool.below(
+          methodKey,
+          false,
+          true,
+          (clazz, ignored) ->
+              !method.getContextType().getPackageName().equals(clazz.getType().getPackageName()))) {
+        return false;
+      }
+      doPublicize(method);
+      return false;
+    }
+
+    assert accessFlags.isPrivate();
+
+    if (accessFlags.isStatic()) {
+      // For private static methods we can just relax the access to public, since
+      // even though JLS prevents from declaring static method in derived class if
+      // an instance method with same signature exists in superclass, JVM actually
+      // does not take into account access of the static methods.
+      doPublicize(method);
+      return false;
+    }
+
+    // We can't publicize private instance methods in interfaces or methods that are copied from
+    // interfaces to lambda-desugared classes because this will be added as a new default method.
+    // TODO(b/111118390): It might be possible to transform it into static methods, though.
+    if (method.getHolder().isInterface() || accessFlags.isSynthetic()) {
+      return false;
+    }
+
+    boolean wasSeen = methodPoolCollection.markIfNotSeen(method.getHolder(), method.getReference());
+    if (wasSeen) {
+      // We can't do anything further because even renaming is not allowed due to the keep rule.
+      if (!appView.appInfo().isMinificationAllowed(method)) {
+        return false;
+      }
+      // TODO(b/111118390): Renaming will enable more private instance methods to be publicized.
+      return false;
+    }
+    lensBuilder.add(method.getReference());
+    accessFlags.promoteToFinal();
+    doPublicize(method);
+    // The method just became public and is therefore not a library override.
+    definition.setLibraryMethodOverride(OptionalBool.FALSE);
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
index 6304490..c197b85 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifier.java
@@ -61,7 +61,9 @@
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
     timing.begin("Access modification");
-    if (appView.options().getAccessModifierOptions().isAccessModificationEnabled()) {
+    AccessModifierOptions accessModifierOptions = appView.options().getAccessModifierOptions();
+    if (accessModifierOptions.isAccessModificationEnabled()
+        && !accessModifierOptions.isLegacyAccessModifierEnabled()) {
       new AccessModifier(appView)
           .processStronglyConnectedComponents(executorService)
           .installLens(executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
index 7644cb3..81c1f03 100644
--- a/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
+++ b/src/main/java/com/android/tools/r8/optimize/accessmodification/AccessModifierOptions.java
@@ -5,9 +5,14 @@
 package com.android.tools.r8.optimize.accessmodification;
 
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SystemPropertyUtils;
 
 public class AccessModifierOptions {
 
+  private boolean enableLegacyAccessModifier =
+      SystemPropertyUtils.parseSystemPropertyOrDefault(
+          "com.android.tools.r8.accessmodification.legacy", false);
+
   // TODO(b/131130038): Do not allow accessmodification when kept.
   private boolean forceModifyPackagePrivateAndProtectedMethods = true;
 
@@ -25,6 +30,9 @@
     if (isAccessModificationRulePresent()) {
       return true;
     }
+    if (isLegacyAccessModifierEnabled()) {
+      return false;
+    }
     // TODO(b/288062771): Enable access modification by default for L8.
     return options.synthesizedClassPrefix.isEmpty()
         && !options.forceProguardCompatibility
@@ -45,4 +53,8 @@
     this.forceModifyPackagePrivateAndProtectedMethods =
         forceModifyPackagePrivateAndProtectedMethods;
   }
+
+  public boolean isLegacyAccessModifierEnabled() {
+    return enableLegacyAccessModifier;
+  }
 }