1package com.xtremelabs.robolectric.res;
2
3import android.content.Context;
4import android.os.Build;
5import android.support.v4.app.Fragment;
6import android.support.v4.app.FragmentActivity;
7import android.text.TextUtils;
8import android.util.AttributeSet;
9import android.view.View;
10import android.view.ViewGroup;
11import android.view.ViewParent;
12import android.widget.FrameLayout;
13import com.xtremelabs.robolectric.tester.android.util.TestAttributeSet;
14import com.xtremelabs.robolectric.util.I18nException;
15import org.w3c.dom.Document;
16import org.w3c.dom.NamedNodeMap;
17import org.w3c.dom.Node;
18import org.w3c.dom.NodeList;
19
20import java.io.File;
21import java.lang.reflect.Constructor;
22import java.lang.reflect.InvocationTargetException;
23import java.lang.reflect.Method;
24import java.util.*;
25
26import static com.xtremelabs.robolectric.Robolectric.shadowOf;
27
28public 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