1 package org.robolectric.res;
2 
3 import com.google.common.collect.BiMap;
4 import com.google.common.collect.HashBiMap;
5 import java.lang.reflect.Field;
6 import java.lang.reflect.Modifier;
7 import java.util.HashMap;
8 import java.util.Map;
9 
10 /**
11  * This class rewrites application R class resource values from multiple input R classes to all have unique values
12  * existing within the same ID space, i.e: no resource collisions. This replicates the behaviour of AAPT when building
13  * the final APK.
14  *
15  * IDs are in the format:-
16  *
17  * 0x PPTTEEEE
18  *
19  * where:
20  *
21  * P is unique for the package
22  * T is unique for the type
23  * E is the entry within that type.
24  */
25 class ResourceRemapper {
26 
27   private BiMap<String, Integer> resIds = HashBiMap.create();
28   private ResourceIdGenerator resourceIdGenerator = new ResourceIdGenerator(0x7F);
29 
30   /**
31    * @param primaryRClass - An R class (usually the applications) that can be assumed to have a complete set of IDs. If
32    *                      this is provided then use the values from this class for re-writting all values in follow up
33    *                      calls to {@link #remapRClass(Class)}. If it is not provided the ResourceRemapper will generate
34    *                      its own unique non-conflicting IDs.
35    */
ResourceRemapper(Class<?> primaryRClass)36   ResourceRemapper(Class<?> primaryRClass) {
37     if (primaryRClass != null) {
38       remapRClass(true, primaryRClass);
39     }
40   }
41 
remapRClass(Class<?> rClass)42   void remapRClass(Class<?> rClass) {
43     remapRClass(false, rClass);
44   }
45 
46   /**
47    * @param isPrimary - Only one R class can allow final values and that is the final R class for the application
48    *                  that has had its resource id values generated to include all libraries in its dependency graph
49    *                  and therefore will be the only R file with the complete set of IDs in a unique ID space so we
50    *                  can assume to use the values from this class only. All other R files are partial R files for each
51    *                  library and on non-Android aware build systems like Maven where library R files are not re-written
52    *                  with the final R values we need to rewrite them ourselves.
53    */
remapRClass(boolean isPrimary, Class<?> rClass)54   private void remapRClass(boolean isPrimary, Class<?> rClass) {
55     // Collect all the local attribute id -> name mappings. These are used when processing the stylables to look up
56     // the reassigned values.
57     Map<Integer, String> localAttributeIds = new HashMap<>();
58     for (Class<?> aClass : rClass.getClasses()) {
59       if (aClass.getSimpleName().equals("attr")) {
60         for (Field field : aClass.getFields()) {
61           try {
62             localAttributeIds.put(field.getInt(null), field.getName());
63           } catch (IllegalAccessException e) {
64             throw new RuntimeException("Could not read attr value for " + field.getName(), e);
65           }
66         }
67       }
68     }
69 
70     for (Class<?> innerClass : rClass.getClasses()) {
71       String resourceType = innerClass.getSimpleName();
72       if (!resourceType.startsWith("styleable")) {
73         for (Field field : innerClass.getFields()) {
74           try {
75             if (!isPrimary && Modifier.isFinal(field.getModifiers())) {
76               throw new IllegalArgumentException(rClass + " contains final fields, these will be inlined by the compiler and cannot be remapped.");
77             }
78 
79             String resourceName = resourceType + "/" + field.getName();
80             Integer value = resIds.get(resourceName);
81             if (value != null) {
82               field.setAccessible(true);
83               field.setInt(null, value);
84               resourceIdGenerator.record(field.getInt(null), resourceType, field.getName());
85             } else if (resIds.containsValue(field.getInt(null))) {
86               int remappedValue = resourceIdGenerator.generate(resourceType, field.getName());
87               field.setInt(null, remappedValue);
88               resIds.put(resourceName, remappedValue);
89             } else {
90               if (isPrimary) {
91                 resourceIdGenerator.record(field.getInt(null), resourceType, field.getName());
92                 resIds.put(resourceName, field.getInt(null));
93               } else {
94                 int remappedValue = resourceIdGenerator.generate(resourceType, field.getName());
95                 field.setInt(null, remappedValue);
96                 resIds.put(resourceName, remappedValue);
97               }
98             }
99           } catch (IllegalAccessException e) {
100             throw new IllegalStateException(e);
101           }
102         }
103       }
104     }
105 
106     // Reassign the ids in the style arrays accordingly.
107     for (Class<?> innerClass : rClass.getClasses()) {
108       String resourceType = innerClass.getSimpleName();
109       if (resourceType.startsWith("styleable")) {
110         for (Field field : innerClass.getFields()) {
111           if (field.getType().equals(int[].class)) {
112             try {
113               int[] styleableArray = (int[]) (field.get(null));
114               for (int k = 0; k < styleableArray.length; k++) {
115                 Integer value = resIds.get("attr/" + localAttributeIds.get(styleableArray[k]));
116                 if (value != null) {
117                   styleableArray[k] = value;
118                 }
119               }
120             } catch (IllegalAccessException e) {
121               throw new IllegalStateException(e);
122             }
123           }
124         }
125       }
126     }
127   }
128 }
129