Parse references to library R fields in R.txt

Now when an application builds against a beta or preview SDK, if an application defines a styleable that references Android resources that have not had their resource ids finalized, the styleable will reference the android.R.attr fields of the non-finalized resources rather than hard code the non-finalized resource ids in the styleable.

IntArrayFieldInitializer.java must be able to parse these static field references and generate the correct source and byte code to read the attribute resource id from the android.R class.

RELNOTES: None
PiperOrigin-RevId: 368012174
diff --git a/src/test/java/com/google/devtools/build/android/resources/BUILD b/src/test/java/com/google/devtools/build/android/resources/BUILD
index 5a7b88e..789953f 100644
--- a/src/test/java/com/google/devtools/build/android/resources/BUILD
+++ b/src/test/java/com/google/devtools/build/android/resources/BUILD
@@ -15,7 +15,9 @@
 java_test(
     name = "RClassGeneratorTest",
     size = "small",
-    srcs = glob(["*.java"]),
+    srcs = glob([
+        "**/*.java",
+    ]),
     deps = [
         "//src/tools/android/java/com/google/devtools/build/android/resources",
         "//third_party:android_common_25_0_0",
diff --git a/src/test/java/com/google/devtools/build/android/resources/RClassGeneratorTest.java b/src/test/java/com/google/devtools/build/android/resources/RClassGeneratorTest.java
index 193e36a..6df5c4c 100644
--- a/src/test/java/com/google/devtools/build/android/resources/RClassGeneratorTest.java
+++ b/src/test/java/com/google/devtools/build/android/resources/RClassGeneratorTest.java
@@ -315,16 +315,20 @@
             "int attr another_attr 0x7f010005",
             "int attr zoo 0x7f010006",
             // Test several > 5 elements, clinit must use bytecodes other than iconst_0 to 5.
-            "int[] styleable ActionButton { 0x010100f2, 0x7f010001, 0x7f010002, 0x7f010003, "
-                + "0x7f010004, 0x7f010005, 0x7f010006 }",
+            "int[] styleable ActionButton { 0x010100f2, "
+                + "com.google.devtools.build.android.resources.android.R.Attr.staged, "
+                + "com.google.devtools.build.android.resources.android.R$Attr.stagedOther, "
+                + "0x7f010001, 0x7f010002, 0x7f010003, 0x7f010004, 0x7f010005, 0x7f010006 }",
             // The array indices of each attribute.
             "int styleable ActionButton_android_layout 0",
-            "int styleable ActionButton_another_attr 5",
-            "int styleable ActionButton_attr 4",
-            "int styleable ActionButton_bar 1",
-            "int styleable ActionButton_baz 2",
-            "int styleable ActionButton_fox 3",
-            "int styleable ActionButton_zoo 6");
+            "int styleable ActionButton_android_staged 1",
+            "int styleable ActionButton_android_stagedOther 2",
+            "int styleable ActionButton_another_attr 7",
+            "int styleable ActionButton_attr 6",
+            "int styleable ActionButton_bar 3",
+            "int styleable ActionButton_baz 4",
+            "int styleable ActionButton_fox 5",
+            "int styleable ActionButton_zoo 8");
     ResourceSymbols symbolsInLibrary = symbolValues;
     Path out = temp.resolve("classes");
     Files.createDirectories(out);
@@ -357,17 +361,21 @@
         outerClass,
         ImmutableMap.<String, Integer>builder()
             .put("ActionButton_android_layout", 0)
-            .put("ActionButton_bar", 1)
-            .put("ActionButton_baz", 2)
-            .put("ActionButton_fox", 3)
-            .put("ActionButton_attr", 4)
-            .put("ActionButton_another_attr", 5)
-            .put("ActionButton_zoo", 6)
+            .put("ActionButton_android_staged", 1)
+            .put("ActionButton_android_stagedOther", 2)
+            .put("ActionButton_bar", 3)
+            .put("ActionButton_baz", 4)
+            .put("ActionButton_fox", 5)
+            .put("ActionButton_attr", 6)
+            .put("ActionButton_another_attr", 7)
+            .put("ActionButton_zoo", 8)
             .build(),
         ImmutableMap.<String, List<Integer>>of(
             "ActionButton",
             ImmutableList.of(
                 0x010100f2,
+                0x0101ff00,
+                0x0101ff01,
                 0x7f010001,
                 0x7f010002,
                 0x7f010003,
@@ -465,7 +473,8 @@
       ImmutableMap<String, List<Integer>> intArrayFields,
       boolean areFieldsFinal)
       throws Exception {
-    try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {baseDir.toUri().toURL()})) {
+    try (URLClassLoader urlClassLoader =
+        new URLClassLoader(new URL[] {baseDir.toUri().toURL()}, getClass().getClassLoader())) {
       Class<?> innerClass = urlClassLoader.loadClass(expectedClassName);
       assertThat(innerClass.getSuperclass()).isEqualTo(Object.class);
       assertThat(innerClass.getEnclosingClass().toString()).isEqualTo(outerClass.toString());
diff --git a/src/test/java/com/google/devtools/build/android/resources/android/R.java b/src/test/java/com/google/devtools/build/android/resources/android/R.java
new file mode 100644
index 0000000..5940e4d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/android/resources/android/R.java
@@ -0,0 +1,27 @@
+// Copyright 2020 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.resources.android;
+
+/** Example framework SDK with resources that have not had their resource ids finalized yet. */
+public final class R {
+  /** Attribute resources. */
+  public static final class Attr {
+    public static int staged = 0x0101ff00;
+    public static int stagedOther = 0x0101ff01;
+
+    private Attr() {}
+  }
+
+  private R() {}
+}
diff --git a/src/tools/android/java/com/google/devtools/build/android/PlaceholderIdFieldInitializerBuilder.java b/src/tools/android/java/com/google/devtools/build/android/PlaceholderIdFieldInitializerBuilder.java
index 4841eb4..5bf8bd0 100644
--- a/src/tools/android/java/com/google/devtools/build/android/PlaceholderIdFieldInitializerBuilder.java
+++ b/src/tools/android/java/com/google/devtools/build/android/PlaceholderIdFieldInitializerBuilder.java
@@ -352,7 +352,9 @@
               dependencyInfo,
               linkageInfo.visibility(),
               field,
-              ImmutableList.copyOf(arrayInitMap.values())));
+              arrayInitMap.values().stream()
+                  .map(IntArrayFieldInitializer.IntegerValue::new)
+                  .collect(ImmutableList.toImmutableList())));
       int index = 0;
       for (String attr : arrayInitMap.keySet()) {
         initList.add(
diff --git a/src/tools/android/java/com/google/devtools/build/android/resources/IntArrayFieldInitializer.java b/src/tools/android/java/com/google/devtools/build/android/resources/IntArrayFieldInitializer.java
index 5ef5506..02792d7 100644
--- a/src/tools/android/java/com/google/devtools/build/android/resources/IntArrayFieldInitializer.java
+++ b/src/tools/android/java/com/google/devtools/build/android/resources/IntArrayFieldInitializer.java
@@ -17,9 +17,12 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.devtools.build.android.DependencyInfo;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassWriter;
@@ -33,16 +36,140 @@
 
   private static final String DESC = "[I";
 
+  /** Represents a value that can be encoded into an int[] field initializer. */
+  public interface IntArrayValue {
+    public void pushValueOntoStack(InstructionAdapter insts);
+
+    public String sourceRepresentation();
+  }
+
+  /** Represents an integer primitive. */
+  public static class IntegerValue implements IntArrayValue {
+    private final int value;
+
+    public IntegerValue(int value) {
+      this.value = value;
+    }
+
+    @Override
+    public void pushValueOntoStack(InstructionAdapter insts) {
+      insts.iconst(value);
+    }
+
+    @Override
+    public String sourceRepresentation() {
+      return String.format("0x%x", value);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof IntegerValue) {
+        IntegerValue other = (IntegerValue) obj;
+        return value == other.value;
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Integer.hashCode(value);
+    }
+
+    @Override
+    public String toString() {
+      return Integer.toString(value);
+    }
+  }
+
+  /** Represents an reference to a static field that holds an integer primitive. */
+  public static class StaticIntFieldReference implements IntArrayValue {
+    private final String className;
+    private final String fieldName;
+
+    private StaticIntFieldReference(String className, String fieldName) {
+      this.className = className;
+      this.fieldName = fieldName;
+    }
+
+    public static StaticIntFieldReference parse(String s) {
+      final int fieldSep = s.lastIndexOf('.');
+      if (fieldSep < 0) {
+        throw new IllegalArgumentException("Unable to parse field reference from '" + s + "'");
+      }
+      final String className = s.substring(0, fieldSep);
+      final String fieldName = s.substring(fieldSep + 1);
+      if (className.isEmpty() || fieldName.isEmpty()) {
+        throw new IllegalArgumentException(
+            "Unable to extract class and field name from '" + s + "'");
+      }
+      return new StaticIntFieldReference(className, fieldName);
+    }
+
+    @Override
+    public void pushValueOntoStack(InstructionAdapter insts) {
+      // The syntax of class names that appear in class file structures differs from the syntax of
+      // class names in source code.
+      // See: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2.1
+
+      final List<String> parts = Splitter.on('.').splitToList(className);
+
+      // If the class name ends in R.[type], replace the ending with R$[type] since [type] is a
+      // class nested within the R class.
+      final boolean replacePeriod = !Iterables.getLast(parts).startsWith("R$");
+
+      final StringBuilder asmClassName = new StringBuilder();
+      for (int i = 0, n = parts.size(); i < n; i++) {
+        final String part = parts.get(i);
+        asmClassName.append(part);
+        if (i == n - 2 && replacePeriod && part.equals("R")) {
+          asmClassName.append('$');
+          continue;
+        }
+        if (i != n - 1) {
+          // Replace all package seperating periods with forward slashes.
+          asmClassName.append('/');
+        }
+      }
+
+      insts.getstatic(asmClassName.toString(), fieldName, "I");
+    }
+
+    @Override
+    public String sourceRepresentation() {
+      return String.format(Locale.US, "%s.%s", className, fieldName);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(className, fieldName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof StaticIntFieldReference) {
+        StaticIntFieldReference other = (StaticIntFieldReference) obj;
+        return Objects.equals(className, other.className)
+            && Objects.equals(fieldName, other.fieldName);
+      }
+      return false;
+    }
+
+    @Override
+    public String toString() {
+      return sourceRepresentation();
+    }
+  }
+
   private final DependencyInfo dependencyInfo;
   private final Visibility visibility;
   private final String fieldName;
-  private final ImmutableList<Integer> values;
+  private final ImmutableList<IntArrayValue> values;
 
   private IntArrayFieldInitializer(
       DependencyInfo dependencyInfo,
       Visibility visibility,
       String fieldName,
-      ImmutableList<Integer> values) {
+      ImmutableList<IntArrayValue> values) {
     this.dependencyInfo = dependencyInfo;
     this.visibility = visibility;
     this.fieldName = fieldName;
@@ -57,11 +184,17 @@
     if (value.length() < 4) {
       return of(dependencyInfo, visibility, fieldName, ImmutableList.of());
     }
-    ImmutableList.Builder<Integer> intValues = ImmutableList.builder();
+    ImmutableList.Builder<IntArrayValue> intValues = ImmutableList.builder();
     String trimmedValue = value.substring(2, value.length() - 2);
     Iterable<String> valueStrings = Splitter.on(',').trimResults().split(trimmedValue);
     for (String valueString : valueStrings) {
-      intValues.add(Integer.decode(valueString));
+      IntArrayValue elementValue;
+      try {
+        elementValue = StaticIntFieldReference.parse(valueString);
+      } catch (IllegalArgumentException e) {
+        elementValue = new IntegerValue(Integer.decode(valueString));
+      }
+      intValues.add(elementValue);
     }
     return of(dependencyInfo, visibility, fieldName, intValues.build());
   }
@@ -70,7 +203,7 @@
       DependencyInfo dependencyInfo,
       Visibility visibility,
       String fieldName,
-      ImmutableList<Integer> values) {
+      ImmutableList<IntArrayValue> values) {
     return new IntArrayFieldInitializer(dependencyInfo, visibility, fieldName, values);
   }
 
@@ -102,10 +235,10 @@
     insts.iconst(values.size());
     insts.newarray(Type.INT_TYPE);
     int curIndex = 0;
-    for (Integer value : values) {
+    for (IntArrayValue value : values) {
       insts.dup();
       insts.iconst(curIndex);
-      insts.iconst(value);
+      value.pushValueOntoStack(insts);
       insts.astore(Type.INT_TYPE);
       ++curIndex;
     }
@@ -119,12 +252,12 @@
   public void writeInitSource(Writer writer, boolean finalFields) throws IOException {
     StringBuilder builder = new StringBuilder();
     boolean first = true;
-    for (Integer attrId : values) {
+    for (IntArrayValue value : values) {
       if (first) {
         first = false;
-        builder.append(String.format("0x%x", attrId));
+        builder.append(value.sourceRepresentation());
       } else {
-        builder.append(String.format(", 0x%x", attrId));
+        builder.append(String.format(Locale.US, ", %s", value.sourceRepresentation()));
       }
     }