• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 java.io.IOException;
20 import java.lang.reflect.Constructor;
21 import java.util.HashMap;
22 
23 import org.xmlpull.v1.XmlPullParser;
24 import org.xmlpull.v1.XmlPullParserException;
25 
26 import android.content.Context;
27 import android.content.res.XmlResourceParser;
28 import android.util.AttributeSet;
29 import android.util.Xml;
30 import android.view.ContextThemeWrapper;
31 import android.view.InflateException;
32 import 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  */
44 abstract 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> {
addItemFromInflater(T child)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          */
onCreateItem(String name, Context context, AttributeSet attrs)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 
FactoryMerger(Factory<T> f1, Factory<T> f2)87         FactoryMerger(Factory<T> f1, Factory<T> f2) {
88             mF1 = f1;
89             mF2 = f2;
90         }
91 
onCreateItem(String name, Context context, AttributeSet attrs)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      */
GenericInflater(Context context)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      */
GenericInflater(GenericInflater<T,P> original, Context newContext)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      */
cloneInContext(Context newContext)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      */
setDefaultPackage(String defaultPackage)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      */
getDefaultPackage()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      */
getContext()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      */
getFactory()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      */
setFactory(Factory<T> factory)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      */
inflate(int resource, P root)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      */
inflate(XmlPullParser parser, P root)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      */
inflate(int resource, P root, boolean attachToRoot)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      */
inflate(XmlPullParser parser, P root, boolean attachToRoot)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      */
createItem(String name, String prefix, AttributeSet attrs)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      */
onCreateItem(String name, AttributeSet attrs)416     protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException {
417         return createItem(name, mDefaultPackage, attrs);
418     }
419 
createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs)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      */
rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)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      */
onCreateCustomFromTag(XmlPullParser parser, T parent, final AttributeSet attrs)512     protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent,
513             final AttributeSet attrs) throws XmlPullParserException {
514         return false;
515     }
516 
onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot)517     protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) {
518         return xmlRoot;
519     }
520 }
521