blob: 3319e6431128a29954320d339b14792273004f6e [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Gilles Debunne76f0ce12010-05-03 18:50:25 -070019import java.io.IOException;
20import java.lang.reflect.Constructor;
21import java.util.HashMap;
22
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
Tor Norbye7b9c9122013-05-30 16:48:33 -070026import android.annotation.XmlRes;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import 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
Gilles Debunne76f0ce12010-05-03 18:50:25 -070041 * @param T The type of the items to inflate
42 * @param P The type of parents (that is those items that contain other items).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043 * Must implement {@link GenericInflater.Parent}
44 */
Gilles Debunne76f0ce12010-05-03 18:50:25 -070045abstract class GenericInflater<T, P extends GenericInflater.Parent> {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 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
Gilles Debunne76f0ce12010-05-03 18:50:25 -070056 private static final Class[] mConstructorSignature = new Class[] {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057 Context.class, AttributeSet.class};
58
Gilles Debunne76f0ce12010-05-03 18:50:25 -070059 private static final HashMap sConstructorMap = new HashMap();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060
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 */
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700138 public abstract GenericInflater cloneInContext(Context newContext);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800139
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("" +
John Spurlock8a985d22014-02-25 09:40:05 -0500195 "A factory has already been set on this inflater");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196 }
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 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700220 public T inflate(@XmlRes int resource, P root) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800221 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 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700260 public T inflate(@XmlRes int resource, P root, boolean attachToRoot) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800261 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 */
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700291 public T inflate(XmlPullParser parser, P root,
292 boolean attachToRoot) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 synchronized (mConstructorArgs) {
294 final AttributeSet attrs = Xml.asAttributeSet(parser);
295 mConstructorArgs[0] = mContext;
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700296 T result = (T) root;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800297
298 try {
299 // Look for the root node.
300 int type;
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700301 while ((type = parser.next()) != parser.START_TAG
302 && type != parser.END_DOCUMENT) {
303 ;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800304 }
305
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700306 if (type != parser.START_TAG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800307 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
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700321 result = (T) onMergeRoots(root, attachToRoot, (P) xmlRoot);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800322
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
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700347 return result;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800348 }
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 *
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700366 * @return The newly instantied item, or null.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367 */
368 public final T createItem(String name, String prefix, AttributeSet attrs)
369 throws ClassNotFoundException, InflateException {
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700370 Constructor constructor = (Constructor) sConstructorMap.get(name);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800371
372 try {
373 if (null == constructor) {
374 // Class not found in the cache, see if it's real,
375 // and try to add it
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700376 Class clazz = mContext.getClassLoader().loadClass(
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 prefix != null ? (prefix + name) : name);
378 constructor = clazz.getConstructor(mConstructorSignature);
Alan Viverette904de2e2015-05-04 10:32:57 -0700379 constructor.setAccessible(true);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800380 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 */
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700462 private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800463 throws XmlPullParserException, IOException {
464 final int depth = parser.getDepth();
465
466 int type;
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700467 while (((type = parser.next()) != parser.END_TAG ||
468 parser.getDepth() > depth) && type != parser.END_DOCUMENT) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800469
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700470 if (type != parser.START_TAG) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800471 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
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700490 ((P) parent).addItemFromInflater(item);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800491
492 if (DEBUG) {
493 System.out.println("-----> start inflating children");
494 }
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700495 rInflate(parser, item, attrs);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800496 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 */
Gilles Debunne76f0ce12010-05-03 18:50:25 -0700514 protected boolean onCreateCustomFromTag(XmlPullParser parser, T parent,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 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}