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