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