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