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