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