summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
authorMaurice Lam <yukl@google.com>2015-10-05 19:06:12 -0700
committerMaurice Lam <yukl@google.com>2015-10-06 20:35:32 -0700
commit5bf291fde3dfd64f264d525534730514a279c8fc (patch)
tree9dd79e2e05eb4820590d8fc5f481b5e94dc51f91 /library
parent48c0aa4ca75887986ba3139b7ef07ed5cc078610 (diff)
downloadsetupwizard-5bf291fde3dfd64f264d525534730514a279c8fc.tar.gz
[SuwLib] Implement Items framework
Items framework is a framework modeled after preferences, which uses XML to inflate a collection of items. Instead of using activity or fragment directly like preferences, a ListAdapter is created and you can use that with any existing ListViews. SetupWizardItemsLayout is a convenient wrapper around SetupWizardListLayout which will automatically inflates android:entries in its attributes to populate the list. Note: A recycler view adapter is under consideration Change-Id: I5eb8853c160cf86fa5b6f21a01dfa4b0030643f6
Diffstat (limited to 'library')
-rw-r--r--library/eclair-mr1/res/layout/suw_items_text.xml48
-rw-r--r--library/eclair-mr1/res/values/styles.xml6
-rw-r--r--library/main/res/values/attrs.xml13
-rw-r--r--library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java49
-rw-r--r--library/main/src/com/android/setupwizardlib/items/GenericInflater.java497
-rw-r--r--library/main/src/com/android/setupwizardlib/items/Item.java88
-rw-r--r--library/main/src/com/android/setupwizardlib/items/ItemAdapter.java112
-rw-r--r--library/main/src/com/android/setupwizardlib/items/ItemGroup.java40
-rw-r--r--library/main/src/com/android/setupwizardlib/items/ItemInflater.java59
-rw-r--r--library/platform/res/layout-v21/suw_items_text.xml41
-rw-r--r--library/platform/res/values-v21/styles.xml6
-rw-r--r--library/test/res/xml/test_items.xml29
-rw-r--r--library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java91
-rw-r--r--library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java40
-rw-r--r--library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java48
-rw-r--r--library/test/src/com/android/setupwizardlib/test/ItemTest.java76
16 files changed, 1243 insertions, 0 deletions
diff --git a/library/eclair-mr1/res/layout/suw_items_text.xml b/library/eclair-mr1/res/layout/suw_items_text.xml
new file mode 100644
index 0000000..89a4894
--- /dev/null
+++ b/library/eclair-mr1/res/layout/suw_items_text.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (c) 2015 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minHeight="?attr/listPreferredItemHeight"
+ android:orientation="vertical"
+ android:paddingEnd="?attr/listPreferredItemPaddingRight"
+ android:paddingLeft="?attr/listPreferredItemPaddingLeft"
+ android:paddingRight="?attr/listPreferredItemPaddingRight"
+ android:paddingStart="?attr/listPreferredItemPaddingLeft">
+
+ <TextView
+ android:id="@+id/suw_items_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:textAlignment="viewStart"
+ android:textAppearance="?attr/textAppearanceListItem"
+ tools:ignore="UnusedAttribute" />
+
+ <TextView
+ android:id="@+id/suw_items_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start"
+ android:textAlignment="viewStart"
+ android:textAppearance="?attr/textAppearanceListItemSmall"
+ tools:ignore="UnusedAttribute" />
+
+</LinearLayout>
diff --git a/library/eclair-mr1/res/values/styles.xml b/library/eclair-mr1/res/values/styles.xml
index 27116ea..2c89807 100644
--- a/library/eclair-mr1/res/values/styles.xml
+++ b/library/eclair-mr1/res/values/styles.xml
@@ -30,8 +30,11 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="colorAccent">@color/suw_color_accent_dark</item>
+ <item name="listPreferredItemPaddingLeft">@dimen/suw_layout_margin_sides</item>
+ <item name="listPreferredItemPaddingRight">@dimen/suw_layout_margin_sides</item>
<item name="suwCardBackground">@drawable/suw_card_bg_dark</item>
<item name="suwNavBarTheme">@style/SuwNavBarThemeDark</item>
+ <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item>
</style>
<style name="SuwThemeMaterial.Light" parent="Theme.AppCompat.Light.NoActionBar">
@@ -45,8 +48,11 @@
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="colorAccent">@color/suw_color_accent_light</item>
+ <item name="listPreferredItemPaddingLeft">@dimen/suw_layout_margin_sides</item>
+ <item name="listPreferredItemPaddingRight">@dimen/suw_layout_margin_sides</item>
<item name="suwCardBackground">@drawable/suw_card_bg_light</item>
<item name="suwNavBarTheme">@style/SuwNavBarThemeLight</item>
+ <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item>
</style>
<!-- Content styles -->
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml
index 6d43280..62fa7e7 100644
--- a/library/main/res/values/attrs.xml
+++ b/library/main/res/values/attrs.xml
@@ -49,4 +49,17 @@
<attr name="suwContainer" format="reference" />
</declare-styleable>
+ <declare-styleable name="SuwSetupWizardItemsLayout">
+ <attr name="android:entries" />
+ </declare-styleable>
+
+ <declare-styleable name="SuwItem">
+ <attr name="android:enabled" />
+ <attr name="android:icon" />
+ <attr name="android:id" />
+ <attr name="android:title" />
+ <attr name="android:layout" />
+ <attr name="android:summary" />
+ </declare-styleable>
+
</resources>
diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java
new file mode 100644
index 0000000..fbf6618
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/SetupWizardItemsLayout.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import com.android.setupwizardlib.items.ItemAdapter;
+import com.android.setupwizardlib.items.ItemGroup;
+import com.android.setupwizardlib.items.ItemInflater;
+
+public class SetupWizardItemsLayout extends SetupWizardListLayout {
+
+ public SetupWizardItemsLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs, 0);
+ }
+
+ public SetupWizardItemsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwSetupWizardItemsLayout,
+ defStyleAttr, 0);
+ int xml = a.getResourceId(R.styleable.SuwSetupWizardItemsLayout_android_entries, 0);
+ if (xml != 0) {
+ ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml);
+ setAdapter(ItemAdapter.create(inflated));
+ }
+ a.recycle();
+ }
+}
diff --git a/library/main/src/com/android/setupwizardlib/items/GenericInflater.java b/library/main/src/com/android/setupwizardlib/items/GenericInflater.java
new file mode 100644
index 0000000..af4ffcc
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/items/GenericInflater.java
@@ -0,0 +1,497 @@
+/*
+ * 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 <T> Type of the items to inflate
+ *
+ * Modified from android.preference.GenericInflater
+ */
+public abstract class GenericInflater<T> {
+
+ 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<T> mFactory;
+
+ private final Object[] mConstructorArgs = new Object[2];
+
+ private static final Class[] mConstructorSignature = new Class[] {
+ Context.class, AttributeSet.class};
+
+ private static final HashMap<String, Constructor<?>> sConstructorMap = new HashMap<>();
+
+ private String mDefaultPackage;
+
+ public interface Factory<T> {
+ /**
+ * 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.
+ * <p>
+ * 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<T> implements Factory<T> {
+ private final Factory<T> mF1, mF2;
+
+ FactoryMerger(Factory<T> f1, Factory<T> 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<T> 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<T> 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.
+ * <p>
+ * 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<T> 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.,
+ * <code>R.layout.main_page</code>)
+ * @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. *
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;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.,
+ * <code>R.layout.main_page</code>)
+ * @param root Optional root to be the parent of the generated hierarchy (if
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> 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.
+ * <p>
+ * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;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
+ * <em>attachToRoot</em> is true), or else simply an object that
+ * provides a set of values for root of the returned
+ * hierarchy (if <em>attachToRoot</em> 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 <var>name</var> found in this
+ * inflater's ClassLoader.
+ *
+ * <p>
+ * 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/Item.java b/library/main/src/com/android/setupwizardlib/items/Item.java
new file mode 100644
index 0000000..9c6832a
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/items/Item.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 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.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.setupwizardlib.R;
+
+/**
+ * Definition of an item in SetupWizardItemsLayout. An item is usually defined in XML and inflated
+ * using {@link ItemInflater}.
+ */
+public class Item {
+
+ private boolean mEnabled;
+ private Drawable mIcon;
+ private int mId;
+ private int mLayoutRes;
+ private CharSequence mSummary;
+ private CharSequence mTitle;
+
+ public Item(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwItem);
+ mEnabled = a.getBoolean(R.styleable.SuwItem_android_enabled, true);
+ mIcon = a.getDrawable(R.styleable.SuwItem_android_icon);
+ mId = a.getResourceId(R.styleable.SuwItem_android_id, 0);
+ mTitle = a.getText(R.styleable.SuwItem_android_title);
+ mSummary = a.getText(R.styleable.SuwItem_android_summary);
+ mLayoutRes = a.getResourceId(R.styleable.SuwItem_android_layout,
+ getDefaultLayoutResource());
+ a.recycle();
+ }
+
+ protected int getDefaultLayoutResource() {
+ return R.layout.suw_items_text;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public int getLayoutResource() {
+ return mLayoutRes;
+ }
+
+ public CharSequence getSummary() {
+ return mSummary;
+ }
+
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ public void onBindView(View view) {
+ // TODO: Show icon if defined
+ TextView label = (TextView) view.findViewById(R.id.suw_items_title);
+ label.setText(getTitle());
+ TextView summary = (TextView) view.findViewById(R.id.suw_items_summary);
+ summary.setText(getSummary());
+ }
+}
diff --git a/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java b/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java
new file mode 100644
index 0000000..3fad091
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/items/ItemAdapter.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 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.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+/**
+ * An adapter typically used with ListView to display a list of Items. The list of items used to
+ * create this adapter can be inflated by {@link ItemInflater} from XML.
+ */
+public class ItemAdapter extends BaseAdapter {
+
+ private final Item[] mItems;
+ private ViewTypes mViewTypes = new ViewTypes();
+
+ public static ItemAdapter create(ItemGroup items) {
+ return new ItemAdapter(items.getChildren());
+ }
+
+ public ItemAdapter(Item[] items) {
+ mItems = items;
+ refreshViewTypes();
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.length;
+ }
+
+ @Override
+ public Item getItem(int position) {
+ return mItems[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int layoutRes = getItem(position).getLayoutResource();
+ return mViewTypes.get(layoutRes);
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return mViewTypes.size();
+ }
+
+ public void refreshViewTypes() {
+ for (Item item : mItems) {
+ mViewTypes.add(item.getLayoutResource());
+ }
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Item item = getItem(position);
+ if (convertView == null) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ convertView = inflater.inflate(item.getLayoutResource(), parent, false);
+ }
+ item.onBindView(convertView);
+ return convertView;
+ }
+
+ /**
+ * A helper class to pack a sparse set of integers (e.g. resource IDs) to a contiguous list of
+ * integers (e.g. adapter positions), providing mapping to retrieve the original ID from a given
+ * position. This is used to pack the view types of the adapter into contiguous integers from
+ * a given layout resource.
+ */
+ private static class ViewTypes {
+ private SparseIntArray mPositionMap = new SparseIntArray();
+ private int nextPosition = 0;
+
+ public int add(int id) {
+ if (mPositionMap.indexOfKey(id) < 0) {
+ mPositionMap.put(id, nextPosition);
+ nextPosition++;
+ }
+ return mPositionMap.get(id);
+ }
+
+ public int size() {
+ return mPositionMap.size();
+ }
+
+ public int get(int id) {
+ return mPositionMap.get(id);
+ }
+ }
+}
diff --git a/library/main/src/com/android/setupwizardlib/items/ItemGroup.java b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java
new file mode 100644
index 0000000..3752e68
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 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.util.AttributeSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ItemGroup extends Item {
+
+ private List<Item> mItems = new ArrayList<>();
+
+ public ItemGroup(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void addChild(Item child) {
+ mItems.add(child);
+ }
+
+ public Item[] getChildren() {
+ return mItems.toArray(new Item[mItems.size()]);
+ }
+}
diff --git a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java
new file mode 100644
index 0000000..e25e2ad
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 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;
+
+/**
+ * Inflate {@link Item} hierarchies from XML files.
+ *
+ * Modified from android.support.v7.preference.PreferenceInflater
+ */
+public class ItemInflater extends GenericInflater<Item> {
+
+ private static final String TAG = "ItemInflater";
+
+ 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.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ protected void onAddChildItem(Item parent, Item child) {
+ if (parent instanceof ItemGroup) {
+ ((ItemGroup) parent).addChild(child);
+ } else {
+ throw new IllegalArgumentException("Cannot add child item to " + parent);
+ }
+ }
+}
diff --git a/library/platform/res/layout-v21/suw_items_text.xml b/library/platform/res/layout-v21/suw_items_text.xml
new file mode 100644
index 0000000..cda5487
--- /dev/null
+++ b/library/platform/res/layout-v21/suw_items_text.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (c) 2015 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="vertical"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <TextView
+ android:id="@+id/suw_items_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItem" />
+
+ <TextView
+ android:id="@+id/suw_items_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall" />
+
+</LinearLayout>
diff --git a/library/platform/res/values-v21/styles.xml b/library/platform/res/values-v21/styles.xml
index c6c5f12..1acc47a 100644
--- a/library/platform/res/values-v21/styles.xml
+++ b/library/platform/res/values-v21/styles.xml
@@ -29,8 +29,11 @@
<item name="android:indeterminateTint">@color/suw_progress_bar_color_dark</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
+ <item name="android:listPreferredItemPaddingEnd">@dimen/suw_layout_margin_sides</item>
+ <item name="android:listPreferredItemPaddingStart">@dimen/suw_layout_margin_sides</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
+ <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textColorLink">@color/suw_link_color_dark</item>
<item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
<item name="android:windowSoftInputMode">adjustResize</item>
@@ -44,8 +47,11 @@
<item name="android:indeterminateTint">@color/suw_progress_bar_color_light</item>
<!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
<item name="android:indeterminateTintMode">src_in</item>
+ <item name="android:listPreferredItemPaddingEnd">@dimen/suw_layout_margin_sides</item>
+ <item name="android:listPreferredItemPaddingStart">@dimen/suw_layout_margin_sides</item>
<item name="android:navigationBarColor">@android:color/black</item>
<item name="android:statusBarColor">@android:color/black</item>
+ <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
<item name="android:textColorLink">@color/suw_link_color_light</item>
<item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
<item name="android:windowSoftInputMode">adjustResize</item>
diff --git a/library/test/res/xml/test_items.xml b/library/test/res/xml/test_items.xml
new file mode 100644
index 0000000..3906222
--- /dev/null
+++ b/library/test/res/xml/test_items.xml
@@ -0,0 +1,29 @@
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<ItemGroup xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <Item
+ android:id="@+id/test_item_1"
+ android:title="Title1"
+ android:summary="Summary1" />
+
+ <Item
+ android:id="@+id/test_item_2"
+ android:title="Title2"
+ android:summary="Summary2" />
+
+</ItemGroup>
diff --git a/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java b/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java
new file mode 100644
index 0000000..1ba677c
--- /dev/null
+++ b/library/test/src/com/android/setupwizardlib/test/ItemAdapterTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.setupwizardlib.R;
+import com.android.setupwizardlib.items.Item;
+import com.android.setupwizardlib.items.ItemAdapter;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+public class ItemAdapterTest extends AndroidTestCase {
+
+ private Item[] mItems;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mItems = new Item[]{
+ new TestItem(mContext, 1),
+ new TestItem(mContext, 2),
+ new TestItem(mContext, 3)
+ };
+ }
+
+ @SmallTest
+ public void testAdapter() {
+ ItemAdapter adapter = new ItemAdapter(mItems);
+ assertEquals("Adapter should have 3 items", 3, adapter.getCount());
+ assertEquals("Adapter should return the first item", mItems[0], adapter.getItem(0));
+ assertEquals("ID should be same as position", 2, adapter.getItemId(2));
+
+ // Each test item has its own layout resource, and therefore its own view type
+ assertEquals("Should have 3 different view types", 3, adapter.getViewTypeCount());
+ HashSet<Integer> viewTypes = new HashSet<>(3);
+ viewTypes.add(adapter.getItemViewType(0));
+ viewTypes.add(adapter.getItemViewType(1));
+ viewTypes.add(adapter.getItemViewType(2));
+
+ assertEquals("View types should be 0, 1, 2", new HashSet<>(Arrays.asList(0, 1, 2)), viewTypes);
+ }
+
+ private static class TestItem extends Item {
+
+ private int mNum;
+
+ public TestItem(Context context, int num) {
+ super(context, null);
+ mNum = num;
+ }
+
+ @Override
+ public int getLayoutResource() {
+ return mNum * 10;
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return "TestTitle" + mNum;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return "TestSummary" + mNum;
+ }
+ }
+}
diff --git a/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java b/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java
new file mode 100644
index 0000000..272b840
--- /dev/null
+++ b/library/test/src/com/android/setupwizardlib/test/ItemGroupTest.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 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 android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.setupwizardlib.items.Item;
+import com.android.setupwizardlib.items.ItemGroup;
+
+import java.util.Arrays;
+
+public class ItemGroupTest extends AndroidTestCase {
+
+ @SmallTest
+ public void testOnBindView() {
+ ItemGroup itemGroup = new ItemGroup(mContext, null);
+ Item child1 = new Item(mContext, null);
+ Item child2 = new Item(mContext, null);
+ itemGroup.addChild(child1);
+ itemGroup.addChild(child2);
+
+ assertEquals("Item group should contain children in the order they were added",
+ Arrays.asList(child1, child2), Arrays.asList(itemGroup.getChildren()));
+ }
+}
diff --git a/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java b/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java
new file mode 100644
index 0000000..faf191e
--- /dev/null
+++ b/library/test/src/com/android/setupwizardlib/test/ItemInflaterTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 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 android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.setupwizardlib.items.Item;
+import com.android.setupwizardlib.items.ItemGroup;
+import com.android.setupwizardlib.items.ItemInflater;
+
+public class ItemInflaterTest extends InstrumentationTestCase {
+
+ @SmallTest
+ public void testDefaultPackage() {
+ ItemInflater inflater = new ItemInflater(getInstrumentation().getContext());
+ assertEquals("Default package should be the one containing Item class",
+ "com.android.setupwizardlib.items.", inflater.getDefaultPackage());
+ }
+
+ @SmallTest
+ public void testInflate() {
+ ItemInflater inflater = new ItemInflater(getInstrumentation().getContext());
+ Item item = inflater.inflate(R.xml.test_items);
+ assertTrue("Inflated item should be ItemGroup", item instanceof ItemGroup);
+ ItemGroup itemGroup = (ItemGroup) item;
+ Item[] children = itemGroup.getChildren();
+ assertEquals("Title of first child should be Title1", "Title1", children[0].getTitle());
+ assertEquals("ID of second child should be test_item_2", R.id.test_item_2,
+ children[1].getId());
+ assertEquals("Summary of second child should be Summary2", "Summary2",
+ children[1].getSummary());
+ }
+}
diff --git a/library/test/src/com/android/setupwizardlib/test/ItemTest.java b/library/test/src/com/android/setupwizardlib/test/ItemTest.java
new file mode 100644
index 0000000..e6bf94e
--- /dev/null
+++ b/library/test/src/com/android/setupwizardlib/test/ItemTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2015 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 android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.setupwizardlib.R;
+import com.android.setupwizardlib.items.Item;
+
+public class ItemTest extends AndroidTestCase {
+
+ private TextView mTitleView;
+ private TextView mSummaryView;
+
+ @SmallTest
+ public void testOnBindView() {
+ Item item = new TestItem(mContext);
+ View view = createLayout();
+
+ item.onBindView(view);
+
+ assertEquals("Title should be \"TestTitle\"", "TestTitle", mTitleView.getText().toString());
+ assertEquals("Summary should be \"TestSummary\"", "TestSummary",
+ mSummaryView.getText().toString());
+ }
+
+ private ViewGroup createLayout() {
+ ViewGroup root = new FrameLayout(mContext);
+
+ mTitleView = new TextView(mContext);
+ mTitleView.setId(R.id.suw_items_title);
+ root.addView(mTitleView);
+ mSummaryView = new TextView(mContext);
+ mSummaryView.setId(R.id.suw_items_summary);
+ root.addView(mSummaryView);
+
+ return root;
+ }
+
+ private static class TestItem extends Item {
+
+ public TestItem(Context context) {
+ super(context, null);
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return "TestTitle";
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return "TestSummary";
+ }
+ }
+}