1 package org.robolectric.manifest; 2 3 import java.util.LinkedHashMap; 4 import java.util.List; 5 import java.util.Map; 6 import org.robolectric.res.ResName; 7 import org.robolectric.res.ResourceTable; 8 import org.robolectric.res.TypedResource; 9 import org.robolectric.res.android.ResTable_config; 10 import org.w3c.dom.NamedNodeMap; 11 import org.w3c.dom.Node; 12 13 public final class MetaData { 14 private final Map<String, Object> valueMap = new LinkedHashMap<>(); 15 private final Map<String, VALUE_TYPE> typeMap = new LinkedHashMap<>(); 16 private boolean initialised; 17 MetaData(List<Node> nodes)18 public MetaData(List<Node> nodes) { 19 for (Node metaNode : nodes) { 20 NamedNodeMap attributes = metaNode.getAttributes(); 21 Node nameAttr = attributes.getNamedItem("android:name"); 22 Node valueAttr = attributes.getNamedItem("android:value"); 23 Node resourceAttr = attributes.getNamedItem("android:resource"); 24 25 if (valueAttr != null) { 26 valueMap.put(nameAttr.getNodeValue(), valueAttr.getNodeValue()); 27 typeMap.put(nameAttr.getNodeValue(), VALUE_TYPE.VALUE); 28 } else if (resourceAttr != null) { 29 valueMap.put(nameAttr.getNodeValue(), resourceAttr.getNodeValue()); 30 typeMap.put(nameAttr.getNodeValue(), VALUE_TYPE.RESOURCE); 31 } 32 } 33 } 34 init(ResourceTable resourceTable, String packageName)35 public void init(ResourceTable resourceTable, String packageName) throws RoboNotFoundException { 36 if (!initialised) { 37 for (Map.Entry<String,VALUE_TYPE> entry : typeMap.entrySet()) { 38 String value = valueMap.get(entry.getKey()).toString(); 39 if (value.startsWith("@")) { 40 ResName resName = ResName.qualifyResName(value.substring(1), packageName, null); 41 42 switch (entry.getValue()) { 43 case RESOURCE: 44 // Was provided by resource attribute, store resource ID 45 valueMap.put(entry.getKey(), resourceTable.getResourceId(resName)); 46 break; 47 case VALUE: 48 // Was provided by value attribute, need to inferFromValue it 49 TypedResource<?> typedRes = resourceTable.getValue(resName, new ResTable_config()); 50 // The typed resource's data is always a String, so need to inferFromValue the value. 51 if (typedRes == null) { 52 throw new RoboNotFoundException(resName.getFullyQualifiedName()); 53 } 54 switch (typedRes.getResType()) { 55 case BOOLEAN: case COLOR: case INTEGER: case FLOAT: 56 valueMap.put(entry.getKey(), parseValue(typedRes.getData().toString())); 57 break; 58 default: 59 valueMap.put(entry.getKey(),typedRes.getData()); 60 } 61 break; 62 } 63 } else if (entry.getValue() == VALUE_TYPE.VALUE) { 64 // Raw value, so inferFromValue it in to the appropriate type and store it 65 valueMap.put(entry.getKey(), parseValue(value)); 66 } 67 } 68 // Finished parsing, mark as initialised 69 initialised = true; 70 } 71 } 72 getValueMap()73 public Map<String, Object> getValueMap() { 74 return valueMap; 75 } 76 77 private enum VALUE_TYPE { 78 RESOURCE, 79 VALUE 80 } 81 parseValue(String value)82 private Object parseValue(String value) { 83 if (value == null) { 84 return null; 85 } else if ("true".equals(value)) { 86 return true; 87 } else if ("false".equals(value)) { 88 return false; 89 } else if (value.startsWith("#")) { 90 // if it's a color, add it and continue 91 try { 92 return getColor(value); 93 } catch (NumberFormatException e) { 94 /* Not a color */ 95 } 96 } else if (value.contains(".")) { 97 // most likely a float 98 try { 99 return Float.parseFloat(value); 100 } catch (NumberFormatException e) { 101 // Not a float 102 } 103 } else { 104 // if it's an int, add it and continue 105 try { 106 return Integer.parseInt(value); 107 } catch (NumberFormatException ei) { 108 // Not an int 109 } 110 } 111 112 // Not one of the above types, keep as String 113 return value; 114 } 115 116 // todo: this is copied from ResourceHelper, dedupe 117 /** 118 * Returns the color value represented by the given string value 119 * @param value the color value 120 * @return the color as an int 121 * @throws NumberFormatException if the conversion failed. 122 */ getColor(String value)123 public static int getColor(String value) { 124 if (value != null) { 125 if (value.startsWith("#") == false) { 126 throw new NumberFormatException( 127 String.format("Color value '%s' must start with #", value)); 128 } 129 130 value = value.substring(1); 131 132 // make sure it's not longer than 32bit 133 if (value.length() > 8) { 134 throw new NumberFormatException(String.format( 135 "Color value '%s' is too long. Format is either" + 136 "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", 137 value)); 138 } 139 140 if (value.length() == 3) { // RGB format 141 char[] color = new char[8]; 142 color[0] = color[1] = 'F'; 143 color[2] = color[3] = value.charAt(0); 144 color[4] = color[5] = value.charAt(1); 145 color[6] = color[7] = value.charAt(2); 146 value = new String(color); 147 } else if (value.length() == 4) { // ARGB format 148 char[] color = new char[8]; 149 color[0] = color[1] = value.charAt(0); 150 color[2] = color[3] = value.charAt(1); 151 color[4] = color[5] = value.charAt(2); 152 color[6] = color[7] = value.charAt(3); 153 value = new String(color); 154 } else if (value.length() == 6) { 155 value = "FF" + value; 156 } 157 158 // this is a RRGGBB or AARRGGBB value 159 160 // Integer.parseInt will fail to inferFromValue strings like "ff191919", so we use 161 // a Long, but cast the result back into an int, since we know that we're only 162 // dealing with 32 bit values. 163 return (int)Long.parseLong(value, 16); 164 } 165 166 throw new NumberFormatException(); 167 } 168 169 } 170