Home | History | Annotate | Download | only in res
      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