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