Home | History | Annotate | Download | only in preference
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.preference;
     18 
     19 import java.io.IOException;
     20 import java.lang.reflect.Constructor;
     21 import java.util.HashMap;
     22 
     23 import org.xmlpull.v1.XmlPullParser;
     24 import org.xmlpull.v1.XmlPullParserException;
     25 
     26 import android.content.Context;
     27 import android.content.res.XmlResourceParser;
     28 import android.util.AttributeSet;
     29 import android.util.Xml;
     30 import android.view.ContextThemeWrapper;
     31 import android.view.InflateException;
     32 import android.view.LayoutInflater;
     33 
     34 // TODO: fix generics
     35 /**
     36  * Generic XML inflater. This has been adapted from {@link LayoutInflater} and
     37  * quickly passed over to use generics.
     38  *
     39  * @hide
     40  * @param T The type of the items to inflate
     41  * @param P The type of parents (that is those items that contain other items).
     42  *            Must implement {@link GenericInflater.Parent}
     43  */
     44 abstract class GenericInflater<T, P extends GenericInflater.Parent> {
     45     private final boolean DEBUG = false;
     46 
     47     protected final Context mContext;
     48 
     49     // these are optional, set by the caller
     50     private boolean mFactorySet;
     51     private Factory<T> mFactory;
     52 
     53     private final Object[] mConstructorArgs = new Object[2];
     54 
     55     private static final Class[] mConstructorSignature = new Class[] {
     56             Context.class, AttributeSet.class};
     57 
     58     private static final HashMap sConstructorMap = new HashMap();
     59 
     60     private String mDefaultPackage;
     61 
     62     public interface Parent<T> {
     63         public void addItemFromInflater(T child);
     64     }
     65 
     66     public interface Factory<T> {
     67         /**
     68          * Hook you can supply that is called when inflating from a
     69          * inflater. You can use this to customize the tag
     70          * names available in your XML files.
     71          * <p>
     72          * Note that it is good practice to prefix these custom names with your
     73          * package (i.e., com.coolcompany.apps) to avoid conflicts with system
     74          * names.
     75          *
     76          * @param name Tag name to be inflated.
     77          * @param context The context the item is being created in.
     78          * @param attrs Inflation attributes as specified in XML file.
     79          * @return Newly created item. Return null for the default behavior.
     80          */
     81         public T onCreateItem(String name, Context context, AttributeSet attrs);
     82     }
     83 
     84     private static class FactoryMerger<T> implements Factory<T> {
     85         private final Factory<T> mF1, mF2;
     86 
     87         FactoryMerger(Factory<T> f1, Factory<T> f2) {
     88             mF1 = f1;
     89             mF2 = f2;
     90         }
     91 
     92         public T onCreateItem(String name, Context context, AttributeSet attrs) {
     93             T v = mF1.onCreateItem(name, context, attrs);
     94             if (v != null) return v;
     95             return mF2.onCreateItem(name, context, attrs);
     96         }
     97     }
     98 
     99     /**
    100      * Create a new inflater instance associated with a
    101      * particular Context.
    102      *
    103      * @param context The Context in which this inflater will
    104      *            create its items; most importantly, this supplies the theme
    105      *            from which the default values for their attributes are
    106      *            retrieved.
    107      */
    108     protected GenericInflater(Context context) {
    109         mContext = context;
    110     }
    111 
    112     /**
    113      * Create a new inflater instance that is a copy of an
    114      * existing inflater, optionally with its Context
    115      * changed. For use in implementing {@link #cloneInContext}.
    116      *
    117      * @param original The original inflater to copy.
    118      * @param newContext The new Context to use.
    119      */
    120     protected GenericInflater(GenericInflater<T,P> original, Context newContext) {
    121         mContext = newContext;
    122         mFactory = original.mFactory;
    123     }
    124 
    125     /**
    126      * Create a copy of the existing inflater object, with the copy
    127      * pointing to a different Context than the original.  This is used by
    128      * {@link ContextThemeWrapper} to create a new inflater to go along
    129      * with the new Context theme.
    130      *
    131      * @param newContext The new Context to associate with the new inflater.
    132      * May be the same as the original Context if desired.
    133      *
    134      * @return Returns a brand spanking new inflater object associated with
    135      * the given Context.
    136      */
    137     public abstract GenericInflater cloneInContext(Context newContext);
    138 
    139     /**
    140      * Sets the default package that will be searched for classes to construct
    141      * for tag names that have no explicit package.
    142      *
    143      * @param defaultPackage The default package. This will be prepended to the
    144      *            tag name, so it should end with a period.
    145      */
    146     public void setDefaultPackage(String defaultPackage) {
    147         mDefaultPackage = defaultPackage;
    148     }
    149 
    150     /**
    151      * Returns the default package, or null if it is not set.
    152      *
    153      * @see #setDefaultPackage(String)
    154      * @return The default package.
    155      */
    156     public String getDefaultPackage() {
    157         return mDefaultPackage;
    158     }
    159 
    160     /**
    161      * Return the context we are running in, for access to resources, class
    162      * loader, etc.
    163      */
    164     public Context getContext() {
    165         return mContext;
    166     }
    167 
    168     /**
    169      * Return the current factory (or null). This is called on each element
    170      * name. If the factory returns an item, add that to the hierarchy. If it
    171      * returns null, proceed to call onCreateItem(name).
    172      */
    173     public final Factory<T> getFactory() {
    174         return mFactory;
    175     }
    176 
    177     /**
    178      * Attach a custom Factory interface for creating items while using this
    179      * inflater. This must not be null, and can only be set
    180      * once; after setting, you can not change the factory. This is called on
    181      * each element name as the XML is parsed. If the factory returns an item,
    182      * that is added to the hierarchy. If it returns null, the next factory
    183      * default {@link #onCreateItem} method is called.
    184      * <p>
    185      * If you have an existing inflater and want to add your
    186      * own factory to it, use {@link #cloneInContext} to clone the existing
    187      * instance and then you can use this function (once) on the returned new
    188      * instance. This will merge your own factory with whatever factory the
    189      * original instance is using.
    190      */
    191     public void setFactory(Factory<T> factory) {
    192         if (mFactorySet) {
    193             throw new IllegalStateException("" +
    194             		"A factory has already been set on this inflater");
    195         }
    196         if (factory == null) {
    197             throw new NullPointerException("Given factory can not be null");
    198         }
    199         mFactorySet = true;
    200         if (mFactory == null) {
    201             mFactory = factory;
    202         } else {
    203             mFactory = new FactoryMerger<T>(factory, mFactory);
    204         }
    205     }
    206 
    207 
    208     /**
    209      * Inflate a new item hierarchy from the specified xml resource. Throws
    210      * InflaterException if there is an error.
    211      *
    212      * @param resource ID for an XML resource to load (e.g.,
    213      *        <code>R.layout.main_page</code>)
    214      * @param root Optional parent of the generated hierarchy.
    215      * @return The root of the inflated hierarchy. If root was supplied,
    216      *         this is the root item; otherwise it is the root of the inflated
    217      *         XML file.
    218      */
    219     public T inflate(int resource, P root) {
    220         return inflate(resource, root, root != null);
    221     }
    222 
    223     /**
    224      * Inflate a new hierarchy from the specified xml node. Throws
    225      * InflaterException if there is an error. *
    226      * <p>
    227      * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
    228      * reasons, inflation relies heavily on pre-processing of XML files
    229      * that is done at build time. Therefore, it is not currently possible to
    230      * use inflater with an XmlPullParser over a plain XML file at runtime.
    231      *
    232      * @param parser XML dom node containing the description of the
    233      *        hierarchy.
    234      * @param root Optional parent of the generated hierarchy.
    235      * @return The root of the inflated hierarchy. If root was supplied,
    236      *         this is the that; otherwise it is the root of the inflated
    237      *         XML file.
    238      */
    239     public T inflate(XmlPullParser parser, P root) {
    240         return inflate(parser, root, root != null);
    241     }
    242 
    243     /**
    244      * Inflate a new hierarchy from the specified xml resource. Throws
    245      * InflaterException if there is an error.
    246      *
    247      * @param resource ID for an XML resource to load (e.g.,
    248      *        <code>R.layout.main_page</code>)
    249      * @param root Optional root to be the parent of the generated hierarchy (if
    250      *        <em>attachToRoot</em> is true), or else simply an object that
    251      *        provides a set of values for root of the returned
    252      *        hierarchy (if <em>attachToRoot</em> is false.)
    253      * @param attachToRoot Whether the inflated hierarchy should be attached to
    254      *        the root parameter?
    255      * @return The root of the inflated hierarchy. If root was supplied and
    256      *         attachToRoot is true, this is root; otherwise it is the root of
    257      *         the inflated XML file.
    258      */
    259     public T inflate(int resource, P root, boolean attachToRoot) {
    260         if (DEBUG) System.out.println("INFLATING from resource: " + resource);
    261         XmlResourceParser parser = getContext().getResources().getXml(resource);
    262         try {
    263             return inflate(parser, root, attachToRoot);
    264         } finally {
    265             parser.close();
    266         }
    267     }
    268 
    269     /**
    270      * Inflate a new hierarchy from the specified XML node. Throws
    271      * InflaterException if there is an error.
    272      * <p>
    273      * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
    274      * reasons, inflation relies heavily on pre-processing of XML files
    275      * that is done at build time. Therefore, it is not currently possible to
    276      * use inflater with an XmlPullParser over a plain XML file at runtime.
    277      *
    278      * @param parser XML dom node containing the description of the
    279      *        hierarchy.
    280      * @param root Optional to be the parent of the generated hierarchy (if
    281      *        <em>attachToRoot</em> is true), or else simply an object that
    282      *        provides a set of values for root of the returned
    283      *        hierarchy (if <em>attachToRoot</em> is false.)
    284      * @param attachToRoot Whether the inflated hierarchy should be attached to
    285      *        the root parameter?
    286      * @return The root of the inflated hierarchy. If root was supplied and
    287      *         attachToRoot is true, this is root; otherwise it is the root of
    288      *         the inflated XML file.
    289      */
    290     public T inflate(XmlPullParser parser, P root,
    291             boolean attachToRoot) {
    292         synchronized (mConstructorArgs) {
    293             final AttributeSet attrs = Xml.asAttributeSet(parser);
    294             mConstructorArgs[0] = mContext;
    295             T result = (T) root;
    296 
    297             try {
    298                 // Look for the root node.
    299                 int type;
    300                 while ((type = parser.next()) != parser.START_TAG
    301                         && type != parser.END_DOCUMENT) {
    302                     ;
    303                 }
    304 
    305                 if (type != parser.START_TAG) {
    306                     throw new InflateException(parser.getPositionDescription()
    307                             + ": No start tag found!");
    308                 }
    309 
    310                 if (DEBUG) {
    311                     System.out.println("**************************");
    312                     System.out.println("Creating root: "
    313                             + parser.getName());
    314                     System.out.println("**************************");
    315                 }
    316                 // Temp is the root that was found in the xml
    317                 T xmlRoot = createItemFromTag(parser, parser.getName(),
    318                         attrs);
    319 
    320                 result = (T) onMergeRoots(root, attachToRoot, (P) xmlRoot);
    321 
    322                 if (DEBUG) {
    323                     System.out.println("-----> start inflating children");
    324                 }
    325                 // Inflate all children under temp
    326                 rInflate(parser, result, attrs);
    327                 if (DEBUG) {
    328                     System.out.println("-----> done inflating children");
    329                 }
    330 
    331             } catch (InflateException e) {
    332                 throw e;
    333 
    334             } catch (XmlPullParserException e) {
    335                 InflateException ex = new InflateException(e.getMessage());
    336                 ex.initCause(e);
    337                 throw ex;
    338             } catch (IOException e) {
    339                 InflateException ex = new InflateException(
    340                         parser.getPositionDescription()
    341                         + ": " + e.getMessage());
    342                 ex.initCause(e);
    343                 throw ex;
    344             }
    345 
    346             return result;
    347         }
    348     }
    349 
    350     /**
    351      * Low-level function for instantiating by name. This attempts to
    352      * instantiate class of the given <var>name</var> found in this
    353      * inflater's ClassLoader.
    354      *
    355      * <p>
    356      * There are two things that can happen in an error case: either the
    357      * exception describing the error will be thrown, or a null will be
    358      * returned. You must deal with both possibilities -- the former will happen
    359      * the first time createItem() is called for a class of a particular name,
    360      * the latter every time there-after for that class name.
    361      *
    362      * @param name The full name of the class to be instantiated.
    363      * @param attrs The XML attributes supplied for this instance.
    364      *
    365      * @return The newly instantied item, or null.
    366      */
    367     public final T createItem(String name, String prefix, AttributeSet attrs)
    368             throws ClassNotFoundException, InflateException {
    369         Constructor constructor = (Constructor) sConstructorMap.get(name);
    370 
    371         try {
    372             if (null == constructor) {
    373                 // Class not found in the cache, see if it's real,
    374                 // and try to add it
    375                 Class clazz = mContext.getClassLoader().loadClass(
    376                         prefix != null ? (prefix + name) : name);
    377                 constructor = clazz.getConstructor(mConstructorSignature);
    378                 sConstructorMap.put(name, constructor);
    379             }
    380 
    381             Object[] args = mConstructorArgs;
    382             args[1] = attrs;
    383             return (T) constructor.newInstance(args);
    384 
    385         } catch (NoSuchMethodException e) {
    386             InflateException ie = new InflateException(attrs
    387                     .getPositionDescription()
    388                     + ": Error inflating class "
    389                     + (prefix != null ? (prefix + name) : name));
    390             ie.initCause(e);
    391             throw ie;
    392 
    393         } catch (ClassNotFoundException e) {
    394             // If loadClass fails, we should propagate the exception.
    395             throw e;
    396         } catch (Exception e) {
    397             InflateException ie = new InflateException(attrs
    398                     .getPositionDescription()
    399                     + ": Error inflating class "
    400                     + constructor.getClass().getName());
    401             ie.initCause(e);
    402             throw ie;
    403         }
    404     }
    405 
    406     /**
    407      * This routine is responsible for creating the correct subclass of item
    408      * given the xml element name. Override it to handle custom item objects. If
    409      * you override this in your subclass be sure to call through to
    410      * super.onCreateItem(name) for names you do not recognize.
    411      *
    412      * @param name The fully qualified class name of the item to be create.
    413      * @param attrs An AttributeSet of attributes to apply to the item.
    414      * @return The item created.
    415      */
    416     protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException {
    417         return createItem(name, mDefaultPackage, attrs);
    418     }
    419 
    420     private final T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) {
    421         if (DEBUG) System.out.println("******** Creating item: " + name);
    422 
    423         try {
    424             T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs);
    425 
    426             if (item == null) {
    427                 if (-1 == name.indexOf('.')) {
    428                     item = onCreateItem(name, attrs);
    429                 } else {
    430                     item = createItem(name, null, attrs);
    431                 }
    432             }
    433 
    434             if (DEBUG) System.out.println("Created item is: " + item);
    435             return item;
    436 
    437         } catch (InflateException e) {
    438             throw e;
    439 
    440         } catch (ClassNotFoundException e) {
    441             InflateException ie = new InflateException(attrs
    442                     .getPositionDescription()
    443                     + ": Error inflating class " + name);
    444             ie.initCause(e);
    445             throw ie;
    446 
    447         } catch (Exception e) {
    448             InflateException ie = new InflateException(attrs
    449                     .getPositionDescription()
    450                     + ": Error inflating class " + name);
    451             ie.initCause(e);
    452             throw ie;
    453         }
    454     }
    455 
    456     /**
    457      * Recursive method used to descend down the xml hierarchy and instantiate
    458      * items, instantiate their children, and then call onFinishInflate().
    459      */
    460     private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)
    461             throws XmlPullParserException, IOException {
    462         final int depth = parser.getDepth();
    463 
    464         int type;
    465         while (((type = parser.next()) != parser.END_TAG ||
    466                 parser.getDepth() > depth) && type != parser.END_DOCUMENT) {
    467 
    468             if (type != parser.START_TAG) {
    469                 continue;
    470             }
    471 
    472             if (onCreateCustomFromTag(parser, parent, attrs)) {
    473                 continue;
    474             }
    475 
    476             if (DEBUG) {
    477                 System.out.println("Now inflating tag: " + parser.getName());
    478             }
    479             String name = parser.getName();
    480 
    481             T item = createItemFromTag(parser, name, attrs);
    482 
    483             if (DEBUG) {
    484                 System.out
    485                         .println("Creating params from parent: " + parent);
    486             }
    487 
    488             ((P) parent).addItemFromInflater(item);
    489 
    490             if (DEBUG) {
    491                 System.out.println("-----> start inflating children");
    492             }
    493             rInflate(parser, item, attrs);
    494             if (DEBUG) {
    495                 System.out.println("-----> done inflating children");
    496             }
    497         }
    498 
    499     }
    500 
    501     /**
    502      * Before this inflater tries to create an item from the tag, this method
    503      * will be called. The parser will be pointing to the start of a tag, you
    504      * must stop parsing and return when you reach the end of this element!
    505      *
    506      * @param parser XML dom node containing the description of the hierarchy.
    507      * @param parent The item that should be the parent of whatever you create.
    508      * @param attrs An AttributeSet of attributes to apply to the item.
    509      * @return Whether you created a custom object (true), or whether this
    510      *         inflater should proceed to create an item.
    511      */
    512     protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent,
    513             final AttributeSet attrs) throws XmlPullParserException {
    514         return false;
    515     }
    516 
    517     protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) {
    518         return xmlRoot;
    519     }
    520 }
    521