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