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( 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