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
17package android.preference;
18
19import java.io.IOException;
20import java.lang.reflect.Constructor;
21import java.util.HashMap;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
26import android.annotation.XmlRes;
27import android.content.Context;
28import android.content.res.XmlResourceParser;
29import android.util.AttributeSet;
30import android.util.Xml;
31import android.view.ContextThemeWrapper;
32import android.view.InflateException;
33import 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 */
45abstract 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