package com.xtremelabs.robolectric.res; import android.content.Context; import android.os.Build; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; import com.xtremelabs.robolectric.tester.android.util.TestAttributeSet; import com.xtremelabs.robolectric.util.I18nException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import static com.xtremelabs.robolectric.Robolectric.shadowOf; public class ViewLoader extends XmlLoader { protected Map viewNodesByLayoutName = new HashMap(); private AttrResourceLoader attrResourceLoader; private List qualifierSearchPath; public ViewLoader(ResourceExtractor resourceExtractor, AttrResourceLoader attrResourceLoader) { super(resourceExtractor); this.attrResourceLoader = attrResourceLoader; setLayoutQualifierSearchPath(); } @Override protected void processResourceXml(File xmlFile, Document document, boolean isSystem) throws Exception { ViewNode topLevelNode = new ViewNode("top-level", new HashMap(), isSystem); processChildren(document.getChildNodes(), topLevelNode); String layoutName = xmlFile.getParentFile().getName() + "/" + xmlFile.getName().replace(".xml", ""); if (isSystem) { layoutName = "android:" + layoutName; } viewNodesByLayoutName.put(layoutName, topLevelNode.getChildren().get(0)); } private void processChildren(NodeList childNodes, ViewNode parent) { for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); processNode(node, parent); } } private void processNode(Node node, ViewNode parent) { String name = node.getNodeName(); NamedNodeMap attributes = node.getAttributes(); Map attrMap = new HashMap(); if (attributes != null) { int length = attributes.getLength(); for (int i = 0; i < length; i++) { Node attr = attributes.item(i); attrMap.put(attr.getNodeName(), attr.getNodeValue()); } } if (name.equals("requestFocus")) { parent.attributes.put("android:focus", "true"); parent.requestFocusOverride = true; } else if (!name.startsWith("#")) { ViewNode viewNode = new ViewNode(name, attrMap, parent.isSystem); if (parent != null) parent.addChild(viewNode); processChildren(node.getChildNodes(), viewNode); } } public View inflateView(Context context, String key) { return inflateView(context, key, null); } public View inflateView(Context context, String key, View parent) { return inflateView(context, key, null, parent); } public View inflateView(Context context, int resourceId, View parent) { return inflateView(context, resourceExtractor.getResourceName(resourceId), parent); } private View inflateView(Context context, String layoutName, Map attributes, View parent) { ViewNode viewNode = getViewNodeByLayoutName(layoutName); if (viewNode == null) { throw new RuntimeException("Could not find layout " + layoutName); } try { if (attributes != null) { for (Map.Entry entry : attributes.entrySet()) { if (!entry.getKey().equals("layout")) { viewNode.attributes.put(entry.getKey(), entry.getValue()); } } } return viewNode.inflate(context, parent); } catch (I18nException e) { throw e; } catch (Exception e) { throw new RuntimeException("error inflating " + layoutName, e); } } private ViewNode getViewNodeByLayoutName(String layoutName) { if (layoutName.startsWith("layout/")) { String rawLayoutName = layoutName.substring("layout/".length()); for (String qualifier : qualifierSearchPath) { for (int version = Math.max(Build.VERSION.SDK_INT, 0); version >= 0; version--) { ViewNode foundNode = findLayoutViewNode(qualifier, version, rawLayoutName); if (foundNode != null) { return foundNode; } } } } return viewNodesByLayoutName.get(layoutName); } private ViewNode findLayoutViewNode(String qualifier, int version, String rawLayoutName) { StringBuilder name = new StringBuilder("layout"); if (!TextUtils.isEmpty(qualifier)) { name.append("-").append(qualifier); } if (version > 0) { name.append("-v").append(version); } name.append("/").append(rawLayoutName); return viewNodesByLayoutName.get(name.toString()); } public void setLayoutQualifierSearchPath(String... locations) { qualifierSearchPath = Arrays.asList(locations); if (!qualifierSearchPath.contains("")) { qualifierSearchPath = new ArrayList(qualifierSearchPath); qualifierSearchPath.add(""); } } public class ViewNode { private String name; private final Map attributes; private List children = new ArrayList(); boolean requestFocusOverride = false; boolean isSystem = false; public ViewNode(String name, Map attributes, boolean isSystem) { this.name = name; this.attributes = attributes; this.isSystem = isSystem; } public List getChildren() { return children; } public void addChild(ViewNode viewNode) { children.add(viewNode); } public View inflate(Context context, View parent) throws Exception { View view = create(context, (ViewGroup) parent); for (ViewNode child : children) { child.inflate(context, view); } invokeOnFinishInflate(view); return view; } private void invokeOnFinishInflate(View view) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { Method onFinishInflate = View.class.getDeclaredMethod("onFinishInflate"); onFinishInflate.setAccessible(true); onFinishInflate.invoke(view); } private View create(Context context, ViewGroup parent) throws Exception { if (name.equals("include")) { String layout = attributes.get("layout"); View view = inflateView(context, layout.substring(1), attributes, parent); return view; } else if (name.equals("merge")) { return parent; } else if (name.equals("fragment")) { View fragment = constructFragment(context); addToParent(parent, fragment); return fragment; } else { applyFocusOverride(parent); View view = constructView(context); addToParent(parent, view); shadowOf(view).applyFocus(); return view; } } private FrameLayout constructFragment(Context context) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { TestAttributeSet attributeSet = new TestAttributeSet(attributes, resourceExtractor, attrResourceLoader, View.class, isSystem); if (strictI18n) { attributeSet.validateStrictI18n(); } Class clazz = loadFragmentClass(attributes.get("android:name")); Fragment fragment = ((Constructor) clazz.getConstructor()).newInstance(); if (!(context instanceof FragmentActivity)) { throw new RuntimeException("Cannot inflate a fragment unless the activity is a FragmentActivity"); } FragmentActivity activity = (FragmentActivity) context; String tag = attributeSet.getAttributeValue("android", "tag"); int id = attributeSet.getAttributeResourceValue("android", "id", 0); // TODO: this should probably be changed to call TestFragmentManager.addFragment so that the // inflated fragments don't get started twice (once in the commit, and once in ShadowFragmentActivity's // onStart() activity.getSupportFragmentManager().beginTransaction().add(id, fragment, tag).commit(); View view = fragment.getView(); FrameLayout container = new FrameLayout(context); container.setId(id); container.addView(view); return container; } private void addToParent(ViewGroup parent, View view) { if (parent != null && parent != view) { parent.addView(view); } } private View constructView(Context context) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { Class clazz = pickViewClass(); try { TestAttributeSet attributeSet = new TestAttributeSet(attributes, resourceExtractor, attrResourceLoader, clazz, isSystem); if (strictI18n) { attributeSet.validateStrictI18n(); } return ((Constructor) clazz.getConstructor(Context.class, AttributeSet.class)).newInstance(context, attributeSet); } catch (NoSuchMethodException e) { try { return ((Constructor) clazz.getConstructor(Context.class)).newInstance(context); } catch (NoSuchMethodException e1) { return ((Constructor) clazz.getConstructor(Context.class, String.class)).newInstance(context, ""); } } } private Class pickViewClass() { Class clazz = loadViewClass(name); if (clazz == null) { clazz = loadViewClass("android.view." + name); } if (clazz == null) { clazz = loadViewClass("android.widget." + name); } if (clazz == null) { clazz = loadViewClass("android.webkit." + name); } if (clazz == null) { clazz = loadViewClass("com.google.android.maps." + name); } if (clazz == null) { throw new RuntimeException("couldn't find view class " + name); } return clazz; } private Class loadClass(String className) { try { return getClass().getClassLoader().loadClass(className); } catch (ClassNotFoundException e) { return null; } } private Class loadViewClass(String className) { // noinspection unchecked return loadClass(className); } private Class loadFragmentClass(String className) { // noinspection unchecked return loadClass(className); } public void applyFocusOverride(ViewParent parent) { if (requestFocusOverride) { View ancestor = (View) parent; while (ancestor.getParent() != null) { ancestor = (View) ancestor.getParent(); } ancestor.clearFocus(); } } } }