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.annotation.XmlRes;
27 import android.content.Context;
28 import android.content.res.XmlResourceParser;
29 import android.util.AttributeSet;
30 import android.util.Xml;
31 import android.view.ContextThemeWrapper;
32 import android.view.InflateException;
33 import 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  */
45 abstract 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> {
addItemFromInflater(T child)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          */
onCreateItem(String name, Context context, AttributeSet attrs)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 
FactoryMerger(Factory<T> f1, Factory<T> f2)88         FactoryMerger(Factory<T> f1, Factory<T> f2) {
89             mF1 = f1;
90             mF2 = f2;
91         }
92 
onCreateItem(String name, Context context, AttributeSet attrs)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      */
GenericInflater(Context context)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      */
GenericInflater(GenericInflater<T,P> original, Context newContext)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      */
cloneInContext(Context newContext)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      */
setDefaultPackage(String defaultPackage)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      */
getDefaultPackage()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      */
getContext()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      */
getFactory()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      */
setFactory(Factory<T> factory)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      */
inflate(@mlRes int resource, P root)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      */
inflate(XmlPullParser parser, P root)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      */
inflate(@mlRes int resource, P root, boolean attachToRoot)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      */
inflate(XmlPullParser parser, P root, boolean attachToRoot)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      */
createItem(String name, String prefix, AttributeSet attrs)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      */
onCreateItem(String name, AttributeSet attrs)418     protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException {
419         return createItem(name, mDefaultPackage, attrs);
420     }
421 
createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs)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      */
rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)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      */
onCreateCustomFromTag(XmlPullParser parser, T parent, final AttributeSet attrs)514     protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent,
515             final AttributeSet attrs) throws XmlPullParserException {
516         return false;
517     }
518 
onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot)519     protected P onMergeRoots(P givenRoot, boolean attachToGivenRoot, P xmlRoot) {
520         return xmlRoot;
521     }
522 }
523