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 com.android.setupwizardlib.items;
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.Log;
30import android.util.Xml;
31import android.view.ContextThemeWrapper;
32import android.view.InflateException;
33
34/**
35 * Generic XML inflater. This class is modeled after {@code android.preference.GenericInflater},
36 * which is in turn modeled after {@code LayoutInflater}. This can be used to recursively inflate a
37 * hierarchy of items. All items in the hierarchy must inherit the generic type {@code T}, and the
38 * specific implementation is expected to handle inserting child items into the parent, by
39 * implementing {@link #onAddChildItem(Object, Object)}.
40 *
41 * @param <T> Type of the items to inflate
42 */
43public abstract class GenericInflater<T> {
44
45    private static final String TAG = "GenericInflater";
46    private static 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<String, Constructor<?>> sConstructorMap = new HashMap<>();
60
61    private String mDefaultPackage;
62
63    public interface Factory<T> {
64        /**
65         * Hook you can supply that is called when inflating from a
66         * inflater. You can use this to customize the tag
67         * names available in your XML files.
68         * <p>
69         * Note that it is good practice to prefix these custom names with your
70         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
71         * names.
72         *
73         * @param name Tag name to be inflated.
74         * @param context The context the item is being created in.
75         * @param attrs Inflation attributes as specified in XML file.
76         * @return Newly created item. Return null for the default behavior.
77         */
78        T onCreateItem(String name, Context context, AttributeSet attrs);
79    }
80
81    private static class FactoryMerger<T> implements Factory<T> {
82        private final Factory<T> mF1, mF2;
83
84        FactoryMerger(Factory<T> f1, Factory<T> f2) {
85            mF1 = f1;
86            mF2 = f2;
87        }
88
89        public T onCreateItem(String name, Context context, AttributeSet attrs) {
90            T v = mF1.onCreateItem(name, context, attrs);
91            if (v != null) return v;
92            return mF2.onCreateItem(name, context, attrs);
93        }
94    }
95
96    /**
97     * Create a new inflater instance associated with a
98     * particular Context.
99     *
100     * @param context The Context in which this inflater will
101     *            create its items; most importantly, this supplies the theme
102     *            from which the default values for their attributes are
103     *            retrieved.
104     */
105    protected GenericInflater(Context context) {
106        mContext = context;
107    }
108
109    /**
110     * Create a new inflater instance that is a copy of an
111     * existing inflater, optionally with its Context
112     * changed. For use in implementing {@link #cloneInContext}.
113     *
114     * @param original The original inflater to copy.
115     * @param newContext The new Context to use.
116     */
117    protected GenericInflater(GenericInflater<T> original, Context newContext) {
118        mContext = newContext;
119        mFactory = original.mFactory;
120    }
121
122    /**
123     * Create a copy of the existing inflater object, with the copy
124     * pointing to a different Context than the original.  This is used by
125     * {@link ContextThemeWrapper} to create a new inflater to go along
126     * with the new Context theme.
127     *
128     * @param newContext The new Context to associate with the new inflater.
129     * May be the same as the original Context if desired.
130     *
131     * @return Returns a brand spanking new inflater object associated with
132     * the given Context.
133     */
134    public abstract GenericInflater cloneInContext(Context newContext);
135
136    /**
137     * Sets the default package that will be searched for classes to construct
138     * for tag names that have no explicit package.
139     *
140     * @param defaultPackage The default package. This will be prepended to the
141     *            tag name, so it should end with a period.
142     */
143    public void setDefaultPackage(String defaultPackage) {
144        mDefaultPackage = defaultPackage;
145    }
146
147    /**
148     * Returns the default package, or null if it is not set.
149     *
150     * @see #setDefaultPackage(String)
151     * @return The default package.
152     */
153    public String getDefaultPackage() {
154        return mDefaultPackage;
155    }
156
157    /**
158     * Return the context we are running in, for access to resources, class
159     * loader, etc.
160     */
161    public Context getContext() {
162        return mContext;
163    }
164
165    /**
166     * Return the current factory (or null). This is called on each element
167     * name. If the factory returns an item, add that to the hierarchy. If it
168     * returns null, proceed to call onCreateItem(name).
169     */
170    public final Factory<T> getFactory() {
171        return mFactory;
172    }
173
174    /**
175     * Attach a custom Factory interface for creating items while using this
176     * inflater. This must not be null, and can only be set
177     * once; after setting, you can not change the factory. This is called on
178     * each element name as the XML is parsed. If the factory returns an item,
179     * that is added to the hierarchy. If it returns null, the next factory
180     * default {@link #onCreateItem} method is called.
181     * <p>
182     * If you have an existing inflater and want to add your
183     * own factory to it, use {@link #cloneInContext} to clone the existing
184     * instance and then you can use this function (once) on the returned new
185     * instance. This will merge your own factory with whatever factory the
186     * original instance is using.
187     */
188    public void setFactory(Factory<T> factory) {
189        if (mFactorySet) {
190            throw new IllegalStateException("" +
191                    "A factory has already been set on this inflater");
192        }
193        if (factory == null) {
194            throw new NullPointerException("Given factory can not be null");
195        }
196        mFactorySet = true;
197        if (mFactory == null) {
198            mFactory = factory;
199        } else {
200            mFactory = new FactoryMerger<>(factory, mFactory);
201        }
202    }
203
204    public T inflate(int resource) {
205        return inflate(resource, null);
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     */
220    public T inflate(int resource, T 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     */
240    public T inflate(XmlPullParser parser, T 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     */
260    public T inflate(int resource, T root, boolean attachToRoot) {
261        if (DEBUG) Log.v(TAG, "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     */
291    public T inflate(XmlPullParser parser, T root, boolean attachToRoot) {
292        synchronized (mConstructorArgs) {
293            final AttributeSet attrs = Xml.asAttributeSet(parser);
294            mConstructorArgs[0] = mContext;
295            T result;
296
297            try {
298                // Look for the root node.
299                int type;
300                while ((type = parser.next()) != XmlPullParser.START_TAG
301                        && type != XmlPullParser.END_DOCUMENT) {
302                }
303
304                if (type != XmlPullParser.START_TAG) {
305                    throw new InflateException(parser.getPositionDescription()
306                            + ": No start tag found!");
307                }
308
309                if (DEBUG) {
310                    Log.v(TAG, "**************************");
311                    Log.v(TAG, "Creating root: "
312                            + parser.getName());
313                    Log.v(TAG, "**************************");
314                }
315                // Temp is the root that was found in the xml
316                T xmlRoot = createItemFromTag(parser, parser.getName(), attrs);
317
318                result = onMergeRoots(root, attachToRoot, xmlRoot);
319
320                if (DEBUG) Log.v(TAG, "-----> start inflating children");
321                // Inflate all children under temp
322                rInflate(parser, result, attrs);
323                if (DEBUG) Log.v(TAG, "-----> done inflating children");
324            } catch (XmlPullParserException e) {
325                InflateException ex = new InflateException(e.getMessage());
326                ex.initCause(e);
327                throw ex;
328            } catch (IOException e) {
329                InflateException ex = new InflateException(
330                        parser.getPositionDescription()
331                                + ": " + e.getMessage());
332                ex.initCause(e);
333                throw ex;
334            }
335
336            return result;
337        }
338    }
339
340    /**
341     * Low-level function for instantiating by name. This attempts to
342     * instantiate class of the given <var>name</var> found in this
343     * inflater's ClassLoader.
344     *
345     * <p>
346     * There are two things that can happen in an error case: either the
347     * exception describing the error will be thrown, or a null will be
348     * returned. You must deal with both possibilities -- the former will happen
349     * the first time createItem() is called for a class of a particular name,
350     * the latter every time there-after for that class name.
351     *
352     * @param name The full name of the class to be instantiated.
353     * @param attrs The XML attributes supplied for this instance.
354     *
355     * @return The newly instantiated item, or null.
356     */
357    public final T createItem(String name, String prefix, AttributeSet attrs)
358            throws ClassNotFoundException, InflateException {
359        Constructor constructor = sConstructorMap.get(name);
360
361        try {
362            if (constructor == null) {
363                // Class not found in the cache, see if it's real,
364                // and try to add it
365                Class<?> clazz = mContext.getClassLoader().loadClass(
366                        prefix != null ? (prefix + name) : name);
367                constructor = clazz.getConstructor(mConstructorSignature);
368                constructor.setAccessible(true);
369                sConstructorMap.put(name, constructor);
370            }
371
372            Object[] args = mConstructorArgs;
373            args[1] = attrs;
374            //noinspection unchecked
375            return (T) constructor.newInstance(args);
376        } catch (NoSuchMethodException e) {
377            InflateException ie = new InflateException(attrs.getPositionDescription()
378                    + ": Error inflating class "
379                    + (prefix != null ? (prefix + name) : name));
380            ie.initCause(e);
381            throw ie;
382
383        } catch (ClassNotFoundException e) {
384            // If loadClass fails, we should propagate the exception.
385            throw e;
386        } catch (Exception e) {
387            InflateException ie = new InflateException(attrs.getPositionDescription()
388                    + ": Error inflating class "
389                    + (prefix != null ? (prefix + name) : name));
390            ie.initCause(e);
391            throw ie;
392        }
393    }
394
395    /**
396     * This routine is responsible for creating the correct subclass of item
397     * given the xml element name. Override it to handle custom item objects. If
398     * you override this in your subclass be sure to call through to
399     * super.onCreateItem(name) for names you do not recognize.
400     *
401     * @param name The fully qualified class name of the item to be create.
402     * @param attrs An AttributeSet of attributes to apply to the item.
403     * @return The item created.
404     */
405    protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException {
406        return createItem(name, mDefaultPackage, attrs);
407    }
408
409    private T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) {
410        if (DEBUG) Log.v(TAG, "******** Creating item: " + name);
411
412        try {
413            T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs);
414
415            if (item == null) {
416                if (-1 == name.indexOf('.')) {
417                    item = onCreateItem(name, attrs);
418                } else {
419                    item = createItem(name, null, attrs);
420                }
421            }
422
423            if (DEBUG) Log.v(TAG, "Created item is: " + item);
424            return item;
425
426        } catch (InflateException e) {
427            throw e;
428
429        } catch (Exception e) {
430            InflateException ie = new InflateException(attrs
431                    .getPositionDescription()
432                    + ": Error inflating class " + name);
433            ie.initCause(e);
434            throw ie;
435        }
436    }
437
438    /**
439     * Recursive method used to descend down the xml hierarchy and instantiate
440     * items, instantiate their children, and then call onFinishInflate().
441     */
442    private void rInflate(XmlPullParser parser, T node, final AttributeSet attrs)
443            throws XmlPullParserException, IOException {
444        final int depth = parser.getDepth();
445
446        int type;
447        while (((type = parser.next()) != XmlPullParser.END_TAG ||
448                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
449
450            if (type != XmlPullParser.START_TAG) {
451                continue;
452            }
453
454            if (onCreateCustomFromTag(parser, node, attrs)) {
455                continue;
456            }
457
458            if (DEBUG) Log.v(TAG, "Now inflating tag: " + parser.getName());
459            String name = parser.getName();
460
461            T item = createItemFromTag(parser, name, attrs);
462
463            if (DEBUG) Log.v(TAG, "Creating params from parent: " + node);
464
465            onAddChildItem(node, item);
466
467
468            if (DEBUG) Log.v(TAG, "-----> start inflating children");
469            rInflate(parser, item, attrs);
470            if (DEBUG) Log.v(TAG, "-----> done inflating children");
471        }
472    }
473
474    /**
475     * Before this inflater tries to create an item from the tag, this method
476     * will be called. The parser will be pointing to the start of a tag, you
477     * must stop parsing and return when you reach the end of this element!
478     *
479     * @param parser XML dom node containing the description of the hierarchy.
480     * @param node The item that should be the parent of whatever you create.
481     * @param attrs An AttributeSet of attributes to apply to the item.
482     * @return Whether you created a custom object (true), or whether this
483     *         inflater should proceed to create an item.
484     */
485    protected boolean onCreateCustomFromTag(XmlPullParser parser, T node,
486            final AttributeSet attrs) throws XmlPullParserException {
487        return false;
488    }
489
490    protected abstract void onAddChildItem(T parent, T child);
491
492    protected T onMergeRoots(T givenRoot, boolean attachToGivenRoot, T xmlRoot) {
493        return xmlRoot;
494    }
495}
496