1 package com.xtremelabs.robolectric.res; 2 3 import android.content.Context; 4 import android.os.Build; 5 import android.support.v4.app.Fragment; 6 import android.support.v4.app.FragmentActivity; 7 import android.text.TextUtils; 8 import android.util.AttributeSet; 9 import android.view.View; 10 import android.view.ViewGroup; 11 import android.view.ViewParent; 12 import android.widget.FrameLayout; 13 import com.xtremelabs.robolectric.tester.android.util.TestAttributeSet; 14 import com.xtremelabs.robolectric.util.I18nException; 15 import org.w3c.dom.Document; 16 import org.w3c.dom.NamedNodeMap; 17 import org.w3c.dom.Node; 18 import org.w3c.dom.NodeList; 19 20 import java.io.File; 21 import java.lang.reflect.Constructor; 22 import java.lang.reflect.InvocationTargetException; 23 import java.lang.reflect.Method; 24 import java.util.*; 25 26 import static com.xtremelabs.robolectric.Robolectric.shadowOf; 27 28 public class ViewLoader extends XmlLoader { 29 protected Map<String, ViewNode> viewNodesByLayoutName = new HashMap<String, ViewNode>(); 30 private AttrResourceLoader attrResourceLoader; 31 private List<String> qualifierSearchPath; 32 33 public ViewLoader(ResourceExtractor resourceExtractor, AttrResourceLoader attrResourceLoader) { 34 super(resourceExtractor); 35 this.attrResourceLoader = attrResourceLoader; 36 setLayoutQualifierSearchPath(); 37 } 38 39 @Override 40 protected void processResourceXml(File xmlFile, Document document, boolean isSystem) throws Exception { 41 ViewNode topLevelNode = new ViewNode("top-level", new HashMap<String, String>(), isSystem); 42 processChildren(document.getChildNodes(), topLevelNode); 43 String layoutName = xmlFile.getParentFile().getName() + "/" + xmlFile.getName().replace(".xml", ""); 44 if (isSystem) { 45 layoutName = "android:" + layoutName; 46 } 47 viewNodesByLayoutName.put(layoutName, topLevelNode.getChildren().get(0)); 48 } 49 50 private void processChildren(NodeList childNodes, ViewNode parent) { 51 for (int i = 0; i < childNodes.getLength(); i++) { 52 Node node = childNodes.item(i); 53 processNode(node, parent); 54 } 55 } 56 57 private void processNode(Node node, ViewNode parent) { 58 String name = node.getNodeName(); 59 NamedNodeMap attributes = node.getAttributes(); 60 Map<String, String> attrMap = new HashMap<String, String>(); 61 if (attributes != null) { 62 int length = attributes.getLength(); 63 for (int i = 0; i < length; i++) { 64 Node attr = attributes.item(i); 65 attrMap.put(attr.getNodeName(), attr.getNodeValue()); 66 } 67 } 68 69 if (name.equals("requestFocus")) { 70 parent.attributes.put("android:focus", "true"); 71 parent.requestFocusOverride = true; 72 } else if (!name.startsWith("#")) { 73 ViewNode viewNode = new ViewNode(name, attrMap, parent.isSystem); 74 if (parent != null) parent.addChild(viewNode); 75 76 processChildren(node.getChildNodes(), viewNode); 77 } 78 } 79 80 public View inflateView(Context context, String key) { 81 return inflateView(context, key, null); 82 } 83 84 public View inflateView(Context context, String key, View parent) { 85 return inflateView(context, key, null, parent); 86 } 87 88 public View inflateView(Context context, int resourceId, View parent) { 89 return inflateView(context, resourceExtractor.getResourceName(resourceId), parent); 90 } 91 92 private View inflateView(Context context, String layoutName, Map<String, String> attributes, View parent) { 93 ViewNode viewNode = getViewNodeByLayoutName(layoutName); 94 if (viewNode == null) { 95 throw new RuntimeException("Could not find layout " + layoutName); 96 } 97 try { 98 if (attributes != null) { 99 for (Map.Entry<String, String> entry : attributes.entrySet()) { 100 if (!entry.getKey().equals("layout")) { 101 viewNode.attributes.put(entry.getKey(), entry.getValue()); 102 } 103 } 104 } 105 return viewNode.inflate(context, parent); 106 } catch (I18nException e) { 107 throw e; 108 } catch (Exception e) { 109 throw new RuntimeException("error inflating " + layoutName, e); 110 } 111 } 112 113 private ViewNode getViewNodeByLayoutName(String layoutName) { 114 if (layoutName.startsWith("layout/")) { 115 String rawLayoutName = layoutName.substring("layout/".length()); 116 for (String qualifier : qualifierSearchPath) { 117 for (int version = Math.max(Build.VERSION.SDK_INT, 0); version >= 0; version--) { 118 ViewNode foundNode = findLayoutViewNode(qualifier, version, rawLayoutName); 119 if (foundNode != null) { 120 return foundNode; 121 } 122 } 123 } 124 } 125 return viewNodesByLayoutName.get(layoutName); 126 } 127 128 private ViewNode findLayoutViewNode(String qualifier, int version, String rawLayoutName) { 129 StringBuilder name = new StringBuilder("layout"); 130 if (!TextUtils.isEmpty(qualifier)) { 131 name.append("-").append(qualifier); 132 } 133 if (version > 0) { 134 name.append("-v").append(version); 135 } 136 name.append("/").append(rawLayoutName); 137 return viewNodesByLayoutName.get(name.toString()); 138 } 139 140 public void setLayoutQualifierSearchPath(String... locations) { 141 qualifierSearchPath = Arrays.asList(locations); 142 if (!qualifierSearchPath.contains("")) { 143 qualifierSearchPath = new ArrayList<String>(qualifierSearchPath); 144 qualifierSearchPath.add(""); 145 } 146 } 147 148 public class ViewNode { 149 private String name; 150 private final Map<String, String> attributes; 151 152 private List<ViewNode> children = new ArrayList<ViewNode>(); 153 boolean requestFocusOverride = false; 154 boolean isSystem = false; 155 156 public ViewNode(String name, Map<String, String> attributes, boolean isSystem) { 157 this.name = name; 158 this.attributes = attributes; 159 this.isSystem = isSystem; 160 } 161 162 public List<ViewNode> getChildren() { 163 return children; 164 } 165 166 public void addChild(ViewNode viewNode) { 167 children.add(viewNode); 168 } 169 170 public View inflate(Context context, View parent) throws Exception { 171 View view = create(context, (ViewGroup) parent); 172 173 for (ViewNode child : children) { 174 child.inflate(context, view); 175 } 176 177 invokeOnFinishInflate(view); 178 return view; 179 } 180 181 private void invokeOnFinishInflate(View view) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 182 Method onFinishInflate = View.class.getDeclaredMethod("onFinishInflate"); 183 onFinishInflate.setAccessible(true); 184 onFinishInflate.invoke(view); 185 } 186 187 private View create(Context context, ViewGroup parent) throws Exception { 188 if (name.equals("include")) { 189 String layout = attributes.get("layout"); 190 View view = inflateView(context, layout.substring(1), attributes, parent); 191 return view; 192 } else if (name.equals("merge")) { 193 return parent; 194 } else if (name.equals("fragment")) { 195 View fragment = constructFragment(context); 196 addToParent(parent, fragment); 197 return fragment; 198 } else { 199 applyFocusOverride(parent); 200 View view = constructView(context); 201 addToParent(parent, view); 202 shadowOf(view).applyFocus(); 203 return view; 204 } 205 } 206 207 private FrameLayout constructFragment(Context context) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { 208 TestAttributeSet attributeSet = new TestAttributeSet(attributes, resourceExtractor, attrResourceLoader, View.class, isSystem); 209 if (strictI18n) { 210 attributeSet.validateStrictI18n(); 211 } 212 213 Class<? extends Fragment> clazz = loadFragmentClass(attributes.get("android:name")); 214 Fragment fragment = ((Constructor<? extends Fragment>) clazz.getConstructor()).newInstance(); 215 if (!(context instanceof FragmentActivity)) { 216 throw new RuntimeException("Cannot inflate a fragment unless the activity is a FragmentActivity"); 217 } 218 219 FragmentActivity activity = (FragmentActivity) context; 220 221 String tag = attributeSet.getAttributeValue("android", "tag"); 222 int id = attributeSet.getAttributeResourceValue("android", "id", 0); 223 // TODO: this should probably be changed to call TestFragmentManager.addFragment so that the 224 // inflated fragments don't get started twice (once in the commit, and once in ShadowFragmentActivity's 225 // onStart() 226 activity.getSupportFragmentManager().beginTransaction().add(id, fragment, tag).commit(); 227 228 View view = fragment.getView(); 229 230 FrameLayout container = new FrameLayout(context); 231 container.setId(id); 232 container.addView(view); 233 return container; 234 } 235 236 private void addToParent(ViewGroup parent, View view) { 237 if (parent != null && parent != view) { 238 parent.addView(view); 239 } 240 } 241 242 private View constructView(Context context) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { 243 Class<? extends View> clazz = pickViewClass(); 244 try { 245 TestAttributeSet attributeSet = new TestAttributeSet(attributes, resourceExtractor, attrResourceLoader, clazz, isSystem); 246 if (strictI18n) { 247 attributeSet.validateStrictI18n(); 248 } 249 return ((Constructor<? extends View>) clazz.getConstructor(Context.class, AttributeSet.class)).newInstance(context, attributeSet); 250 } catch (NoSuchMethodException e) { 251 try { 252 return ((Constructor<? extends View>) clazz.getConstructor(Context.class)).newInstance(context); 253 } catch (NoSuchMethodException e1) { 254 return ((Constructor<? extends View>) clazz.getConstructor(Context.class, String.class)).newInstance(context, ""); 255 } 256 } 257 } 258 259 private Class<? extends View> pickViewClass() { 260 Class<? extends View> clazz = loadViewClass(name); 261 if (clazz == null) { 262 clazz = loadViewClass("android.view." + name); 263 } 264 if (clazz == null) { 265 clazz = loadViewClass("android.widget." + name); 266 } 267 if (clazz == null) { 268 clazz = loadViewClass("android.webkit." + name); 269 } 270 if (clazz == null) { 271 clazz = loadViewClass("com.google.android.maps." + name); 272 } 273 274 if (clazz == null) { 275 throw new RuntimeException("couldn't find view class " + name); 276 } 277 return clazz; 278 } 279 280 private Class loadClass(String className) { 281 try { 282 return getClass().getClassLoader().loadClass(className); 283 } catch (ClassNotFoundException e) { 284 return null; 285 } 286 } 287 288 private Class<? extends View> loadViewClass(String className) { 289 // noinspection unchecked 290 return loadClass(className); 291 } 292 293 private Class<? extends Fragment> loadFragmentClass(String className) { 294 // noinspection unchecked 295 return loadClass(className); 296 } 297 298 public void applyFocusOverride(ViewParent parent) { 299 if (requestFocusOverride) { 300 View ancestor = (View) parent; 301 while (ancestor.getParent() != null) { 302 ancestor = (View) ancestor.getParent(); 303 } 304 ancestor.clearFocus(); 305 } 306 } 307 } 308 } 309