1/*
2 * Copyright (C) 2017 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 android.content.res.Resources;
20import android.content.res.XmlResourceParser;
21import android.support.annotation.NonNull;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.util.Xml;
25import android.view.InflateException;
26
27import org.xmlpull.v1.XmlPullParser;
28import org.xmlpull.v1.XmlPullParserException;
29
30import java.io.IOException;
31
32/**
33 * A simple XML inflater, which takes care of moving the parser to the correct position. Subclasses
34 * need to implement {@link #onCreateItem(String, AttributeSet)} to create an object representation
35 * and {@link #onAddChildItem(Object, Object)} to attach a child tag to the parent tag.
36 *
37 * @param <T> The class where all instances (including child elements) belong to. If parent and
38 *     child elements belong to different class hierarchies, it's OK to set this to {@link Object}.
39 */
40public abstract class SimpleInflater<T> {
41
42    private static final String TAG = "SimpleInflater";
43    private static final boolean DEBUG = false;
44
45    protected final Resources mResources;
46
47    /**
48     * Create a new inflater instance associated with a particular Resources bundle.
49     *
50     * @param resources The Resources class used to resolve given resource IDs.
51     */
52    protected SimpleInflater(@NonNull Resources resources) {
53        mResources = resources;
54    }
55
56    public Resources getResources() {
57        return mResources;
58    }
59
60    /**
61     * Inflate a new hierarchy from the specified XML resource. Throws InflaterException if there is
62     * an error.
63     *
64     * @param resId ID for an XML resource to load (e.g. <code>R.xml.my_xml</code>)
65     * @return The root of the inflated hierarchy.
66     */
67    public T inflate(int resId) {
68        XmlResourceParser parser = getResources().getXml(resId);
69        try {
70            return inflate(parser);
71        } finally {
72            parser.close();
73        }
74    }
75
76    /**
77     * Inflate a new hierarchy from the specified XML node. Throws InflaterException if there is an
78     * error.
79     * <p>
80     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
81     * reasons, inflation relies heavily on pre-processing of XML files
82     * that is done at build time. Therefore, it is not currently possible to
83     * use inflater with an XmlPullParser over a plain XML file at runtime.
84     *
85     * @param parser XML dom node containing the description of the hierarchy.
86     * @return The root of the inflated hierarchy.
87     */
88    public T inflate(XmlPullParser parser) {
89        final AttributeSet attrs = Xml.asAttributeSet(parser);
90        T createdItem;
91
92        try {
93            // Look for the root node.
94            int type;
95            while ((type = parser.next()) != XmlPullParser.START_TAG
96                    && type != XmlPullParser.END_DOCUMENT) {
97                // continue
98            }
99
100            if (type != XmlPullParser.START_TAG) {
101                throw new InflateException(parser.getPositionDescription()
102                        + ": No start tag found!");
103            }
104
105            createdItem = createItemFromTag(parser.getName(), attrs);
106
107            rInflate(parser, createdItem, attrs);
108        } catch (XmlPullParserException e) {
109            throw new InflateException(e.getMessage(), e);
110        } catch (IOException e) {
111            throw new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e);
112        }
113
114        return createdItem;
115    }
116
117    /**
118     * This routine is responsible for creating the correct subclass of item
119     * given the xml element name.
120     *
121     * @param tagName The XML tag name for the item to be created.
122     * @param attrs An AttributeSet of attributes to apply to the item.
123     * @return The item created.
124     */
125    protected abstract T onCreateItem(String tagName, AttributeSet attrs);
126
127    private T createItemFromTag(String name, AttributeSet attrs) {
128        try {
129            T item = onCreateItem(name, attrs);
130            if (DEBUG) Log.v(TAG, item + " created for <" + name + ">");
131            return item;
132        } catch (InflateException e) {
133            throw e;
134        } catch (Exception e) {
135            throw new InflateException(attrs.getPositionDescription()
136                    + ": Error inflating class " + name, e);
137        }
138    }
139
140    /**
141     * Recursive method used to descend down the xml hierarchy and instantiate
142     * items, instantiate their children, and then call onFinishInflate().
143     */
144    private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs)
145            throws XmlPullParserException, IOException {
146        final int depth = parser.getDepth();
147
148        int type;
149        while (((type = parser.next()) != XmlPullParser.END_TAG
150                || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
151
152            if (type != XmlPullParser.START_TAG) {
153                continue;
154            }
155
156            if (onInterceptCreateItem(parser, parent, attrs)) {
157                continue;
158            }
159
160            String name = parser.getName();
161            T item = createItemFromTag(name, attrs);
162
163            onAddChildItem(parent, item);
164
165            rInflate(parser, item, attrs);
166        }
167    }
168
169    /**
170     * Whether item creation should be intercepted to perform custom handling on the parser rather
171     * than creating an object from it. This is used in rare cases where a tag doesn't correspond
172     * to creation of an object.
173     *
174     * The parser will be pointing to the start of a tag, you must stop parsing and return when you
175     * reach the end of this element. That is, this method is responsible for parsing the element
176     * at the given position together with all of its child tags.
177     *
178     * Note that parsing of the root tag cannot be intercepted.
179     *
180     * @param parser XML dom node containing the description of the hierarchy.
181     * @param parent The item that should be the parent of whatever you create.
182     * @param attrs An AttributeSet of attributes to apply to the item.
183     * @return True to continue parsing without calling {@link #onCreateItem(String, AttributeSet)},
184     *     or false if this inflater should proceed to create an item.
185     */
186    protected boolean onInterceptCreateItem(XmlPullParser parser, T parent, AttributeSet attrs)
187            throws XmlPullParserException {
188        return false;
189    }
190
191    protected abstract void onAddChildItem(T parent, T child);
192}
193