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