diff --git a/library/main/src/com/android/setupwizardlib/items/GenericInflater.java b/library/main/src/com/android/setupwizardlib/items/GenericInflater.java deleted file mode 100644 index f3b6e19..0000000 --- a/library/main/src/com/android/setupwizardlib/items/GenericInflater.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.setupwizardlib.items; - -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.util.HashMap; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import android.content.Context; -import android.content.res.XmlResourceParser; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Xml; -import android.view.ContextThemeWrapper; -import android.view.InflateException; - -/** - * Generic XML inflater. This class is modeled after {@code android.preference.GenericInflater}, - * which is in turn modeled after {@code LayoutInflater}. This can be used to recursively inflate a - * hierarchy of items. All items in the hierarchy must inherit the generic type {@code T}, and the - * specific implementation is expected to handle inserting child items into the parent, by - * implementing {@link #onAddChildItem(Object, Object)}. - * - * @param Type of the items to inflate - */ -public abstract class GenericInflater { - - private static final String TAG = "GenericInflater"; - private static final boolean DEBUG = false; - - protected final Context mContext; - - // these are optional, set by the caller - private boolean mFactorySet; - private Factory mFactory; - - private final Object[] mConstructorArgs = new Object[2]; - - private static final Class[] mConstructorSignature = new Class[] { - Context.class, AttributeSet.class}; - - private static final HashMap> sConstructorMap = new HashMap<>(); - - private String mDefaultPackage; - - public interface Factory { - /** - * Hook you can supply that is called when inflating from a - * inflater. You can use this to customize the tag - * names available in your XML files. - *

- * Note that it is good practice to prefix these custom names with your - * package (i.e., com.coolcompany.apps) to avoid conflicts with system - * names. - * - * @param name Tag name to be inflated. - * @param context The context the item is being created in. - * @param attrs Inflation attributes as specified in XML file. - * @return Newly created item. Return null for the default behavior. - */ - T onCreateItem(String name, Context context, AttributeSet attrs); - } - - private static class FactoryMerger implements Factory { - private final Factory mF1, mF2; - - FactoryMerger(Factory f1, Factory f2) { - mF1 = f1; - mF2 = f2; - } - - public T onCreateItem(String name, Context context, AttributeSet attrs) { - T v = mF1.onCreateItem(name, context, attrs); - if (v != null) return v; - return mF2.onCreateItem(name, context, attrs); - } - } - - /** - * Create a new inflater instance associated with a - * particular Context. - * - * @param context The Context in which this inflater will - * create its items; most importantly, this supplies the theme - * from which the default values for their attributes are - * retrieved. - */ - protected GenericInflater(Context context) { - mContext = context; - } - - /** - * Create a new inflater instance that is a copy of an - * existing inflater, optionally with its Context - * changed. For use in implementing {@link #cloneInContext}. - * - * @param original The original inflater to copy. - * @param newContext The new Context to use. - */ - protected GenericInflater(GenericInflater original, Context newContext) { - mContext = newContext; - mFactory = original.mFactory; - } - - /** - * Create a copy of the existing inflater object, with the copy - * pointing to a different Context than the original. This is used by - * {@link ContextThemeWrapper} to create a new inflater to go along - * with the new Context theme. - * - * @param newContext The new Context to associate with the new inflater. - * May be the same as the original Context if desired. - * - * @return Returns a brand spanking new inflater object associated with - * the given Context. - */ - public abstract GenericInflater cloneInContext(Context newContext); - - /** - * Sets the default package that will be searched for classes to construct - * for tag names that have no explicit package. - * - * @param defaultPackage The default package. This will be prepended to the - * tag name, so it should end with a period. - */ - public void setDefaultPackage(String defaultPackage) { - mDefaultPackage = defaultPackage; - } - - /** - * Returns the default package, or null if it is not set. - * - * @see #setDefaultPackage(String) - * @return The default package. - */ - public String getDefaultPackage() { - return mDefaultPackage; - } - - /** - * Return the context we are running in, for access to resources, class - * loader, etc. - */ - public Context getContext() { - return mContext; - } - - /** - * Return the current factory (or null). This is called on each element - * name. If the factory returns an item, add that to the hierarchy. If it - * returns null, proceed to call onCreateItem(name). - */ - public final Factory getFactory() { - return mFactory; - } - - /** - * Attach a custom Factory interface for creating items while using this - * inflater. This must not be null, and can only be set - * once; after setting, you can not change the factory. This is called on - * each element name as the XML is parsed. If the factory returns an item, - * that is added to the hierarchy. If it returns null, the next factory - * default {@link #onCreateItem} method is called. - *

- * If you have an existing inflater and want to add your - * own factory to it, use {@link #cloneInContext} to clone the existing - * instance and then you can use this function (once) on the returned new - * instance. This will merge your own factory with whatever factory the - * original instance is using. - */ - public void setFactory(Factory factory) { - if (mFactorySet) { - throw new IllegalStateException("" + - "A factory has already been set on this inflater"); - } - if (factory == null) { - throw new NullPointerException("Given factory can not be null"); - } - mFactorySet = true; - if (mFactory == null) { - mFactory = factory; - } else { - mFactory = new FactoryMerger<>(factory, mFactory); - } - } - - public T inflate(int resource) { - return inflate(resource, null); - } - - - /** - * Inflate a new item hierarchy from the specified xml resource. Throws - * InflaterException if there is an error. - * - * @param resource ID for an XML resource to load (e.g., - * R.layout.main_page) - * @param root Optional parent of the generated hierarchy. - * @return The root of the inflated hierarchy. If root was supplied, - * this is the root item; otherwise it is the root of the inflated - * XML file. - */ - public T inflate(int resource, T root) { - return inflate(resource, root, root != null); - } - - /** - * Inflate a new hierarchy from the specified xml node. Throws - * InflaterException if there is an error. * - *

- * Important   For performance - * reasons, inflation relies heavily on pre-processing of XML files - * that is done at build time. Therefore, it is not currently possible to - * use inflater with an XmlPullParser over a plain XML file at runtime. - * - * @param parser XML dom node containing the description of the - * hierarchy. - * @param root Optional parent of the generated hierarchy. - * @return The root of the inflated hierarchy. If root was supplied, - * this is the that; otherwise it is the root of the inflated - * XML file. - */ - public T inflate(XmlPullParser parser, T root) { - return inflate(parser, root, root != null); - } - - /** - * Inflate a new hierarchy from the specified xml resource. Throws - * InflaterException if there is an error. - * - * @param resource ID for an XML resource to load (e.g., - * R.layout.main_page) - * @param root Optional root to be the parent of the generated hierarchy (if - * attachToRoot is true), or else simply an object that - * provides a set of values for root of the returned - * hierarchy (if attachToRoot is false.) - * @param attachToRoot Whether the inflated hierarchy should be attached to - * the root parameter? - * @return The root of the inflated hierarchy. If root was supplied and - * attachToRoot is true, this is root; otherwise it is the root of - * the inflated XML file. - */ - public T inflate(int resource, T root, boolean attachToRoot) { - if (DEBUG) Log.v(TAG, "INFLATING from resource: " + resource); - XmlResourceParser parser = getContext().getResources().getXml(resource); - try { - return inflate(parser, root, attachToRoot); - } finally { - parser.close(); - } - } - - /** - * Inflate a new hierarchy from the specified XML node. Throws - * InflaterException if there is an error. - *

- * Important   For performance - * reasons, inflation relies heavily on pre-processing of XML files - * that is done at build time. Therefore, it is not currently possible to - * use inflater with an XmlPullParser over a plain XML file at runtime. - * - * @param parser XML dom node containing the description of the - * hierarchy. - * @param root Optional to be the parent of the generated hierarchy (if - * attachToRoot is true), or else simply an object that - * provides a set of values for root of the returned - * hierarchy (if attachToRoot is false.) - * @param attachToRoot Whether the inflated hierarchy should be attached to - * the root parameter? - * @return The root of the inflated hierarchy. If root was supplied and - * attachToRoot is true, this is root; otherwise it is the root of - * the inflated XML file. - */ - public T inflate(XmlPullParser parser, T root, boolean attachToRoot) { - synchronized (mConstructorArgs) { - final AttributeSet attrs = Xml.asAttributeSet(parser); - mConstructorArgs[0] = mContext; - T result; - - try { - // Look for the root node. - int type; - while ((type = parser.next()) != XmlPullParser.START_TAG - && type != XmlPullParser.END_DOCUMENT) { - } - - if (type != XmlPullParser.START_TAG) { - throw new InflateException(parser.getPositionDescription() - + ": No start tag found!"); - } - - if (DEBUG) { - Log.v(TAG, "**************************"); - Log.v(TAG, "Creating root: " - + parser.getName()); - Log.v(TAG, "**************************"); - } - // Temp is the root that was found in the xml - T xmlRoot = createItemFromTag(parser, parser.getName(), attrs); - - result = onMergeRoots(root, attachToRoot, xmlRoot); - - if (DEBUG) Log.v(TAG, "-----> start inflating children"); - // Inflate all children under temp - rInflate(parser, result, attrs); - if (DEBUG) Log.v(TAG, "-----> done inflating children"); - } catch (XmlPullParserException e) { - InflateException ex = new InflateException(e.getMessage()); - ex.initCause(e); - throw ex; - } catch (IOException e) { - InflateException ex = new InflateException( - parser.getPositionDescription() - + ": " + e.getMessage()); - ex.initCause(e); - throw ex; - } - - return result; - } - } - - /** - * Low-level function for instantiating by name. This attempts to - * instantiate class of the given name found in this - * inflater's ClassLoader. - * - *

- * There are two things that can happen in an error case: either the - * exception describing the error will be thrown, or a null will be - * returned. You must deal with both possibilities -- the former will happen - * the first time createItem() is called for a class of a particular name, - * the latter every time there-after for that class name. - * - * @param name The full name of the class to be instantiated. - * @param attrs The XML attributes supplied for this instance. - * - * @return The newly instantiated item, or null. - */ - public final T createItem(String name, String prefix, AttributeSet attrs) - throws ClassNotFoundException, InflateException { - Constructor constructor = sConstructorMap.get(name); - - try { - if (constructor == null) { - // Class not found in the cache, see if it's real, - // and try to add it - Class clazz = mContext.getClassLoader().loadClass( - prefix != null ? (prefix + name) : name); - constructor = clazz.getConstructor(mConstructorSignature); - constructor.setAccessible(true); - sConstructorMap.put(name, constructor); - } - - Object[] args = mConstructorArgs; - args[1] = attrs; - //noinspection unchecked - return (T) constructor.newInstance(args); - } catch (NoSuchMethodException e) { - InflateException ie = new InflateException(attrs.getPositionDescription() - + ": Error inflating class " - + (prefix != null ? (prefix + name) : name)); - ie.initCause(e); - throw ie; - - } catch (ClassNotFoundException e) { - // If loadClass fails, we should propagate the exception. - throw e; - } catch (Exception e) { - InflateException ie = new InflateException(attrs.getPositionDescription() - + ": Error inflating class " - + (prefix != null ? (prefix + name) : name)); - ie.initCause(e); - throw ie; - } - } - - /** - * This routine is responsible for creating the correct subclass of item - * given the xml element name. Override it to handle custom item objects. If - * you override this in your subclass be sure to call through to - * super.onCreateItem(name) for names you do not recognize. - * - * @param name The fully qualified class name of the item to be create. - * @param attrs An AttributeSet of attributes to apply to the item. - * @return The item created. - */ - protected T onCreateItem(String name, AttributeSet attrs) throws ClassNotFoundException { - return createItem(name, mDefaultPackage, attrs); - } - - private T createItemFromTag(XmlPullParser parser, String name, AttributeSet attrs) { - if (DEBUG) Log.v(TAG, "******** Creating item: " + name); - - try { - T item = (mFactory == null) ? null : mFactory.onCreateItem(name, mContext, attrs); - - if (item == null) { - if (-1 == name.indexOf('.')) { - item = onCreateItem(name, attrs); - } else { - item = createItem(name, null, attrs); - } - } - - if (DEBUG) Log.v(TAG, "Created item is: " + item); - return item; - - } catch (InflateException e) { - throw e; - - } catch (Exception e) { - InflateException ie = new InflateException(attrs - .getPositionDescription() - + ": Error inflating class " + name); - ie.initCause(e); - throw ie; - } - } - - /** - * Recursive method used to descend down the xml hierarchy and instantiate - * items, instantiate their children, and then call onFinishInflate(). - */ - private void rInflate(XmlPullParser parser, T node, final AttributeSet attrs) - throws XmlPullParserException, IOException { - final int depth = parser.getDepth(); - - int type; - while (((type = parser.next()) != XmlPullParser.END_TAG || - parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { - - if (type != XmlPullParser.START_TAG) { - continue; - } - - if (onCreateCustomFromTag(parser, node, attrs)) { - continue; - } - - if (DEBUG) Log.v(TAG, "Now inflating tag: " + parser.getName()); - String name = parser.getName(); - - T item = createItemFromTag(parser, name, attrs); - - if (DEBUG) Log.v(TAG, "Creating params from parent: " + node); - - onAddChildItem(node, item); - - - if (DEBUG) Log.v(TAG, "-----> start inflating children"); - rInflate(parser, item, attrs); - if (DEBUG) Log.v(TAG, "-----> done inflating children"); - } - } - - /** - * Before this inflater tries to create an item from the tag, this method - * will be called. The parser will be pointing to the start of a tag, you - * must stop parsing and return when you reach the end of this element! - * - * @param parser XML dom node containing the description of the hierarchy. - * @param node The item that should be the parent of whatever you create. - * @param attrs An AttributeSet of attributes to apply to the item. - * @return Whether you created a custom object (true), or whether this - * inflater should proceed to create an item. - */ - protected boolean onCreateCustomFromTag(XmlPullParser parser, T node, - final AttributeSet attrs) throws XmlPullParserException { - return false; - } - - protected abstract void onAddChildItem(T parent, T child); - - protected T onMergeRoots(T givenRoot, boolean attachToGivenRoot, T xmlRoot) { - return xmlRoot; - } -} diff --git a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java index cadf1a4..618d785 100644 --- a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java +++ b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java @@ -20,39 +20,18 @@ import android.content.Context; /** * Inflate {@link Item} hierarchies from XML files. - * - * Modified from android.support.v7.preference.PreferenceInflater */ -public class ItemInflater extends GenericInflater { - - private static final String TAG = "ItemInflater"; +public class ItemInflater extends ReflectionInflater { public interface ItemParent { void addChild(ItemHierarchy child); } - private final Context mContext; - public ItemInflater(Context context) { super(context); - mContext = context; setDefaultPackage(Item.class.getPackage().getName() + "."); } - @Override - public ItemInflater cloneInContext(Context newContext) { - return new ItemInflater(newContext); - } - - /** - * Return the context we are running in, for access to resources, class - * loader, etc. - */ - @Override - public Context getContext() { - return mContext; - } - @Override protected void onAddChildItem(ItemHierarchy parent, ItemHierarchy child) { if (parent instanceof ItemParent) { diff --git a/library/main/src/com/android/setupwizardlib/items/ReflectionInflater.java b/library/main/src/com/android/setupwizardlib/items/ReflectionInflater.java new file mode 100644 index 0000000..feef2f9 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ReflectionInflater.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.items; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.InflateException; + +import java.lang.reflect.Constructor; +import java.util.HashMap; + +/** + * An XML inflater that creates items by reading the tag as a class name, and constructs said class + * by invoking the 2-argument constructor {@code Constructor(Context, AttributeSet)} via reflection. + * + *

Optionally a "default package" can be specified so that for unqualified tag names (i.e. names + * that do not contain "."), the default package will be prefixed onto the tag. + * + * @param The class where all instances (including child elements) belong to. If parent and + * child elements belong to different class hierarchies, it's OK to set this to {@link Object}. + */ +public abstract class ReflectionInflater extends SimpleInflater { + + /* static section */ + + private static final Class[] CONSTRUCTOR_SIGNATURE = + new Class[] {Context.class, AttributeSet.class}; + + private static final HashMap> sConstructorMap = new HashMap<>(); + + /* non-static section */ + + // Array used to contain the constructor arguments (Context, AttributeSet), to avoid allocating + // a new array for creation of every item. + private final Object[] mTempConstructorArgs = new Object[2]; + + @Nullable + private String mDefaultPackage; + + @NonNull + private final Context mContext; + + /** + * Create a new inflater instance associated with a particular Context. + * + * @param context The context used to resolve resource IDs. This context is also passed to the + * constructor of the items created as the first argument. + */ + protected ReflectionInflater(@NonNull Context context) { + super(context.getResources()); + mContext = context; + } + + @NonNull + public Context getContext() { + return mContext; + } + + /** + * Instantiate the class by name. This attempts to instantiate class of the given {@code name} + * found in this inflater's ClassLoader. + * + * @param tagName The full name of the class to be instantiated. + * @param attrs The XML attributes supplied for this instance. + * + * @return The newly instantiated item. + */ + @NonNull + public final T createItem(String tagName, String prefix, AttributeSet attrs) { + String qualifiedName = prefix != null ? prefix.concat(tagName) : tagName; + Constructor constructor = sConstructorMap.get(qualifiedName); + + try { + if (constructor == null) { + // Class not found in the cache, see if it's real, + // and try to add it + Class clazz = mContext.getClassLoader().loadClass(qualifiedName); + constructor = clazz.getConstructor(CONSTRUCTOR_SIGNATURE); + constructor.setAccessible(true); + sConstructorMap.put(tagName, constructor); + } + + mTempConstructorArgs[0] = mContext; + mTempConstructorArgs[1] = attrs; + // noinspection unchecked + final T item = (T) constructor.newInstance(mTempConstructorArgs); + mTempConstructorArgs[0] = null; + mTempConstructorArgs[1] = null; + return item; + } catch (Exception e) { + throw new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + qualifiedName, e); + } + } + + @Override + protected T onCreateItem(String tagName, AttributeSet attrs) { + return createItem(tagName, mDefaultPackage, attrs); + } + + /** + * Sets the default package that will be searched for classes to construct for tag names that + * have no explicit package. + * + * @param defaultPackage The default package. This will be prepended to the tag name, so it + * should end with a period. + */ + public void setDefaultPackage(@Nullable String defaultPackage) { + mDefaultPackage = defaultPackage; + } + + /** + * Returns the default package, or null if it is not set. + * + * @see #setDefaultPackage(String) + * @return The default package. + */ + @Nullable + public String getDefaultPackage() { + return mDefaultPackage; + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/SimpleInflater.java b/library/main/src/com/android/setupwizardlib/items/SimpleInflater.java new file mode 100644 index 0000000..1b48108 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/SimpleInflater.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.items; + +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; +import android.view.InflateException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; + +/** + * A simple XML inflater, which takes care of moving the parser to the correct position. Subclasses + * need to implement {@link #onCreateItem(String, AttributeSet)} to create an object representation + * and {@link #onAddChildItem(Object, Object)} to attach a child tag to the parent tag. + * + * @param The class where all instances (including child elements) belong to. If parent and + * child elements belong to different class hierarchies, it's OK to set this to {@link Object}. + */ +public abstract class SimpleInflater { + + private static final String TAG = "SimpleInflater"; + private static final boolean DEBUG = true; + + protected final Resources mResources; + + /** + * Create a new inflater instance associated with a particular Resources bundle. + * + * @param resources The Resources class used to resolve given resource IDs. + */ + protected SimpleInflater(@NonNull Resources resources) { + mResources = resources; + } + + public Resources getResources() { + return mResources; + } + + /** + * Inflate a new hierarchy from the specified XML resource. Throws InflaterException if there is + * an error. + * + * @param resId ID for an XML resource to load (e.g. R.xml.my_xml) + * @return The root of the inflated hierarchy. + */ + public T inflate(int resId) { + XmlResourceParser parser = getResources().getXml(resId); + try { + return inflate(parser); + } finally { + parser.close(); + } + } + + /** + * Inflate a new hierarchy from the specified XML node. Throws InflaterException if there is an + * error. + *

+ * Important   For performance + * reasons, inflation relies heavily on pre-processing of XML files + * that is done at build time. Therefore, it is not currently possible to + * use inflater with an XmlPullParser over a plain XML file at runtime. + * + * @param parser XML dom node containing the description of the hierarchy. + * @return The root of the inflated hierarchy. + */ + public T inflate(XmlPullParser parser) { + final AttributeSet attrs = Xml.asAttributeSet(parser); + T createdItem; + + try { + // Look for the root node. + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + // continue + } + + if (type != XmlPullParser.START_TAG) { + throw new InflateException(parser.getPositionDescription() + + ": No start tag found!"); + } + + createdItem = createItemFromTag(parser.getName(), attrs); + + rInflate(parser, createdItem, attrs); + } catch (XmlPullParserException e) { + throw new InflateException(e.getMessage(), e); + } catch (IOException e) { + throw new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e); + } + + return createdItem; + } + + /** + * This routine is responsible for creating the correct subclass of item + * given the xml element name. + * + * @param tagName The XML tag name for the item to be created. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return The item created. + */ + protected abstract T onCreateItem(String tagName, AttributeSet attrs); + + private T createItemFromTag(String name, AttributeSet attrs) { + try { + T item = onCreateItem(name, attrs); + if (DEBUG) Log.v(TAG, item + " created for <" + name + ">"); + return item; + } catch (InflateException e) { + throw e; + } catch (Exception e) { + throw new InflateException(attrs.getPositionDescription() + + ": Error inflating class " + name, e); + } + } + + /** + * Recursive method used to descend down the xml hierarchy and instantiate + * items, instantiate their children, and then call onFinishInflate(). + */ + private void rInflate(XmlPullParser parser, T parent, final AttributeSet attrs) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + + int type; + while (((type = parser.next()) != XmlPullParser.END_TAG + || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { + + if (type != XmlPullParser.START_TAG) { + continue; + } + + if (onInterceptCreateItem(parser, parent, attrs)) { + continue; + } + + String name = parser.getName(); + T item = createItemFromTag(name, attrs); + + onAddChildItem(parent, item); + + rInflate(parser, item, attrs); + } + } + + /** + * Whether item creation should be intercepted to perform custom handling on the parser rather + * than creating an object from it. This is used in rare cases where a tag doesn't correspond + * to creation of an object. + * + * The parser will be pointing to the start of a tag, you must stop parsing and return when you + * reach the end of this element. That is, this method is responsible for parsing the element + * at the given position together with all of its child tags. + * + * Note that parsing of the root tag cannot be intercepted. + * + * @param parser XML dom node containing the description of the hierarchy. + * @param parent The item that should be the parent of whatever you create. + * @param attrs An AttributeSet of attributes to apply to the item. + * @return True to continue parsing without calling {@link #onCreateItem(String, AttributeSet)}, + * or false if this inflater should proceed to create an item. + */ + protected boolean onInterceptCreateItem(XmlPullParser parser, T parent, AttributeSet attrs) + throws XmlPullParserException { + return false; + } + + protected abstract void onAddChildItem(T parent, T child); +} diff --git a/library/test/res/xml/reflection_inflater_test.xml b/library/test/res/xml/reflection_inflater_test.xml new file mode 100644 index 0000000..9343d75 --- /dev/null +++ b/library/test/res/xml/reflection_inflater_test.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/library/test/res/xml/simple_inflater_test.xml b/library/test/res/xml/simple_inflater_test.xml new file mode 100644 index 0000000..144eae7 --- /dev/null +++ b/library/test/res/xml/simple_inflater_test.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/library/test/src/com/android/setupwizardlib/test/ReflectionInflaterTest.java b/library/test/src/com/android/setupwizardlib/test/ReflectionInflaterTest.java new file mode 100644 index 0000000..aaff0f7 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ReflectionInflaterTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.ScaleAnimation; + +import com.android.setupwizardlib.items.ReflectionInflater; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ReflectionInflaterTest { + + @Test + public void testInflateXml() { + final Context context = InstrumentationRegistry.getContext(); + TestInflater inflater = new TestInflater(context); + final Animation result = inflater.inflate(R.xml.reflection_inflater_test); + + assertTrue(result instanceof AnimationSet); + final AnimationSet set = (AnimationSet) result; + final List animations = set.getAnimations(); + assertEquals(1, animations.size()); + assertTrue(animations.get(0) instanceof ScaleAnimation); + } + + private static class TestInflater extends ReflectionInflater { + + protected TestInflater(@NonNull Context context) { + super(context); + } + + @Override + protected void onAddChildItem(Animation parent, Animation child) { + final AnimationSet group = (AnimationSet) parent; + group.addAnimation(child); + } + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/SimpleInflaterTest.java b/library/test/src/com/android/setupwizardlib/test/SimpleInflaterTest.java new file mode 100644 index 0000000..bd4c251 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/SimpleInflaterTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.content.res.Resources; +import android.support.annotation.NonNull; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.AttributeSet; + +import com.android.setupwizardlib.items.SimpleInflater; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SimpleInflaterTest { + + @Test + public void testInflateXml() { + final Context context = InstrumentationRegistry.getContext(); + TestInflater inflater = new TestInflater(context.getResources()); + final StringBuilder result = inflater.inflate(R.xml.simple_inflater_test); + + assertEquals("Parent[null] > Child[foobar]", result.toString()); + } + + private static class TestInflater extends SimpleInflater { + + protected TestInflater(@NonNull Resources resources) { + super(resources); + } + + @Override + protected StringBuilder onCreateItem(String tagName, AttributeSet attrs) { + final String attribute = attrs.getAttributeValue(null, "myattribute"); + return new StringBuilder(tagName).append("[").append(attribute).append("]"); + } + + @Override + protected void onAddChildItem(StringBuilder parent, StringBuilder child) { + parent.append(" > ").append(child); + } + } +}