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