Apply package renamings to resource filenames

If the package "foo.bar.baz" is renamed to "a.a.a", then all resources in the directory foo/bar/baz/ will be moved to a/a/a/ in the output.

This CL extends NamingLens with a method lookupPackageName and applies the package renamings to the resource directories.

Change-Id: I9c604ef347dbd0666b4ea486a4668e1ba6523bfd
diff --git a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
index e1af350..673fa6e 100644
--- a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
+++ b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
@@ -190,6 +190,11 @@
         if (currentChar == getClassNameSeparator()
             && !eof(position + 1)
             && Character.isJavaIdentifierPart(contents.charAt(position + 1))) {
+          if (allowRenamingOfPrefixes()
+              && shouldRecordPrefix(currentChar)
+              && isRenamingCandidate(start, position)) {
+            prefixEndPositionsExclusive.push(position);
+          }
           // Consume the dot and the Java identifier part that follows the dot.
           position += 2;
           continue;
@@ -205,7 +210,7 @@
         while (!prefixEndPositionsExclusive.isEmpty() && !renamingSucceeded) {
           int prefixEndExclusive = prefixEndPositionsExclusive.popInt();
           assert isRenamingCandidate(start, prefixEndExclusive);
-          renamingSucceeded = renameJavaTypeInRange(start, prefixEndExclusive);
+          renamingSucceeded = handlePrefix(start, prefixEndExclusive);
         }
       }
 
@@ -217,7 +222,7 @@
     }
 
     // Returns true if the Java type in the range [from; toExclusive[ was renamed.
-    private boolean renameJavaTypeInRange(int from, int toExclusive) {
+    protected boolean renameJavaTypeInRange(int from, int toExclusive) {
       String javaType = contents.substring(from, toExclusive);
       if (getClassNameSeparator() != '.') {
         javaType = javaType.replace(getClassNameSeparator(), '.');
@@ -246,12 +251,34 @@
       return false;
     }
 
+    // Returns true if the Java package in the range [from; toExclusive[ was renamed.
+    protected boolean renameJavaPackageInRange(int from, int toExclusive) {
+      String javaPackage = contents.substring(from, toExclusive);
+      if (getClassNameSeparator() != '/') {
+        javaPackage = javaPackage.replace(getClassNameSeparator(), '/');
+      }
+      String minifiedJavaPackage = namingLense.lookupPackageName(javaPackage);
+      if (!javaPackage.equals(minifiedJavaPackage)) {
+        outputRangeFromInput(outputFrom, from);
+        outputJavaType(
+            getClassNameSeparator() != '/'
+                ? minifiedJavaPackage.replace('/', getClassNameSeparator())
+                : minifiedJavaPackage);
+        outputFrom = toExclusive;
+        changed = true;
+        return true;
+      }
+      return false;
+    }
+
     protected abstract char getClassNameSeparator();
 
     protected abstract boolean allowRenamingOfPrefixes();
 
     protected abstract boolean shouldRecordPrefix(char c);
 
+    protected abstract boolean handlePrefix(int from, int toExclusive);
+
     protected abstract boolean isRenamingCandidate(int from, int toExclusive);
 
     private void outputRangeFromInput(int from, int toExclusive) {
@@ -295,6 +322,11 @@
     }
 
     @Override
+    protected boolean handlePrefix(int from, int toExclusive) {
+      throw new Unreachable();
+    }
+
+    @Override
     public boolean isRenamingCandidate(int from, int toExclusive) {
       // If the Java type starts with '-' or '.', it should not be renamed.
       return (from <= 0 || !isDashOrDot(contents.charAt(from - 1)))
@@ -328,6 +360,15 @@
     }
 
     @Override
+    protected boolean handlePrefix(int from, int toExclusive) {
+      assert !eof(toExclusive);
+      if (contents.charAt(toExclusive) == '/') {
+        return renameJavaPackageInRange(from, toExclusive);
+      }
+      return renameJavaTypeInRange(from, toExclusive);
+    }
+
+    @Override
     public boolean isRenamingCandidate(int from, int toExclusive) {
       return from == 0 && !eof(toExclusive);
     }
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 01fff01..bf5dd5d 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import java.lang.reflect.GenericSignatureFormatError;
@@ -38,6 +39,7 @@
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -92,7 +94,18 @@
     states.computeIfAbsent("", k -> topLevelState);
   }
 
-  Map<DexType, DexString> computeRenaming(Timing timing) {
+  static class ClassRenaming {
+    protected final Map<String, String> packageRenaming;
+    protected final Map<DexType, DexString> classRenaming;
+
+    private ClassRenaming(
+        Map<DexType, DexString> classRenaming, Map<String, String> packageRenaming) {
+      this.classRenaming = classRenaming;
+      this.packageRenaming = packageRenaming;
+    }
+  }
+
+  ClassRenaming computeRenaming(Timing timing) {
     // Use deterministic class order to make sure renaming is deterministic.
     Iterable<DexProgramClass> classes = appInfo.classesWithDeterministicOrder();
     // Collect names we have to keep.
@@ -128,7 +141,19 @@
     appInfo.dexItemFactory.forAllTypes(this::renameArrayTypeIfNeeded);
     timing.end();
 
-    return Collections.unmodifiableMap(renaming);
+    return new ClassRenaming(Collections.unmodifiableMap(renaming), getPackageRenaming());
+  }
+
+  private Map<String, String> getPackageRenaming() {
+    ImmutableMap.Builder<String, String> packageRenaming = ImmutableMap.builder();
+    for (Entry<String, Namespace> entry : states.entrySet()) {
+      String originalPackageName = entry.getKey();
+      String minifiedPackageName = entry.getValue().getPackageName();
+      if (!minifiedPackageName.equals(originalPackageName)) {
+        packageRenaming.put(originalPackageName, minifiedPackageName);
+      }
+    }
+    return packageRenaming.build();
   }
 
   private void renameDanglingTypes(DexClass clazz) {
@@ -406,6 +431,7 @@
 
   private class Namespace {
 
+    private final String packageName;
     private final char[] packagePrefix;
     private int typeCounter = 1;
     private int packageCounter = 1;
@@ -417,6 +443,7 @@
     }
 
     Namespace(String packageName, String separator) {
+      this.packageName = packageName;
       this.packagePrefix = ("L" + packageName
           // L or La/b/ (or La/b/C$)
           + (packageName.isEmpty() ? "" : separator))
@@ -425,6 +452,10 @@
       this.classDictionaryIterator = classDictionary.iterator();
     }
 
+    public String getPackageName() {
+      return packageName;
+    }
+
     private String nextSuggestedNameForClass() {
       StringBuilder nextName = new StringBuilder();
       if (classDictionaryIterator.hasNext()) {
@@ -466,7 +497,6 @@
       usedPackagePrefixes.add(candidate);
       return candidate;
     }
-
   }
 
   private class GenericSignatureRewriter implements GenericSignatureAction<DexType> {
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index 382ac92..10aa8af 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.naming.ClassNameMinifier.ClassRenaming;
 import com.android.tools.r8.naming.MethodNameMinifier.MethodRenaming;
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
@@ -42,8 +43,8 @@
   public NamingLens run(Timing timing) {
     assert options.enableMinification;
     timing.begin("MinifyClasses");
-    Map<DexType, DexString> classRenaming =
-        new ClassNameMinifier(appInfo, rootSet, options).computeRenaming(timing);
+    ClassNameMinifier classNameMinifier = new ClassNameMinifier(appInfo, rootSet, options);
+    ClassRenaming classRenaming = classNameMinifier.computeRenaming(timing);
     timing.end();
     timing.begin("MinifyMethods");
     MethodRenaming methodRenaming =
@@ -53,7 +54,12 @@
     Map<DexField, DexString> fieldRenaming =
         new FieldNameMinifier(appInfo, rootSet, options).computeRenaming(timing);
     timing.end();
-    NamingLens lens = new MinifiedRenaming(classRenaming, methodRenaming, fieldRenaming, appInfo);
+    NamingLens lens =
+        new MinifiedRenaming(
+            classRenaming,
+            methodRenaming,
+            fieldRenaming,
+            appInfo);
     timing.begin("MinifyIdentifiers");
     new IdentifierMinifier(appInfo, options.proguardConfiguration.getAdaptClassStrings(), lens)
         .run();
@@ -64,21 +70,28 @@
   private static class MinifiedRenaming extends NamingLens {
 
     private final AppInfo appInfo;
+    private final Map<String, String> packageRenaming;
     private final Map<DexItem, DexString> renaming = new IdentityHashMap<>();
 
     private MinifiedRenaming(
-        Map<DexType, DexString> classRenaming,
+        ClassRenaming classRenaming,
         MethodRenaming methodRenaming,
         Map<DexField, DexString> fieldRenaming,
         AppInfo appInfo) {
       this.appInfo = appInfo;
-      renaming.putAll(classRenaming);
+      this.packageRenaming = classRenaming.packageRenaming;
+      renaming.putAll(classRenaming.classRenaming);
       renaming.putAll(methodRenaming.renaming);
       renaming.putAll(methodRenaming.callSiteRenaming);
       renaming.putAll(fieldRenaming);
     }
 
     @Override
+    public String lookupPackageName(String packageName) {
+      return packageRenaming.getOrDefault(packageName, packageName);
+    }
+
+    @Override
     public DexString lookupDescriptor(DexType type) {
       return renaming.getOrDefault(type, type.descriptor);
     }
diff --git a/src/main/java/com/android/tools/r8/naming/NamingLens.java b/src/main/java/com/android/tools/r8/naming/NamingLens.java
index f1f13b3..47459c0 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingLens.java
@@ -30,6 +30,8 @@
  */
 public abstract class NamingLens {
 
+  public abstract String lookupPackageName(String packageName);
+
   public abstract DexString lookupDescriptor(DexType type);
 
   public abstract String lookupSimpleName(DexType inner, DexString innerName);
@@ -99,6 +101,11 @@
     }
 
     @Override
+    public String lookupPackageName(String packageName) {
+      return packageName;
+    }
+
+    @Override
     void forAllRenamedTypes(Consumer<DexType> consumer) {
       // Intentionally left empty.
     }
diff --git a/src/test/examples/adaptresourcefilenames/TestClass.java b/src/test/examples/adaptresourcefilenames/TestClass.java
index 14aa7f8..a5e96e5 100644
--- a/src/test/examples/adaptresourcefilenames/TestClass.java
+++ b/src/test/examples/adaptresourcefilenames/TestClass.java
@@ -4,10 +4,14 @@
 
 package adaptresourcefilenames;
 
+import adaptresourcefilenames.pkg.C;
+import adaptresourcefilenames.pkg.innerpkg.D;
+
 public class TestClass {
 
   public static void main(String[] args) {
-    B obj = new B();
-    obj.method();
+    new B().method();
+    new C().method();
+    new D().method();
   }
 }
diff --git a/src/test/examples/adaptresourcefilenames/pkg/C.java b/src/test/examples/adaptresourcefilenames/pkg/C.java
new file mode 100644
index 0000000..93d4446
--- /dev/null
+++ b/src/test/examples/adaptresourcefilenames/pkg/C.java
@@ -0,0 +1,12 @@
+// 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 adaptresourcefilenames.pkg;
+
+public class C {
+
+  public void method() {
+    System.out.println("In C.method()");
+  }
+}
diff --git a/src/test/examples/adaptresourcefilenames/pkg/innerpkg/D.java b/src/test/examples/adaptresourcefilenames/pkg/innerpkg/D.java
new file mode 100644
index 0000000..2107547
--- /dev/null
+++ b/src/test/examples/adaptresourcefilenames/pkg/innerpkg/D.java
@@ -0,0 +1,12 @@
+// 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 adaptresourcefilenames.pkg.innerpkg;
+
+public class D {
+
+  public void method() {
+    System.out.println("In D.method()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
index 33a297b..dbe6541 100644
--- a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
@@ -368,7 +368,27 @@
             "adaptresourcefilenames/prefixB.txt",
             // Filename with numeric prefix and extension.
             "adaptresourcefilenames/42TestClass.txt",
-            "adaptresourcefilenames/42B.txt");
+            "adaptresourcefilenames/42B.txt",
+            //
+            // PACKAGE RENAMING TESTS:
+            //
+            // Filename that matches a type, but only the directory should be renamed.
+            "adaptresourcefilenames/pkg/C",
+            // Filename that matches a type that should be renamed.
+            "adaptresourcefilenames/pkg/C.txt",
+            // Filename that does not match a type, but where the directory should be renamed.
+            "adaptresourcefilenames/pkg/file.txt",
+            // Filename that does not match a type, but where a directory-prefix should be renamed.
+            "adaptresourcefilenames/pkg/directory/file.txt",
+            // Filename that matches a type, but only the directory should be renamed.
+            "adaptresourcefilenames/pkg/innerpkg/D",
+            // Filename that matches a type that should be renamed.
+            "adaptresourcefilenames/pkg/innerpkg/D.txt",
+            // Filename that does not match a type, but where the directory should be renamed.
+            "adaptresourcefilenames/pkg/innerpkg/file.txt",
+            // Filename that does not match a type, but where a directory-prefix should be renamed.
+            "adaptresourcefilenames/pkg/innerpkg/directory/file.txt"
+            );
     return filenames
         .stream()
         .map(filename -> DataEntryResource.fromBytes(new byte[0], filename, Origin.unknown()))
@@ -424,6 +444,17 @@
         typeName = "adaptresourcefilenames.B";
         suffix = " suffix.txt";
         break;
+        //
+        // PACKAGE RENAMING TESTS
+        //
+      case "adaptresourcefilenames/pkg/C.txt":
+        typeName = "adaptresourcefilenames.pkg.C";
+        suffix = ".txt";
+        break;
+      case "adaptresourcefilenames/pkg/innerpkg/D.txt":
+        typeName = "adaptresourcefilenames.pkg.innerpkg.D";
+        suffix = ".txt";
+        break;
     }
     if (typeName != null) {
       String renamedName = mapper.getObfuscatedToOriginalMapping().inverse().get(typeName);
@@ -431,6 +462,44 @@
       assertNotEquals(typeName, renamedName);
       return renamedName.replace('.', '/') + suffix;
     }
+    // Renamings for files in directories that match packages that have been renamed,
+    // but where the filename itself should not be renamed.
+    String samePackageAsType = null;
+    switch (filename) {
+      case "adaptresourcefilenames/pkg/C":
+        samePackageAsType = "adaptresourcefilenames.pkg.C";
+        suffix = "C";
+        break;
+      case "adaptresourcefilenames/pkg/file.txt":
+        samePackageAsType = "adaptresourcefilenames.pkg.C";
+        suffix = "file.txt";
+        break;
+      case "adaptresourcefilenames/pkg/directory/file.txt":
+        samePackageAsType = "adaptresourcefilenames.pkg.C";
+        suffix = "directory/file.txt";
+        break;
+      case "adaptresourcefilenames/pkg/innerpkg/D":
+        samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D";
+        suffix = "D";
+        break;
+      case "adaptresourcefilenames/pkg/innerpkg/file.txt":
+        samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D";
+        suffix = "file.txt";
+        break;
+      case "adaptresourcefilenames/pkg/innerpkg/directory/file.txt":
+        samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D";
+        suffix = "directory/file.txt";
+        break;
+    }
+    if (samePackageAsType != null) {
+      String renamedName = mapper.getObfuscatedToOriginalMapping().inverse().get(samePackageAsType);
+      assertNotNull(renamedName);
+      assertNotEquals(samePackageAsType, renamedName);
+      if (renamedName.contains(".")) {
+        String renamedPackageName = renamedName.substring(0, renamedName.lastIndexOf('.'));
+        return renamedPackageName.replace('.', '/') + "/" + suffix;
+      }
+    }
     return filename;
   }
 }