Home | History | Annotate | Download | only in preference
      1 /*
      2  * Copyright 2018 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 androidx.preference;
     18 
     19 import android.content.Context;
     20 import android.content.Intent;
     21 import android.content.res.XmlResourceParser;
     22 import android.util.AttributeSet;
     23 import android.util.Xml;
     24 import android.view.InflateException;
     25 
     26 import androidx.annotation.NonNull;
     27 import androidx.annotation.Nullable;
     28 
     29 import org.xmlpull.v1.XmlPullParser;
     30 import org.xmlpull.v1.XmlPullParserException;
     31 
     32 import java.io.IOException;
     33 import java.lang.reflect.Constructor;
     34 import java.util.HashMap;
     35 
     36 /**
     37  * The {@link PreferenceInflater} is used to inflate preference hierarchies from
     38  * XML files.
     39  */
     40 class PreferenceInflater {
     41     private static final String TAG = "PreferenceInflater";
     42 
     43     private static final Class<?>[] CONSTRUCTOR_SIGNATURE = new Class[] {
     44             Context.class, AttributeSet.class};
     45 
     46     private static final HashMap<String, Constructor> CONSTRUCTOR_MAP = new HashMap<>();
     47 
     48     private final Context mContext;
     49 
     50     private final Object[] mConstructorArgs = new Object[2];
     51 
     52     private PreferenceManager mPreferenceManager;
     53 
     54     private String[] mDefaultPackages;
     55 
     56     private static final String INTENT_TAG_NAME = "intent";
     57     private static final String EXTRA_TAG_NAME = "extra";
     58 
     59     public PreferenceInflater(Context context, PreferenceManager preferenceManager) {
     60         mContext = context;
     61         init(preferenceManager);
     62     }
     63 
     64     private void init(PreferenceManager preferenceManager) {
     65         mPreferenceManager = preferenceManager;
     66 
     67         // Handle legacy case for de-Jetification. These preference classes were originally
     68         // in separate packages, so we need two defaults when de-Jetified.
     69         setDefaultPackages(new String[] {
     70                 // Preference was originally in android.support.v7.preference.
     71                 Preference.class.getPackage().getName() + ".",
     72                 // SwitchPreference was originally in android.support.v14.preference.
     73                 SwitchPreference.class.getPackage().getName() + "."
     74         });
     75     }
     76 
     77     /**
     78      * Sets the default package that will be searched for classes to construct
     79      * for tag names that have no explicit package.
     80      *
     81      * @param defaultPackage The default package. This will be prepended to the
     82      *            tag name, so it should end with a period.
     83      */
     84     public void setDefaultPackages(String[] defaultPackage) {
     85         mDefaultPackages = defaultPackage;
     86     }
     87 
     88     /**
     89      * Returns the default package, or null if it is not set.
     90      *
     91      * @see #setDefaultPackages(String[])
     92      * @return The default package.
     93      */
     94     public String[] getDefaultPackages() {
     95         return mDefaultPackages;
     96     }
     97 
     98     /**
     99      * Return the context we are running in, for access to resources, class
    100      * loader, etc.
    101      */
    102     public Context getContext() {
    103         return mContext;
    104     }
    105 
    106     /**
    107      * Inflate a new item hierarchy from the specified xml resource. Throws
    108      * InflaterException if there is an error.
    109      *
    110      * @param resource ID for an XML resource to load (e.g.,
    111      *        <code>R.layout.main_page</code>)
    112      * @param root Optional parent of the generated hierarchy.
    113      * @return The root of the inflated hierarchy. If root was supplied,
    114      *         this is the root item; otherwise it is the root of the inflated
    115      *         XML file.
    116      */
    117     public Preference inflate(int resource, @Nullable PreferenceGroup root) {
    118         XmlResourceParser parser = getContext().getResources().getXml(resource);
    119         try {
    120             return inflate(parser, root);
    121         } finally {
    122             parser.close();
    123         }
    124     }
    125 
    126     /**
    127      * Inflate a new hierarchy from the specified XML node. Throws
    128      * InflaterException if there is an error.
    129      * <p>
    130      * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
    131      * reasons, inflation relies heavily on pre-processing of XML files
    132      * that is done at build time. Therefore, it is not currently possible to
    133      * use inflater with an XmlPullParser over a plain XML file at runtime.
    134      *
    135      * @param parser XML dom node containing the description of the
    136      *        hierarchy.
    137      * @param root Optional to be the parent of the generated hierarchy (if
    138      *        <em>attachToRoot</em> is true), or else simply an object that
    139      *        provides a set of values for root of the returned
    140      *        hierarchy (if <em>attachToRoot</em> is false.)
    141      * @return The root of the inflated hierarchy. If root was supplied,
    142      *         this is root; otherwise it is the root of
    143      *         the inflated XML file.
    144      */
    145     public Preference inflate(XmlPullParser parser, @Nullable PreferenceGroup root) {
    146         synchronized (mConstructorArgs) {
    147             final AttributeSet attrs = Xml.asAttributeSet(parser);
    148             mConstructorArgs[0] = mContext;
    149             final Preference result;
    150 
    151             try {
    152                 // Look for the root node.
    153                 int type;
    154                 do {
    155                     type = parser.next();
    156                 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
    157 
    158                 if (type != XmlPullParser.START_TAG) {
    159                     throw new InflateException(parser.getPositionDescription()
    160                             + ": No start tag found!");
    161                 }
    162 
    163                 // Temp is the root that was found in the xml
    164                 Preference xmlRoot = createItemFromTag(parser.getName(),
    165                         attrs);
    166 
    167                 result = onMergeRoots(root, (PreferenceGroup) xmlRoot);
    168 
    169                 // Inflate all children under temp
    170                 rInflate(parser, result, attrs);
    171 
    172             } catch (InflateException e) {
    173                 throw e;
    174             } catch (XmlPullParserException e) {
    175                 final InflateException ex = new InflateException(e.getMessage());
    176                 ex.initCause(e);
    177                 throw ex;
    178             } catch (IOException e) {
    179                 final InflateException ex = new InflateException(
    180                         parser.getPositionDescription()
    181                                 + ": " + e.getMessage());
    182                 ex.initCause(e);
    183                 throw ex;
    184             }
    185 
    186             return result;
    187         }
    188     }
    189 
    190     private @NonNull PreferenceGroup onMergeRoots(PreferenceGroup givenRoot,
    191             @NonNull PreferenceGroup xmlRoot) {
    192         // If we were given a Preferences, use it as the root (ignoring the root
    193         // Preferences from the XML file).
    194         if (givenRoot == null) {
    195             xmlRoot.onAttachedToHierarchy(mPreferenceManager);
    196             return xmlRoot;
    197         } else {
    198             return givenRoot;
    199         }
    200     }
    201 
    202     /**
    203      * Low-level function for instantiating by name. This attempts to
    204      * instantiate class of the given <var>name</var> found in this
    205      * inflater's ClassLoader.
    206      *
    207      * <p>
    208      * There are two things that can happen in an error case: either the
    209      * exception describing the error will be thrown, or a null will be
    210      * returned. You must deal with both possibilities -- the former will happen
    211      * the first time createItem() is called for a class of a particular name,
    212      * the latter every time there-after for that class name.
    213      *
    214      * @param name The full name of the class to be instantiated.
    215      * @param attrs The XML attributes supplied for this instance.
    216      *
    217      * @return The newly instantiated item, or null.
    218      */
    219     private Preference createItem(@NonNull String name, @Nullable String[] prefixes,
    220             AttributeSet attrs)
    221             throws ClassNotFoundException, InflateException {
    222         Constructor constructor = CONSTRUCTOR_MAP.get(name);
    223 
    224         try {
    225             if (constructor == null) {
    226                 // Class not found in the cache, see if it's real,
    227                 // and try to add it
    228                 final ClassLoader classLoader = mContext.getClassLoader();
    229                 Class<?> clazz = null;
    230                 if (prefixes == null || prefixes.length == 0) {
    231                     clazz = classLoader.loadClass(name);
    232                 } else {
    233                     ClassNotFoundException notFoundException = null;
    234                     for (final String prefix : prefixes) {
    235                         try {
    236                             clazz = classLoader.loadClass(prefix + name);
    237                             break;
    238                         } catch (final ClassNotFoundException e) {
    239                             notFoundException = e;
    240                         }
    241                     }
    242                     if (clazz == null) {
    243                         if (notFoundException == null) {
    244                             throw new InflateException(attrs
    245                                     .getPositionDescription()
    246                                     + ": Error inflating class " + name);
    247                         } else {
    248                             throw notFoundException;
    249                         }
    250                     }
    251                 }
    252                 constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE);
    253                 constructor.setAccessible(true);
    254                 CONSTRUCTOR_MAP.put(name, constructor);
    255             }
    256 
    257             Object[] args = mConstructorArgs;
    258             args[1] = attrs;
    259             return (Preference) constructor.newInstance(args);
    260 
    261         } catch (ClassNotFoundException e) {
    262             // If loadClass fails, we should propagate the exception.
    263             throw e;
    264         } catch (Exception e) {
    265             final InflateException ie = new InflateException(attrs
    266                     .getPositionDescription() + ": Error inflating class " + name);
    267             ie.initCause(e);
    268             throw ie;
    269         }
    270     }
    271 
    272     /**
    273      * This routine is responsible for creating the correct subclass of item
    274      * given the xml element name. Override it to handle custom item objects. If
    275      * you override this in your subclass be sure to call through to
    276      * super.onCreateItem(name) for names you do not recognize.
    277      *
    278      * @param name The fully qualified class name of the item to be create.
    279      * @param attrs An AttributeSet of attributes to apply to the item.
    280      * @return The item created.
    281      */
    282     protected Preference onCreateItem(String name, AttributeSet attrs)
    283             throws ClassNotFoundException {
    284         return createItem(name, mDefaultPackages, attrs);
    285     }
    286 
    287     private Preference createItemFromTag(String name,
    288             AttributeSet attrs) {
    289         try {
    290             final Preference item;
    291 
    292             if (-1 == name.indexOf('.')) {
    293                 item = onCreateItem(name, attrs);
    294             } else {
    295                 item = createItem(name, null, attrs);
    296             }
    297 
    298             return item;
    299 
    300         } catch (InflateException e) {
    301             throw e;
    302 
    303         } catch (ClassNotFoundException e) {
    304             final InflateException ie = new InflateException(attrs
    305                     .getPositionDescription()
    306                     + ": Error inflating class (not found)" + name);
    307             ie.initCause(e);
    308             throw ie;
    309 
    310         } catch (Exception e) {
    311             final InflateException ie = new InflateException(attrs
    312                     .getPositionDescription()
    313                     + ": Error inflating class " + name);
    314             ie.initCause(e);
    315             throw ie;
    316         }
    317     }
    318 
    319     /**
    320      * Recursive method used to descend down the xml hierarchy and instantiate
    321      * items, instantiate their children, and then call onFinishInflate().
    322      */
    323     private void rInflate(XmlPullParser parser, Preference parent, final AttributeSet attrs)
    324             throws XmlPullParserException, IOException {
    325         final int depth = parser.getDepth();
    326 
    327         int type;
    328         while (((type = parser.next()) != XmlPullParser.END_TAG ||
    329                 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    330 
    331             if (type != XmlPullParser.START_TAG) {
    332                 continue;
    333             }
    334 
    335             final String name = parser.getName();
    336 
    337             if (INTENT_TAG_NAME.equals(name)) {
    338                 final Intent intent;
    339 
    340                 try {
    341                     intent = Intent.parseIntent(getContext().getResources(), parser, attrs);
    342                 } catch (IOException e) {
    343                     XmlPullParserException ex = new XmlPullParserException(
    344                             "Error parsing preference");
    345                     ex.initCause(e);
    346                     throw ex;
    347                 }
    348 
    349                 parent.setIntent(intent);
    350             } else if (EXTRA_TAG_NAME.equals(name)) {
    351                 getContext().getResources().parseBundleExtra(EXTRA_TAG_NAME, attrs,
    352                         parent.getExtras());
    353                 try {
    354                     skipCurrentTag(parser);
    355                 } catch (IOException e) {
    356                     XmlPullParserException ex = new XmlPullParserException(
    357                             "Error parsing preference");
    358                     ex.initCause(e);
    359                     throw ex;
    360                 }
    361             } else {
    362                 final Preference item = createItemFromTag(name, attrs);
    363                 ((PreferenceGroup) parent).addItemFromInflater(item);
    364                 rInflate(parser, item, attrs);
    365             }
    366         }
    367 
    368     }
    369 
    370     private static void skipCurrentTag(XmlPullParser parser)
    371             throws XmlPullParserException, IOException {
    372         int outerDepth = parser.getDepth();
    373         int type;
    374         do {
    375             type = parser.next();
    376         } while (type != XmlPullParser.END_DOCUMENT
    377                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth));
    378     }
    379 
    380 }
    381