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 
ViewLoader(ResourceExtractor resourceExtractor, AttrResourceLoader attrResourceLoader)33     public ViewLoader(ResourceExtractor resourceExtractor, AttrResourceLoader attrResourceLoader) {
34         super(resourceExtractor);
35         this.attrResourceLoader = attrResourceLoader;
36         setLayoutQualifierSearchPath();
37     }
38 
39     @Override
processResourceXml(File xmlFile, Document document, boolean isSystem)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 
processChildren(NodeList childNodes, ViewNode parent)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 
processNode(Node node, ViewNode parent)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 
inflateView(Context context, String key)80     public View inflateView(Context context, String key) {
81         return inflateView(context, key, null);
82     }
83 
inflateView(Context context, String key, View parent)84     public View inflateView(Context context, String key, View parent) {
85         return inflateView(context, key, null, parent);
86     }
87 
inflateView(Context context, int resourceId, View parent)88     public View inflateView(Context context, int resourceId, View parent) {
89         return inflateView(context, resourceExtractor.getResourceName(resourceId), parent);
90     }
91 
inflateView(Context context, String layoutName, Map<String, String> attributes, View parent)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 
getViewNodeByLayoutName(String layoutName)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 
findLayoutViewNode(String qualifier, int version, String rawLayoutName)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 
setLayoutQualifierSearchPath(String... locations)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 
ViewNode(String name, Map<String, String> attributes, boolean isSystem)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 
getChildren()162         public List<ViewNode> getChildren() {
163             return children;
164         }
165 
addChild(ViewNode viewNode)166         public void addChild(ViewNode viewNode) {
167             children.add(viewNode);
168         }
169 
inflate(Context context, View parent)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 
invokeOnFinishInflate(View view)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 
create(Context context, ViewGroup parent)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 
constructFragment(Context context)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 
addToParent(ViewGroup parent, View view)236         private void addToParent(ViewGroup parent, View view) {
237             if (parent != null && parent != view) {
238                 parent.addView(view);
239             }
240         }
241 
constructView(Context context)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 
pickViewClass()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 
loadClass(String className)280         private Class loadClass(String className) {
281             try {
282                 return getClass().getClassLoader().loadClass(className);
283             } catch (ClassNotFoundException e) {
284                 return null;
285             }
286         }
287 
loadViewClass(String className)288         private Class<? extends View> loadViewClass(String className) {
289             // noinspection unchecked
290             return loadClass(className);
291         }
292 
loadFragmentClass(String className)293         private Class<? extends Fragment> loadFragmentClass(String className) {
294             // noinspection unchecked
295             return loadClass(className);
296         }
297 
applyFocusOverride(ViewParent parent)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