Merge remote-tracking branch 'goog/nyc-mr1-dev-plus-aosp' into HEAD
diff --git a/library/common-full-support.mk b/library/common-full-support.mk
index 9ff82ce..43a6433 100644
--- a/library/common-full-support.mk
+++ b/library/common-full-support.mk
@@ -9,7 +9,7 @@
 #   LOCAL_RESOURCE_DIR := \
 #        $(LOCAL_PATH)/res
 #
-#   include frameworks/opt/setupwizard/library/common-eclair-mr1.mk
+#   include frameworks/opt/setupwizard/library/common-full-support.mk
 #
 
 # Check that LOCAL_RESOURCE_DIR is defined
diff --git a/library/eclair-mr1/res/layout/suw_items_switch.xml b/library/eclair-mr1/res/layout/suw_items_switch.xml
index 14c7650..62bb479 100644
--- a/library/eclair-mr1/res/layout/suw_items_switch.xml
+++ b/library/eclair-mr1/res/layout/suw_items_switch.xml
@@ -45,7 +45,7 @@
         android:layout_weight="1"
         android:orientation="vertical">
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_title"
             style="@style/SuwItemTitle.Verbose"
             android:layout_width="match_parent"
@@ -54,7 +54,7 @@
             android:textAlignment="viewStart"
             tools:ignore="UnusedAttribute" />
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_summary"
             style="@style/SuwItemSummary"
             android:layout_width="match_parent"
diff --git a/library/eclair-mr1/res/values/styles.xml b/library/eclair-mr1/res/values/styles.xml
index fa954c6..25e7757 100644
--- a/library/eclair-mr1/res/values/styles.xml
+++ b/library/eclair-mr1/res/values/styles.xml
@@ -36,6 +36,7 @@
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
         <item name="suwCardBackground">@drawable/suw_card_bg_dark</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
         <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item>
         <item name="suwNavBarTheme">@style/SuwNavBarThemeDark</item>
@@ -59,30 +60,33 @@
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
         <item name="suwCardBackground">@drawable/suw_card_bg_light</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
         <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item>
         <item name="suwNavBarTheme">@style/SuwNavBarThemeLight</item>
         <item name="textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item>
     </style>
 
-    <!-- Placeholder for GLIF dark theme, colors are not updated yet -->
     <style name="SuwThemeGlif" parent="Theme.AppCompat.NoActionBar">
         <item name="android:indeterminateTint" tools:ignore="NewApi">?attr/colorControlActivated</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
         <item name="android:listPreferredItemHeight">@dimen/suw_items_preferred_height</item>
+        <item name="android:listPreferredItemPaddingEnd" tools:ignore="NewApi">?attr/suwMarginSides</item>
+        <item name="android:listPreferredItemPaddingStart" tools:ignore="NewApi">?attr/suwMarginSides</item>
         <item name="android:navigationBarColor" tools:ignore="NewApi">@android:color/black</item>
         <item name="android:statusBarColor" tools:ignore="NewApi">@android:color/transparent</item>
         <item name="android:textAppearanceListItemSmall" tools:ignore="NewApi">?attr/textAppearanceListItemSmall</item>
-        <item name="android:textColorLink">@color/suw_link_color_light</item>
+        <item name="android:textColorLink">@color/suw_link_color_dark</item>
         <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
-        <item name="colorAccent">@color/suw_color_accent_light</item>
-        <item name="colorPrimary">@color/suw_color_accent_light</item>
+        <item name="colorAccent">@color/suw_color_accent_glif_dark</item>
+        <item name="colorPrimary">@color/suw_color_accent_glif_dark</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
         <item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
         <item name="textAppearanceListItem">@style/TextAppearance.SuwGlifItemTitle</item>
@@ -104,10 +108,11 @@
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
-        <item name="colorAccent">@color/suw_color_accent_light</item>
-        <item name="colorPrimary">@color/suw_color_accent_light</item>
+        <item name="colorAccent">@color/suw_color_accent_glif_light</item>
+        <item name="colorPrimary">@color/suw_color_accent_glif_light</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
         <item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
         <item name="textAppearanceListItem">@style/TextAppearance.SuwGlifItemTitle</item>
diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
index aaeaf34..40e6bd0 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java
@@ -82,6 +82,7 @@
     public void onBindView(View view) {
         super.onBindView(view);
         final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch);
+        switchView.setOnCheckedChangeListener(null);
         switchView.setChecked(mChecked);
         switchView.setOnCheckedChangeListener(this);
         switchView.setEnabled(isEnabled());
@@ -93,6 +94,7 @@
 
     @Override
     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+        mChecked = isChecked;
         if (mListener != null) {
             mListener.onCheckedChange(this, isChecked);
         }
diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java b/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
index 2c53ee7..e6fa497 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
@@ -118,13 +118,11 @@
         info.setFocusable(true);
         info.setClickable(true);
         getBoundsForSpan(span, mTempRect);
-        if (!mTempRect.isEmpty()) {
-            info.setBoundsInParent(getBoundsForSpan(span, mTempRect));
-        } else {
+        if (mTempRect.isEmpty()) {
             Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
             mTempRect.set(0, 0, 1, 1);
-            info.setBoundsInParent(mTempRect);
         }
+        info.setBoundsInParent(mTempRect);
         info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
     }
 
@@ -171,22 +169,36 @@
         CharSequence text = mView.getText();
         outRect.setEmpty();
         if (text instanceof Spanned) {
-            Spanned spannedText = (Spanned) text;
-            final int spanStart = spannedText.getSpanStart(span);
-            final int spanEnd = spannedText.getSpanEnd(span);
             final Layout layout = mView.getLayout();
-            final float xStart = layout.getPrimaryHorizontal(spanStart);
-            final float xEnd = layout.getPrimaryHorizontal(spanEnd);
-            final int lineStart = layout.getLineForOffset(spanStart);
-            final int lineEnd = layout.getLineForOffset(spanEnd);
-            layout.getLineBounds(lineStart, outRect);
-            outRect.left = (int) xStart;
-            if (lineEnd == lineStart) {
-                outRect.right = (int) xEnd;
-            } // otherwise just leave it at the end of the start line
+            if (layout != null) {
+                Spanned spannedText = (Spanned) text;
+                final int spanStart = spannedText.getSpanStart(span);
+                final int spanEnd = spannedText.getSpanEnd(span);
+                final float xStart = layout.getPrimaryHorizontal(spanStart);
+                final float xEnd = layout.getPrimaryHorizontal(spanEnd);
+                final int lineStart = layout.getLineForOffset(spanStart);
+                final int lineEnd = layout.getLineForOffset(spanEnd);
+                layout.getLineBounds(lineStart, outRect);
+                if (lineEnd == lineStart) {
+                    // If the span is on a single line, adjust both the left and right bounds
+                    // so outrect is exactly bounding the span.
+                    outRect.left = (int) Math.min(xStart, xEnd);
+                    outRect.right = (int) Math.max(xStart, xEnd);
+                } else {
+                    // If the span wraps across multiple lines, only use the first line (as returned
+                    // by layout.getLineBounds above), and adjust the "start" of outrect to where
+                    // the span starts, leaving the "end" of outrect at the end of the line.
+                    // ("start" being left for LTR, and right for RTL)
+                    if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) {
+                        outRect.right = (int) xStart;
+                    } else {
+                        outRect.left = (int) xStart;
+                    }
+                }
 
-            // Offset for padding
-            outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
+                // Offset for padding
+                outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
+            }
         }
         return outRect;
     }
diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java b/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java
index 1ffc034..6e555a1 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java
@@ -44,7 +44,7 @@
             Drawable[] drawables = getCompoundDrawablesRelative();
             for (int i = 0; i < drawables.length; i++) {
                 if (drawables[i] != null) {
-                    drawables[i] = TintedDrawable.wrap(drawables[i].mutate());
+                    drawables[i] = TintedDrawable.wrap(drawables[i]);
                 }
             }
             setCompoundDrawablesRelativeWithIntrinsicBounds(drawables[0], drawables[1],
@@ -54,10 +54,10 @@
 
     @Override
     public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) {
-        if (left != null) left = TintedDrawable.wrap(left.mutate());
-        if (top != null) top = TintedDrawable.wrap(top.mutate());
-        if (right != null) right = TintedDrawable.wrap(right.mutate());
-        if (bottom != null) bottom = TintedDrawable.wrap(bottom.mutate());
+        if (left != null) left = TintedDrawable.wrap(left);
+        if (top != null) top = TintedDrawable.wrap(top);
+        if (right != null) right = TintedDrawable.wrap(right);
+        if (bottom != null) bottom = TintedDrawable.wrap(bottom);
         super.setCompoundDrawables(left, top, right, bottom);
         tintDrawables();
     }
@@ -65,10 +65,10 @@
     @Override
     public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end,
             Drawable bottom) {
-        if (start != null) start = TintedDrawable.wrap(start.mutate());
-        if (top != null) top = TintedDrawable.wrap(top.mutate());
-        if (end != null) end = TintedDrawable.wrap(end.mutate());
-        if (bottom != null) bottom = TintedDrawable.wrap(bottom.mutate());
+        if (start != null) start = TintedDrawable.wrap(start);
+        if (top != null) top = TintedDrawable.wrap(top);
+        if (end != null) end = TintedDrawable.wrap(end);
+        if (bottom != null) bottom = TintedDrawable.wrap(bottom);
         super.setCompoundDrawablesRelative(start, top, end, bottom);
         tintDrawables();
     }
@@ -114,7 +114,7 @@
             if (drawable instanceof TintedDrawable) {
                 return (TintedDrawable) drawable;
             }
-            return new TintedDrawable(drawable);
+            return new TintedDrawable(drawable.mutate());
         }
 
         private ColorStateList mTintList = null;
diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java b/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java
index d79d149..2931e27 100644
--- a/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java
+++ b/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java
@@ -22,6 +22,7 @@
 import android.text.SpannableString;
 import android.text.Spanned;
 import android.text.method.LinkMovementMethod;
+import android.text.style.ClickableSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -99,13 +100,40 @@
     private void init() {
         mAccessibilityHelper = new LinkAccessibilityHelper(this);
         ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
-        setMovementMethod(LinkMovementMethod.getInstance());
     }
 
     @Override
     public void setText(CharSequence text, BufferType type) {
         text = getRichText(getContext(), text);
+        // Set text first before doing anything else because setMovementMethod internally calls
+        // setText. This in turn ends up calling this method with mText as the first parameter
         super.setText(text, type);
+        boolean hasLinks = hasLinks(text);
+
+        if (hasLinks) {
+            // When a TextView has a movement method, it will set the view to clickable. This makes
+            // View.onTouchEvent always return true and consumes the touch event, essentially
+            // nullifying any return values of MovementMethod.onTouchEvent.
+            // To still allow propagating touch events to the parent when this view doesn't have
+            // links, we only set the movement method here if the text contains links.
+            setMovementMethod(LinkMovementMethod.getInstance());
+        } else {
+            setMovementMethod(null);
+        }
+        // ExploreByTouchHelper automatically enables focus for RichTextView
+        // even though it may not have any links. Causes problems during talkback
+        // as individual TextViews consume touch events and thereby reducing the focus window
+        // shown by Talkback. Disable focus if there are no links
+        setFocusable(hasLinks);
+    }
+
+    private boolean hasLinks(CharSequence text) {
+        if (text instanceof Spanned) {
+            final ClickableSpan[] spans =
+                    ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
+            return spans.length > 0;
+        }
+        return false;
     }
 
     @Override
diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
index 8d42fa3..76885b9 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
@@ -121,6 +121,53 @@
         info.recycle();
     }
 
+    @SmallTest
+    public void testNullLayout() {
+        // Setting the padding will cause the layout to be null-ed out.
+        mTextView.setPadding(1, 1, 1, 1);
+
+        AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+        mHelper.onPopulateNodeForVirtualView(0, info);
+
+        Rect bounds = new Rect();
+        info.getBoundsInParent(bounds);
+        assertEquals("LinkSpan bounds should be (0, 0, 1, 1)",
+                new Rect(0, 0, 1, 1), bounds);
+
+        info.recycle();
+    }
+
+    @SmallTest
+    public void testRtlLayout() {
+        // Redo setUp with a Hebrew (RTL) string.
+        mSpan = new LinkSpan("foobar");
+        SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
+        ssb.setSpan(mSpan, 1, 2, 0 /* flags */);
+
+        mTextView = new TextView(getContext());
+        mTextView.setText(ssb);
+        mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
+        mHelper = new TestLinkAccessibilityHelper(mTextView);
+
+        mTextView.measure(dp2Px(500), dp2Px(500));
+        mTextView.layout(dp2Px(0), dp2Px(0), dp2Px(500), dp2Px(500));
+        // End redo setup
+
+        AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+        mHelper.onPopulateNodeForVirtualView(1, info);
+
+        assertEquals("LinkSpan description should be \"כ\"",
+                "כ", info.getContentDescription().toString());
+        assertTrue("LinkSpan should be focusable", info.isFocusable());
+        assertTrue("LinkSpan should be clickable", info.isClickable());
+        Rect bounds = new Rect();
+        info.getBoundsInParent(bounds);
+        assertEquals("LinkSpan bounds should be (70.5dp, 0dp, 78.5dp, 20.5dp)",
+                new Rect(dp2Px(70.5f), dp2Px(0f), dp2Px(78.5f), dp2Px(20.5f)), bounds);
+
+        info.recycle();
+    }
+
     private int dp2Px(float dp) {
         if (mDisplayMetrics == null) {
             mDisplayMetrics = getContext().getResources().getDisplayMetrics();
diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java
index c591580..b5bf73e 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java
@@ -72,4 +72,43 @@
         assertTrue("The span should be a TextAppearanceSpan",
                 spans[0] instanceof TextAppearanceSpan);
     }
+
+    @SmallTest
+    public void testTextContaininingLinksAreFocusable() {
+        Annotation testLink = new Annotation("link", "value");
+        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
+        spannableStringBuilder.setSpan(testLink, 0, 3, 0);
+
+        RichTextView view = new RichTextView(getContext());
+        view.setText(spannableStringBuilder);
+
+        assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
+    }
+
+
+    @SmallTest
+    public void testTextContainingNoLinksAreNotFocusable() {
+        RichTextView textView = new RichTextView(getContext());
+        textView.setText("Thou shall not be focusable!");
+
+        assertFalse("TextView should not be focusable since it does not contain any span",
+                textView.isFocusable());
+    }
+
+
+    // Based on the text contents of the text view, the "focusable" property of the element
+    // should also be automatically changed.
+    @SmallTest
+    public void testRichTxtViewFocusChangesWithTextChange() {
+        RichTextView textView = new RichTextView(getContext());
+        textView.setText("Thou shall not be focusable!");
+
+        assertFalse(textView.isFocusable());
+
+        SpannableStringBuilder spannableStringBuilder =
+                new SpannableStringBuilder("I am focusable");
+        spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
+        textView.setText(spannableStringBuilder);
+        assertTrue(textView.isFocusable());
+    }
 }
diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java
index e7c93ba..479516f 100644
--- a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java
+++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java
@@ -87,6 +87,37 @@
     }
 
     @SmallTest
+    public void testRebind() {
+        SwitchItem item1 = new SwitchItem();
+        item1.setTitle("TestTitle1");
+        item1.setSummary("TestSummary1");
+        item1.setChecked(false);
+
+        SwitchItem item2 = new SwitchItem();
+        item2.setTitle("TestTitle2");
+        item2.setSummary("TestSummary2");
+        item2.setChecked(true);
+
+        View view = createLayout();
+
+        item1.onBindView(view);
+        item2.onBindView(view);
+
+        // Switch should be bound to item2, and therefore checked
+        assertTrue("Switch should be checked", mSwitch.isChecked());
+
+        // Switching the switch to false should change the checked state of item 2 only
+        mSwitch.setChecked(false);
+        assertFalse("Item1 should still be unchecked", item1.isChecked());
+        assertFalse("Item2 should not be checked", item2.isChecked());
+
+        // Switching the switch to true should change the checked state of item 2 only
+        mSwitch.setChecked(true);
+        assertFalse("Item1 should still be unchecked", item1.isChecked());
+        assertTrue("Item2 should be checked", item2.isChecked());
+    }
+
+    @SmallTest
     public void testListenerSetChecked() {
         // Check that calling setChecked on the item will also call the listener.
 
diff --git a/library/full-support/res/layout/suw_glif_recycler_template_card.xml b/library/full-support/res/layout/suw_glif_recycler_template_card.xml
index cf67bd9..7b5c6b0 100644
--- a/library/full-support/res/layout/suw_glif_recycler_template_card.xml
+++ b/library/full-support/res/layout/suw_glif_recycler_template_card.xml
@@ -15,21 +15,35 @@
     limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/suw_pattern_bg"
     style="@style/SuwGlifCardBackground"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:fitsSystemWindows="true"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
 
-    <FrameLayout
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+    <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
         style="@style/SuwGlifCardContainer"
         android:layout_width="@dimen/suw_glif_card_width"
-        android:layout_height="@dimen/suw_glif_card_height"
-        android:layout_centerInParent="true">
+        android:layout_height="wrap_content"
+        android:height="@dimen/suw_glif_card_height">
 
         <include layout="@layout/suw_glif_recycler_template_content" />
 
-    </FrameLayout>
+    </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
 
-</RelativeLayout>
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/full-support/res/layout/suw_glif_recycler_template_content.xml b/library/full-support/res/layout/suw_glif_recycler_template_content.xml
index d8ae7b5..13da5db 100644
--- a/library/full-support/res/layout/suw_glif_recycler_template_content.xml
+++ b/library/full-support/res/layout/suw_glif_recycler_template_content.xml
@@ -15,10 +15,24 @@
     limitations under the License.
 -->
 
-<com.android.setupwizardlib.view.HeaderRecyclerView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/suw_recycler_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    app:suwHeader="@layout/suw_glif_header" />
+    android:orientation="vertical">
+
+    <com.android.setupwizardlib.view.HeaderRecyclerView
+        android:id="@+id/suw_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:scrollbars="vertical"
+        app:suwHeader="@layout/suw_glif_header" />
+
+    <ViewStub
+        android:id="@+id/suw_layout_footer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
index 8c937a1..bbd7b50 100644
--- a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
+++ b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java
@@ -67,11 +67,25 @@
     public static final int DIVIDER_CONDITION_BOTH = 1;
 
     /**
-     * Creates a default instance of {@link DividerItemDecoration}, using
-     * {@code android:attr/listDivider} as the divider and {@code android:attr/dividerHeight} as the
-     * divider height.
+     * @deprecated Use {@link #DividerItemDecoration(android.content.Context)}
      */
+    @Deprecated
     public static DividerItemDecoration getDefault(Context context) {
+        return new DividerItemDecoration(context);
+    }
+
+    /* non-static section */
+
+    private Drawable mDivider;
+    private int mDividerHeight;
+    private int mDividerIntrinsicHeight;
+    @DividerCondition
+    private int mDividerCondition;
+
+    public DividerItemDecoration() {
+    }
+
+    public DividerItemDecoration(Context context) {
         final TypedArray a = context.obtainStyledAttributes(R.styleable.SuwDividerItemDecoration);
         final Drawable divider = a.getDrawable(
                 R.styleable.SuwDividerItemDecoration_android_listDivider);
@@ -82,21 +96,11 @@
                 DIVIDER_CONDITION_EITHER);
         a.recycle();
 
-        final DividerItemDecoration decoration = new DividerItemDecoration();
-        decoration.setDivider(divider);
-        decoration.setDividerHeight(dividerHeight);
-        decoration.setDividerCondition(dividerCondition);
-        return decoration;
+        setDivider(divider);
+        setDividerHeight(dividerHeight);
+        setDividerCondition(dividerCondition);
     }
 
-    /* non-static section */
-
-    private Drawable mDivider;
-    private int mDividerHeight;
-    private int mDividerIntrinsicHeight;
-    @DividerCondition
-    private int mDividerCondition;
-
     @Override
     public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
         if (mDivider == null) {
@@ -127,27 +131,24 @@
         final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
         final int index = holder.getLayoutPosition();
         final int lastItemIndex = parent.getAdapter().getItemCount() - 1;
-        if ((holder instanceof DividedViewHolder)) {
-            if (((DividedViewHolder) holder).isDividerAllowedBelow()) {
-                if (mDividerCondition == DIVIDER_CONDITION_EITHER) {
-                    // Draw the divider without consulting the next item if we only
-                    // need permission for either above or below.
-                    return true;
-                }
-            } else if (mDividerCondition == DIVIDER_CONDITION_BOTH || index == lastItemIndex) {
-                // Don't draw if the current view holder doesn't allow drawing below
-                // and the current theme requires permission for both the item below and above.
-                // Also, if this is the last item, there is no item below to ask permission
-                // for whether to draw a divider above, so don't draw it.
-                return false;
+        if (isDividerAllowedBelow(holder)) {
+            if (mDividerCondition == DIVIDER_CONDITION_EITHER) {
+                // Draw the divider without consulting the next item if we only
+                // need permission for either above or below.
+                return true;
             }
+        } else if (mDividerCondition == DIVIDER_CONDITION_BOTH || index == lastItemIndex) {
+            // Don't draw if the current view holder doesn't allow drawing below
+            // and the current theme requires permission for both the item below and above.
+            // Also, if this is the last item, there is no item below to ask permission
+            // for whether to draw a divider above, so don't draw it.
+            return false;
         }
         // Require permission from index below to draw the divider.
         if (index < lastItemIndex) {
             final RecyclerView.ViewHolder nextHolder =
                     parent.findViewHolderForLayoutPosition(index + 1);
-            if ((nextHolder instanceof DividedViewHolder)
-                    && !((DividedViewHolder) nextHolder).isDividerAllowedAbove()) {
+            if (!isDividerAllowedAbove(nextHolder)) {
                 // Don't draw if the next view holder doesn't allow drawing above
                 return false;
             }
@@ -156,6 +157,34 @@
     }
 
     /**
+     * Whether a divider is allowed above the view holder. The allowed values will be combined
+     * according to {@link #getDividerCondition()}. The default implementation delegates to
+     * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+     * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+     * override this to give more information to decide whether a divider should be drawn.
+     *
+     * @return True if divider is allowed above this view holder.
+     */
+    protected boolean isDividerAllowedAbove(RecyclerView.ViewHolder viewHolder) {
+        return !(viewHolder instanceof DividedViewHolder)
+                || ((DividedViewHolder) viewHolder).isDividerAllowedAbove();
+    }
+
+    /**
+     * Whether a divider is allowed below the view holder. The allowed values will be combined
+     * according to {@link #getDividerCondition()}. The default implementation delegates to
+     * {@link com.android.setupwizardlib.DividerItemDecoration.DividedViewHolder}, or simply allows
+     * the divider if the view holder doesn't implement {@code DividedViewHolder}. Subclasses can
+     * override this to give more information to decide whether a divider should be drawn.
+     *
+     * @return True if divider is allowed below this view holder.
+     */
+    protected boolean isDividerAllowedBelow(RecyclerView.ViewHolder viewHolder) {
+        return !(viewHolder instanceof DividedViewHolder)
+                || ((DividedViewHolder) viewHolder).isDividerAllowedBelow();
+    }
+
+    /**
      * Sets the drawable to be used as the divider.
      */
     public void setDivider(Drawable divider) {
diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
index 509532f..476abce 100644
--- a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
+++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -130,7 +130,7 @@
         if (mRecyclerView instanceof HeaderRecyclerView) {
             mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader();
         }
-        mDividerDecoration = DividerItemDecoration.getDefault(getContext());
+        mDividerDecoration = new DividerItemDecoration(getContext());
         mRecyclerView.addItemDecoration(mDividerDecoration);
     }
 
@@ -145,6 +145,13 @@
         return super.findViewById(id);
     }
 
+    public void setDividerItemDecoration(DividerItemDecoration decoration) {
+        mRecyclerView.removeItemDecoration(mDividerDecoration);
+        mDividerDecoration = decoration;
+        mRecyclerView.addItemDecoration(mDividerDecoration);
+        updateDivider();
+    }
+
     public RecyclerView getRecyclerView() {
         return mRecyclerView;
     }
diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
index 5159bae..ddde2ae 100644
--- a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
+++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
@@ -27,7 +27,6 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.TextView;
 
 import com.android.setupwizardlib.items.ItemGroup;
 import com.android.setupwizardlib.items.ItemInflater;
@@ -139,7 +138,7 @@
         if (mRecyclerView instanceof HeaderRecyclerView) {
             mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader();
         }
-        mDividerDecoration = DividerItemDecoration.getDefault(getContext());
+        mDividerDecoration = new DividerItemDecoration(getContext());
         mRecyclerView.addItemDecoration(mDividerDecoration);
     }
 
diff --git a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
index e0c0e46..29329b4 100644
--- a/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
+++ b/library/full-support/src/com/android/setupwizardlib/view/HeaderRecyclerView.java
@@ -25,6 +25,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
 
 import com.android.setupwizardlib.DividerItemDecoration;
 import com.android.setupwizardlib.R;
@@ -102,8 +103,21 @@
 
         @Override
         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            /*
+             * Returning the same view (mHeader) results in crash ".. but view is not a real child."
+             * The framework creates more than one instance of header because of "disappear"
+             * animations applied on the header and this necessitates creation of another headerview
+             * to use after the animation. We work around this restriction by returning an empty
+             * framelayout to which the header is attached using #onBindViewHolder method.
+             */
             if (viewType == HEADER_VIEW_TYPE) {
-                return new HeaderViewHolder(mHeader);
+                FrameLayout frameLayout = new FrameLayout(parent.getContext());
+                FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.MATCH_PARENT,
+                    FrameLayout.LayoutParams.WRAP_CONTENT
+                );
+                frameLayout.setLayoutParams(params);
+                return new HeaderViewHolder(frameLayout);
             } else {
                 return mAdapter.onCreateViewHolder(parent, viewType);
             }
@@ -115,7 +129,14 @@
             if (mHeader != null) {
                 position--;
             }
-            if (position >= 0) {
+
+            if (holder instanceof HeaderViewHolder) {
+                if (mHeader.getParent() != null) {
+                    ((ViewGroup) mHeader.getParent()).removeView(mHeader);
+                }
+                FrameLayout mHeaderParent = (FrameLayout) holder.itemView;
+                mHeaderParent.addView(mHeader);
+            } else {
                 mAdapter.onBindViewHolder(holder, position);
             }
         }
diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java
index a06f6f7..da9ceb8 100644
--- a/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java
+++ b/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java
@@ -22,7 +22,6 @@
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
-import android.graphics.Xfermode;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.support.v7.widget.LinearLayoutManager;
diff --git a/library/main/res/layout/suw_glif_blank_template_card.xml b/library/main/res/layout/suw_glif_blank_template_card.xml
index 6f1ab9d..d120ab0 100644
--- a/library/main/res/layout/suw_glif_blank_template_card.xml
+++ b/library/main/res/layout/suw_glif_blank_template_card.xml
@@ -15,18 +15,32 @@
     limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/suw_pattern_bg"
     style="@style/SuwGlifCardBackground"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:fitsSystemWindows="true"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
 
-    <FrameLayout
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+    <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
         android:id="@+id/suw_layout_content"
         style="@style/SuwGlifCardContainer"
         android:layout_width="@dimen/suw_glif_card_width"
-        android:layout_height="@dimen/suw_glif_card_height"
-        android:layout_centerInParent="true" />
+        android:layout_height="wrap_content"
+        android:height="@dimen/suw_glif_card_height" />
 
-</RelativeLayout>
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_list_template_card.xml b/library/main/res/layout/suw_glif_list_template_card.xml
index 8d7fa95..5a3ba89 100644
--- a/library/main/res/layout/suw_glif_list_template_card.xml
+++ b/library/main/res/layout/suw_glif_list_template_card.xml
@@ -15,21 +15,35 @@
     limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/suw_pattern_bg"
     style="@style/SuwGlifCardBackground"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:fitsSystemWindows="true"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
 
-    <FrameLayout
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+    <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
         style="@style/SuwGlifCardContainer"
         android:layout_width="@dimen/suw_glif_card_width"
-        android:layout_height="@dimen/suw_glif_card_height"
-        android:layout_centerInParent="true">
+        android:layout_height="wrap_content"
+        android:height="@dimen/suw_glif_card_height">
 
         <include layout="@layout/suw_glif_list_template_content" />
 
-    </FrameLayout>
+    </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
 
-</RelativeLayout>
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_list_template_content.xml b/library/main/res/layout/suw_glif_list_template_content.xml
index 3756898..3793aeb 100644
--- a/library/main/res/layout/suw_glif_list_template_content.xml
+++ b/library/main/res/layout/suw_glif_list_template_content.xml
@@ -15,10 +15,23 @@
     limitations under the License.
 -->
 
-<com.android.setupwizardlib.view.StickyHeaderListView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@android:id/list"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    app:suwHeader="@layout/suw_glif_header" />
+    android:orientation="vertical">
+
+    <com.android.setupwizardlib.view.StickyHeaderListView
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        app:suwHeader="@layout/suw_glif_header" />
+
+    <ViewStub
+        android:id="@+id/suw_layout_footer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_template_card.xml b/library/main/res/layout/suw_glif_template_card.xml
index d1d354f..7b190a1 100644
--- a/library/main/res/layout/suw_glif_template_card.xml
+++ b/library/main/res/layout/suw_glif_template_card.xml
@@ -15,22 +15,35 @@
     limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/suw_pattern_bg"
     style="@style/SuwGlifCardBackground"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:fitsSystemWindows="true"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
 
-    <FrameLayout
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
+
+    <com.android.setupwizardlib.view.IntrinsicSizeFrameLayout
         style="@style/SuwGlifCardContainer"
         android:layout_width="@dimen/suw_glif_card_width"
-        android:layout_height="@dimen/suw_glif_card_height"
-        android:layout_centerInParent="true">
+        android:layout_height="wrap_content"
+        android:height="@dimen/suw_glif_card_height">
 
         <include layout="@layout/suw_glif_template_content" />
 
-    </FrameLayout>
+    </com.android.setupwizardlib.view.IntrinsicSizeFrameLayout>
 
+    <View
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:visibility="invisible" />
 
-</RelativeLayout>
+</LinearLayout>
diff --git a/library/main/res/layout/suw_glif_template_content.xml b/library/main/res/layout/suw_glif_template_content.xml
index f867e50..0eda2ae 100644
--- a/library/main/res/layout/suw_glif_template_content.xml
+++ b/library/main/res/layout/suw_glif_template_content.xml
@@ -15,25 +15,39 @@
     limitations under the License.
 -->
 
-<ScrollView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/suw_scroll_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:fillViewport="true">
+    android:orientation="vertical">
 
-    <LinearLayout
+    <ScrollView
+        android:id="@+id/suw_scroll_view"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical">
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:fillViewport="true">
 
-        <include layout="@layout/suw_glif_header" />
-
-        <FrameLayout
-            android:id="@+id/suw_layout_content"
+        <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
 
-    </LinearLayout>
+            <include layout="@layout/suw_glif_header" />
 
-</ScrollView>
+            <FrameLayout
+                android:id="@+id/suw_layout_content"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1" />
+
+        </LinearLayout>
+
+    </ScrollView>
+
+    <ViewStub
+        android:id="@+id/suw_layout_footer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/library/main/res/layout/suw_items_default.xml b/library/main/res/layout/suw_items_default.xml
index a61b1ab..ba6b809 100644
--- a/library/main/res/layout/suw_items_default.xml
+++ b/library/main/res/layout/suw_items_default.xml
@@ -44,7 +44,7 @@
         android:gravity="center_vertical"
         android:orientation="vertical">
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_title"
             style="@style/SuwItemTitle"
             android:layout_width="match_parent"
@@ -53,7 +53,7 @@
             android:textAlignment="viewStart"
             tools:ignore="UnusedAttribute" />
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_summary"
             style="@style/SuwItemSummary"
             android:layout_width="match_parent"
diff --git a/library/main/res/layout/suw_items_description.xml b/library/main/res/layout/suw_items_description.xml
index 8712be3..4e09824 100644
--- a/library/main/res/layout/suw_items_description.xml
+++ b/library/main/res/layout/suw_items_description.xml
@@ -17,12 +17,10 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    style="@style/SuwItemContainer"
+    style="?attr/suwItemDescriptionStyle"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:orientation="horizontal"
-    android:paddingTop="@dimen/suw_description_margin_top"
-    android:paddingBottom="@dimen/suw_description_margin_bottom_lists">
+    android:orientation="horizontal">
 
     <FrameLayout
         android:id="@+id/suw_items_icon_container"
@@ -45,7 +43,7 @@
         android:layout_weight="1"
         android:orientation="vertical">
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_title"
             style="@style/SuwItemTitle"
             android:layout_width="match_parent"
@@ -56,7 +54,7 @@
             android:textAppearance="@style/TextAppearance.SuwDescription"
             tools:ignore="UnusedAttribute" />
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_summary"
             style="@style/SuwItemSummary"
             android:layout_width="match_parent"
diff --git a/library/main/res/layout/suw_items_verbose.xml b/library/main/res/layout/suw_items_verbose.xml
index 0eaa8da..3babe40 100644
--- a/library/main/res/layout/suw_items_verbose.xml
+++ b/library/main/res/layout/suw_items_verbose.xml
@@ -26,7 +26,7 @@
         android:id="@+id/suw_items_icon_container"
         android:layout_width="@dimen/suw_items_icon_container_width"
         android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
+        android:layout_gravity="top"
         android:gravity="start">
 
         <ImageView
@@ -43,7 +43,7 @@
         android:layout_weight="1"
         android:orientation="vertical">
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_title"
             style="@style/SuwItemTitle.Verbose"
             android:layout_width="match_parent"
@@ -52,7 +52,7 @@
             android:textAlignment="viewStart"
             tools:ignore="UnusedAttribute" />
 
-        <TextView
+        <com.android.setupwizardlib.view.RichTextView
             android:id="@+id/suw_items_summary"
             style="@style/SuwItemSummary"
             android:layout_width="match_parent"
diff --git a/library/main/res/values-sw360dp/dimens.xml b/library/main/res/values-sw360dp/dimens.xml
new file mode 100644
index 0000000..9ed9a82
--- /dev/null
+++ b/library/main/res/values-sw360dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 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.
+-->
+
+<resources>
+    <dimen name="suw_description_text_size">16sp</dimen>
+</resources>
diff --git a/library/main/res/values-sw600dp/dimens.xml b/library/main/res/values-sw600dp/dimens.xml
index bd05972..98c3dd7 100644
--- a/library/main/res/values-sw600dp/dimens.xml
+++ b/library/main/res/values-sw600dp/dimens.xml
@@ -22,5 +22,7 @@
 
     <!-- Illustration -->
     <item name="suw_illustration_aspect_ratio" format="float" type="dimen">0</item>
+    <dimen name="suw_header_title_size">34sp</dimen>
+    <dimen name="suw_glif_margin_sides">40dp</dimen>
 
 </resources>
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml
index 8501e94..73f8863 100644
--- a/library/main/res/values/attrs.xml
+++ b/library/main/res/values/attrs.xml
@@ -36,6 +36,7 @@
     <attr name="suwHeader" format="reference" />
     <attr name="suwHeaderText" format="string" localization="suggested" />
     <attr name="suwDividerInset" format="dimension|reference" />
+    <attr name="suwItemDescriptionStyle" format="reference" />
 
     <declare-styleable name="SuwIllustration">
         <attr name="suwAspectRatio" format="float" />
@@ -65,6 +66,11 @@
         <attr name="suwStatusBarBackground" format="color|reference" />
     </declare-styleable>
 
+    <declare-styleable name="SuwMaxSizeFrameLayout">
+        <attr name="android:height" />
+        <attr name="android:width" />
+    </declare-styleable>
+
     <declare-styleable name="SuwSetupWizardLayout">
         <attr name="suwBackground" format="color|reference" />
         <attr name="suwBackgroundTile" format="color|reference" />
diff --git a/library/main/res/values/colors.xml b/library/main/res/values/colors.xml
index a4470d1..e6ee76b 100644
--- a/library/main/res/values/colors.xml
+++ b/library/main/res/values/colors.xml
@@ -23,7 +23,7 @@
     <color name="suw_color_accent_light">#ff3367d6</color>
     <color name="suw_link_color_dark">#ff448aff</color>
     <color name="suw_link_color_light">#ff3367d6</color>
-    <color name="suw_list_item_icon_color_dark">#89ffffff</color>
+    <color name="suw_list_item_icon_color_dark">#b3ffffff</color>
     <color name="suw_list_item_icon_color_light">#89000000</color>
     <color name="suw_progress_bar_color_dark">#ffffcd40</color>
     <color name="suw_progress_bar_color_light">#fff4b400</color>
@@ -33,4 +33,8 @@
     <color name="suw_navbar_bg_dark">#ff21272b</color>
     <color name="suw_navbar_bg_light">#ffe4e7e9</color>
 
+    <!-- GLIF colors -->
+    <color name="suw_color_accent_glif_dark">#ff4285f4</color>
+    <color name="suw_color_accent_glif_light">#ff4285f4</color>
+
 </resources>
diff --git a/library/main/res/values/dimens.xml b/library/main/res/values/dimens.xml
index 55b9a45..2e89936 100644
--- a/library/main/res/values/dimens.xml
+++ b/library/main/res/values/dimens.xml
@@ -20,7 +20,7 @@
     <!-- General -->
     <dimen name="suw_layout_margin_sides">40dp</dimen>
     <dimen name="suw_glif_margin_sides">24dp</dimen>
-    <dimen name="suw_glif_margin_top">24dp</dimen>
+    <dimen name="suw_glif_margin_top">48dp</dimen>
 
     <!-- Content styles -->
     <dimen name="suw_check_box_line_spacing_extra">4sp</dimen>
@@ -39,6 +39,9 @@
     <dimen name="suw_description_line_spacing_extra">4sp</dimen>
     <dimen name="suw_description_text_size">16sp</dimen>
 
+    <dimen name="suw_description_glif_margin_top">3dp</dimen>
+    <dimen name="suw_description_glif_margin_bottom_lists">24dp</dimen>
+
     <!-- Margin on the start to offset for margin in the drawable -->
     <dimen name="suw_radio_button_margin_start">-6dp</dimen>
     <dimen name="suw_radio_button_margin_top">0dp</dimen>
@@ -79,8 +82,8 @@
     <!-- This is the extra spacing required to make the leading exactly 32sp -->
     <dimen name="suw_header_title_line_spacing_extra">3.67sp</dimen>
 
-    <dimen name="suw_glif_header_title_margin_top">24dp</dimen>
-    <dimen name="suw_glif_header_title_margin_bottom">8dp</dimen>
+    <dimen name="suw_glif_header_title_margin_top">15dp</dimen>
+    <dimen name="suw_glif_header_title_margin_bottom">2dp</dimen>
 
     <dimen name="suw_glif_icon_max_height">32dp</dimen>
 
diff --git a/library/main/res/values/styles.xml b/library/main/res/values/styles.xml
index 7c845a2..4f0e09a 100644
--- a/library/main/res/values/styles.xml
+++ b/library/main/res/values/styles.xml
@@ -44,6 +44,10 @@
         <item name="android:textAppearance">@style/TextAppearance.SuwDescription</item>
     </style>
 
+    <style name="SuwDescription.Glif" parent="SuwDescription">
+        <item name="android:layout_marginTop">@dimen/suw_description_glif_margin_top</item>
+    </style>
+
     <style name="TextAppearance.SuwDescription.Light" parent="TextAppearance.SuwDescription">
         <item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
     </style>
@@ -123,6 +127,17 @@
 
     <!-- Items styles -->
 
+    <style name="SuwItemContainer.Description" parent="SuwItemContainer">
+        <item name="android:paddingTop">@dimen/suw_description_margin_top</item>
+        <item name="android:paddingBottom">@dimen/suw_description_margin_bottom_lists</item>
+    </style>
+
+    <style name="SuwItemContainer.Description.Glif" parent="SuwItemContainer.Description">
+        <item name="android:minHeight">0dp</item>
+        <item name="android:paddingTop">@dimen/suw_description_glif_margin_top</item>
+        <item name="android:paddingBottom">@dimen/suw_description_glif_margin_bottom_lists</item>
+    </style>
+
     <style name="SuwItemContainer.Verbose" parent="SuwItemContainer">
         <item name="android:paddingBottom">@dimen/suw_items_verbose_padding_vertical</item>
         <item name="android:paddingTop">@dimen/suw_items_verbose_padding_vertical</item>
diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java
index c3202ad..765e6a6 100644
--- a/library/main/src/com/android/setupwizardlib/GlifLayout.java
+++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java
@@ -234,28 +234,55 @@
     }
 
     public void setProgressBarShown(boolean shown) {
-        final View progressBar = findManagedViewById(R.id.suw_layout_progress);
         if (shown) {
+            View progressBar = getProgressBar();
             if (progressBar != null) {
                 progressBar.setVisibility(View.VISIBLE);
-            } else {
-                final ViewStub progressBarStub =
-                        (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub);
-                if (progressBarStub != null) {
-                    progressBarStub.inflate();
-                }
             }
-            setProgressBarColor(mPrimaryColor);
         } else {
+            View progressBar = peekProgressBar();
             if (progressBar != null) {
                 progressBar.setVisibility(View.GONE);
             }
         }
     }
 
+    /**
+     * Gets the progress bar in the layout. If the progress bar has not been used before, it will be
+     * installed (i.e. inflated from its view stub).
+     *
+     * @return The progress bar of this layout. May be null only if the template used doesn't have a
+     *         progress bar built-in.
+     */
+    private ProgressBar getProgressBar() {
+        final View progressBar = peekProgressBar();
+        if (progressBar == null) {
+            final ViewStub progressBarStub =
+                    (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub);
+            if (progressBarStub != null) {
+                progressBarStub.inflate();
+            }
+            setProgressBarColor(mPrimaryColor);
+        }
+        return peekProgressBar();
+    }
+
+    /**
+     * Gets the progress bar in the layout only if it has been installed.
+     * {@link #setProgressBarShown(boolean)} should be called before this to ensure the progress bar
+     * is set up correctly.
+     *
+     * @return The progress bar of this layout, or null if the progress bar is not installed. The
+     *         null case can happen either if {@link #setProgressBarShown(boolean)} with true was
+     *         not called before this, or if the template does not contain a progress bar.
+     */
+    public ProgressBar peekProgressBar() {
+        return (ProgressBar) findManagedViewById(R.id.suw_layout_progress);
+    }
+
     private void setProgressBarColor(ColorStateList color) {
         if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
-            final ProgressBar bar = (ProgressBar) findManagedViewById(R.id.suw_layout_progress);
+            final ProgressBar bar = peekProgressBar();
             if (bar != null) {
                 bar.setIndeterminateTintList(color);
             }
diff --git a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
index 3d0efdf..6d082a1 100644
--- a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
+++ b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
@@ -23,15 +23,28 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
+import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
 import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 
 import com.android.setupwizardlib.annotations.VisibleForTesting;
 
+import java.lang.ref.SoftReference;
+
+/**
+ * This class draws the GLIF pattern used as the status bar background for phones and background for
+ * tablets in GLIF layout.
+ */
 public class GlifPatternDrawable extends Drawable {
+    /*
+     * This class essentially implements a simple SVG in Java code, with some special handling of
+     * scaling when given different bounds.
+     */
 
     /* static section */
 
@@ -40,8 +53,28 @@
 
     private static final float VIEWBOX_HEIGHT = 768f;
     private static final float VIEWBOX_WIDTH = 1366f;
-    private static final float SCALE_FOCUS_X = 200f;
-    private static final float SCALE_FOCUS_Y = 175f;
+    // X coordinate of scale focus, as a fraction of of the width. (Range is 0 - 1)
+    private static final float SCALE_FOCUS_X = .146f;
+    // Y coordinate of scale focus, as a fraction of of the height. (Range is 0 - 1)
+    private static final float SCALE_FOCUS_Y = .228f;
+
+    // Alpha component of the color to be drawn, on top of the grayscale pattern. (Range is 0 - 1)
+    private static final float COLOR_ALPHA = .8f;
+    // Int version of COLOR_ALPHA. (Range is 0 - 255)
+    private static final int COLOR_ALPHA_INT = (int) (COLOR_ALPHA * 255);
+
+    // Cap the bitmap size, such that it won't hurt the performance too much
+    // and it won't crash due to a very large scale.
+    // The drawable will look blurry above this size.
+    // This is a multiplier applied on top of the viewbox size.
+    // Resulting max cache size = (1.5 x 1366, 1.5 x 768) = (2049, 1152)
+    private static final float MAX_CACHED_BITMAP_SCALE = 1.5f;
+
+    private static final int NUM_PATHS = 7;
+
+    private static SoftReference<Bitmap> sBitmapCache;
+    private static Path[] sPatternPaths;
+    private static int[] sPatternLightness;
 
     public static GlifPatternDrawable getDefault(Context context) {
         int colorPrimary = 0;
@@ -53,136 +86,194 @@
         return new GlifPatternDrawable(colorPrimary);
     }
 
+    @VisibleForTesting
+    public static void invalidatePattern() {
+        sBitmapCache = null;
+    }
+
     /* non-static section */
 
     private int mColor;
-    private Paint mPaint;
-    private float[] mTempHsv = new float[3];
-    private Path mTempPath = new Path();
-    private Bitmap mBitmap;
+    private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private ColorFilter mColorFilter;
 
     public GlifPatternDrawable(int color) {
-        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
         setColor(color);
     }
 
     @Override
     public void draw(Canvas canvas) {
-        if (mBitmap == null) {
-            mBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(),
-                    Bitmap.Config.ARGB_8888);
-            Canvas bitmapCanvas = new Canvas(mBitmap);
-            renderOnCanvas(bitmapCanvas);
+        final Rect bounds = getBounds();
+        int drawableWidth = bounds.width();
+        int drawableHeight = bounds.height();
+        Bitmap bitmap = null;
+        if (sBitmapCache != null) {
+            bitmap = sBitmapCache.get();
         }
-        canvas.drawBitmap(mBitmap, 0, 0, null);
-    }
+        if (bitmap != null) {
+            final int bitmapWidth = bitmap.getWidth();
+            final int bitmapHeight = bitmap.getHeight();
+            // Invalidate the cache if this drawable is bigger and we can still create a bigger
+            // cache.
+            if (drawableWidth > bitmapWidth
+                    && bitmapWidth < VIEWBOX_WIDTH * MAX_CACHED_BITMAP_SCALE) {
+                bitmap = null;
+            } else if (drawableHeight > bitmapHeight
+                    && bitmapHeight < VIEWBOX_HEIGHT * MAX_CACHED_BITMAP_SCALE) {
+                bitmap = null;
+            }
+        }
 
-    private void renderOnCanvas(Canvas canvas) {
+        if (bitmap == null) {
+            // Reset the paint so it can be used to draw the paths in renderOnCanvas
+            mTempPaint.reset();
+
+            bitmap = createBitmapCache(drawableWidth, drawableHeight);
+            sBitmapCache = new SoftReference<>(bitmap);
+
+            // Reset the paint to so it can be used to draw the bitmap
+            mTempPaint.reset();
+        }
+
         canvas.save();
-        canvas.clipRect(getBounds());
+        canvas.clipRect(bounds);
 
-        Color.colorToHSV(mColor, mTempHsv);
-        scaleCanvasToBounds(canvas);
-
-        // Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
-        // values are extracted from the SVG of the pattern file.
-
-        mTempHsv[2] = 0.753f;
-        Path p = mTempPath;
-        p.reset();
-        p.moveTo(1029.4f, 357.5f);
-        p.lineTo(1366f, 759.1f);
-        p.lineTo(1366f, 0f);
-        p.lineTo(1137.7f, 0f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.78f;
-        p.reset();
-        p.moveTo(1138.1f, 0f);
-        p.rLineTo(-144.8f, 768f);
-        p.rLineTo(372.7f, 0f);
-        p.rLineTo(0f, -524f);
-        p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.804f;
-        p.reset();
-        p.moveTo(949.8f, 768f);
-        p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
-        p.lineTo(585f, 0f);
-        p.rLineTo(2.1f, 766f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.827f;
-        p.reset();
-        p.moveTo(471.1f, 768f);
-        p.rMoveTo(704.5f, 0f);
-        p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
-        p.lineTo(476.4f, 0f);
-        p.rLineTo(-5.3f, 768f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.867f;
-        p.reset();
-        p.moveTo(323.1f, 768f);
-        p.moveTo(777.5f, 768f);
-        p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
-        p.lineTo(323.1f, 768f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.894f;
-        p.reset();
-        p.moveTo(178.44286f, 766.85714f);
-        p.lineTo(308.7f, 768f);
-        p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
-        p.lineTo(0f, 0f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
-
-        mTempHsv[2] = 0.929f;
-        p.reset();
-        p.moveTo(146f, 0f);
-        p.lineTo(0f, 0f);
-        p.lineTo(0f, 768f);
-        p.lineTo(394.2f, 768f);
-        p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
-        p.close();
-        drawPath(canvas, p, mTempHsv);
+        scaleCanvasToBounds(canvas, bitmap, bounds);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
+                && canvas.isHardwareAccelerated()) {
+            mTempPaint.setColorFilter(mColorFilter);
+            canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+        } else {
+            // Software renderer doesn't work properly with ColorMatrix filter on ALPHA_8 bitmaps.
+            canvas.drawColor(Color.BLACK);
+            mTempPaint.setColor(Color.WHITE);
+            canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+            canvas.drawColor(mColor);
+        }
 
         canvas.restore();
     }
 
     @VisibleForTesting
-    public void scaleCanvasToBounds(Canvas canvas) {
-        final Rect bounds = getBounds();
-        final int height = bounds.height();
-        final int width = bounds.width();
+    public Bitmap createBitmapCache(int drawableWidth, int drawableHeight) {
+        float scaleX = drawableWidth / VIEWBOX_WIDTH;
+        float scaleY = drawableHeight / VIEWBOX_HEIGHT;
+        float scale = Math.max(scaleX, scaleY);
+        scale = Math.min(MAX_CACHED_BITMAP_SCALE, scale);
 
-        float scaleY = height / VIEWBOX_HEIGHT;
-        float scaleX = width / VIEWBOX_WIDTH;
+
+        int scaledWidth = (int) (VIEWBOX_WIDTH * scale);
+        int scaledHeight = (int) (VIEWBOX_HEIGHT * scale);
+
+        // Use ALPHA_8 mask to save memory, since the pattern is grayscale only anyway.
+        Bitmap bitmap = Bitmap.createBitmap(
+                scaledWidth,
+                scaledHeight,
+                Bitmap.Config.ALPHA_8);
+        Canvas bitmapCanvas = new Canvas(bitmap);
+        renderOnCanvas(bitmapCanvas, scale);
+        return bitmap;
+    }
+
+    private void renderOnCanvas(Canvas canvas, float scale) {
+        canvas.save();
+        canvas.scale(scale, scale);
+
+        mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+
+        // Draw the pattern by creating the paths, adjusting the colors and drawing them. The path
+        // values are extracted from the SVG of the pattern file.
+
+        if (sPatternPaths == null) {
+            sPatternPaths = new Path[NUM_PATHS];
+            // Lightness values of the pattern, range 0 - 255
+            sPatternLightness = new int[] { 10, 40, 51, 66, 91, 112, 130 };
+
+            Path p = sPatternPaths[0] = new Path();
+            p.moveTo(1029.4f, 357.5f);
+            p.lineTo(1366f, 759.1f);
+            p.lineTo(1366f, 0f);
+            p.lineTo(1137.7f, 0f);
+            p.close();
+
+            p = sPatternPaths[1] = new Path();
+            p.moveTo(1138.1f, 0f);
+            p.rLineTo(-144.8f, 768f);
+            p.rLineTo(372.7f, 0f);
+            p.rLineTo(0f, -524f);
+            p.cubicTo(1290.7f, 121.6f, 1219.2f, 41.1f, 1178.7f, 0f);
+            p.close();
+
+            p = sPatternPaths[2] = new Path();
+            p.moveTo(949.8f, 768f);
+            p.rCubicTo(92.6f, -170.6f, 213f, -440.3f, 269.4f, -768f);
+            p.lineTo(585f, 0f);
+            p.rLineTo(2.1f, 766f);
+            p.close();
+
+            p = sPatternPaths[3] = new Path();
+            p.moveTo(471.1f, 768f);
+            p.rMoveTo(704.5f, 0f);
+            p.cubicTo(1123.6f, 563.3f, 1027.4f, 275.2f, 856.2f, 0f);
+            p.lineTo(476.4f, 0f);
+            p.rLineTo(-5.3f, 768f);
+            p.close();
+
+            p = sPatternPaths[4] = new Path();
+            p.moveTo(323.1f, 768f);
+            p.moveTo(777.5f, 768f);
+            p.cubicTo(661.9f, 348.8f, 427.2f, 21.4f, 401.2f, 25.4f);
+            p.lineTo(323.1f, 768f);
+            p.close();
+
+            p = sPatternPaths[5] = new Path();
+            p.moveTo(178.44286f, 766.85714f);
+            p.lineTo(308.7f, 768f);
+            p.cubicTo(381.7f, 604.6f, 481.6f, 344.3f, 562.2f, 0f);
+            p.lineTo(0f, 0f);
+            p.close();
+
+            p = sPatternPaths[6] = new Path();
+            p.moveTo(146f, 0f);
+            p.lineTo(0f, 0f);
+            p.lineTo(0f, 768f);
+            p.lineTo(394.2f, 768f);
+            p.cubicTo(327.7f, 475.3f, 228.5f, 201f, 146f, 0f);
+            p.close();
+        }
+
+        for (int i = 0; i < NUM_PATHS; i++) {
+            // Color is 0xAARRGGBB, so alpha << 24 will create a color with (alpha)% black.
+            // Although the color components don't really matter, since the backing bitmap cache is
+            // ALPHA_8.
+            mTempPaint.setColor(sPatternLightness[i] << 24);
+            canvas.drawPath(sPatternPaths[i], mTempPaint);
+        }
+
+        canvas.restore();
+        mTempPaint.reset();
+    }
+
+    @VisibleForTesting
+    public void scaleCanvasToBounds(Canvas canvas, Bitmap bitmap, Rect drawableBounds) {
+        int bitmapWidth = bitmap.getWidth();
+        int bitmapHeight = bitmap.getHeight();
+        float scaleX = drawableBounds.width() / (float) bitmapWidth;
+        float scaleY = drawableBounds.height() / (float) bitmapHeight;
+
         // First scale both sides to fit independently.
         canvas.scale(scaleX, scaleY);
         if (scaleY > scaleX) {
-            // Adjust x-scale to maintain aspect ratio, but using (200, 0) as the pivot instead,
-            // so that more of the texture and less of the blank space on the left edge is seen.
-            canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X, 0f);
-        } else {
-            // Adjust y-scale to maintain aspect ratio, but using (0, 175) as the pivot instead,
-            // so that an intersection of two "circles" can always be seen.
-            canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y);
+            // Adjust x-scale to maintain aspect ratio using the pivot, so that more of the texture
+            // and less of the blank space on the left edge is seen.
+            canvas.scale(scaleY / scaleX, 1f, SCALE_FOCUS_X * bitmapWidth, 0f);
+        } else if (scaleX > scaleY) {
+            // Adjust y-scale to maintain aspect ratio using the pivot, so that an intersection of
+            // two "circles" can always be seen.
+            canvas.scale(1f, scaleX / scaleY, 0f, SCALE_FOCUS_Y * bitmapHeight);
         }
     }
 
-    private void drawPath(Canvas canvas, Path path, float[] hsv) {
-        mPaint.setColor(Color.HSVToColor(hsv));
-        canvas.drawPath(path, mPaint);
-    }
-
     @Override
     public void setAlpha(int i) {
         // Ignore
@@ -198,14 +289,29 @@
         return 0;
     }
 
+    /**
+     * Sets the color used as the base color of this pattern drawable. The alpha component of the
+     * color will be ignored.
+     */
     public void setColor(int color) {
-        mColor = color;
-        mPaint.setColor(color);
-        mBitmap = null;
+        final int r = Color.red(color);
+        final int g = Color.green(color);
+        final int b = Color.blue(color);
+        mColor = Color.argb(COLOR_ALPHA_INT, r, g, b);
+        mColorFilter = new ColorMatrixColorFilter(new float[] {
+                0, 0, 0, 1 - COLOR_ALPHA, r * COLOR_ALPHA,
+                0, 0, 0, 1 - COLOR_ALPHA, g * COLOR_ALPHA,
+                0, 0, 0, 1 - COLOR_ALPHA, b * COLOR_ALPHA,
+                0, 0, 0,               0,             255
+        });
         invalidateSelf();
     }
 
+    /**
+     * @return The color used as the base color of this pattern drawable. The alpha component of
+     * this is always 255.
+     */
     public int getColor() {
-        return mColor;
+        return Color.argb(255, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
     }
 }
diff --git a/library/main/src/com/android/setupwizardlib/items/ButtonItem.java b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
index 4faeff4..2ec6489 100644
--- a/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
+++ b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java
@@ -114,6 +114,13 @@
         throw new UnsupportedOperationException("Cannot bind to ButtonItem's view");
     }
 
+    /**
+     * Create a button according to this button item.
+     *
+     * @param parent The parent of the button, used to retrieve the theme and context for this
+     *               button.
+     * @return A button that can be added to the parent.
+     */
     protected Button createButton(ViewGroup parent) {
         if (mButton == null) {
             Context context = parent.getContext();
@@ -122,6 +129,12 @@
             }
             mButton = new Button(context);
             mButton.setOnClickListener(this);
+        } else {
+            if (mButton.getParent() instanceof ViewGroup) {
+                // A view cannot be added to a different parent if one already exists. Remove this
+                // button from its parent before returning.
+                ((ViewGroup) mButton.getParent()).removeView(mButton);
+            }
         }
         mButton.setEnabled(mEnabled);
         mButton.setText(mText);
diff --git a/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java b/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java
new file mode 100644
index 0000000..e9ab1a7
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/view/IntrinsicSizeFrameLayout.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 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.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.setupwizardlib.R;
+
+/**
+ * A FrameLayout subclass that has an "intrinsic size", which is the size it wants to be if that is
+ * within the constraints given by the parent. The intrinsic size can be set with the
+ * {@code android:width} and {@code android:height} attributes in XML.
+ *
+ * Note that for the intrinsic size to be meaningful, {@code android:layout_width} and/or
+ * {@code android:layout_height} will need to be {@code wrap_content}.
+ */
+public class IntrinsicSizeFrameLayout extends FrameLayout {
+
+    private int mIntrinsicHeight = 0;
+    private int mIntrinsicWidth = 0;
+
+    public IntrinsicSizeFrameLayout(Context context) {
+        super(context);
+        init(context, null, 0);
+    }
+
+    public IntrinsicSizeFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context, attrs, 0);
+    }
+
+    @TargetApi(VERSION_CODES.HONEYCOMB)
+    public IntrinsicSizeFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init(context, attrs, defStyleAttr);
+    }
+
+    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.SuwMaxSizeFrameLayout, defStyleAttr, 0);
+        mIntrinsicHeight =
+                a.getDimensionPixelSize(R.styleable.SuwMaxSizeFrameLayout_android_height, 0);
+        mIntrinsicWidth =
+                a.getDimensionPixelSize(R.styleable.SuwMaxSizeFrameLayout_android_width, 0);
+        a.recycle();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(getIntrinsicMeasureSpec(widthMeasureSpec, mIntrinsicWidth),
+                getIntrinsicMeasureSpec(heightMeasureSpec, mIntrinsicHeight));
+    }
+
+    private int getIntrinsicMeasureSpec(int measureSpec, int intrinsicSize) {
+        if (intrinsicSize <= 0) {
+            // Intrinsic size is not set, just return the original spec
+            return measureSpec;
+        }
+        final int mode = MeasureSpec.getMode(measureSpec);
+        final int size = MeasureSpec.getSize(measureSpec);
+        if (mode == MeasureSpec.UNSPECIFIED) {
+            // Parent did not give any constraint, so we'll be the intrinsic size
+            return MeasureSpec.makeMeasureSpec(mIntrinsicHeight, MeasureSpec.EXACTLY);
+        } else if (mode == MeasureSpec.AT_MOST) {
+            // If intrinsic size is within parents constraint, take the intrinsic size.
+            // Otherwise take the parents size because that's closest to the intrinsic size.
+            return MeasureSpec.makeMeasureSpec(Math.min(size, mIntrinsicHeight),
+                    MeasureSpec.EXACTLY);
+        }
+        // Parent specified EXACTLY, or in all other cases, just return the original spec
+        return measureSpec;
+    }
+}
diff --git a/library/platform/res/values-v21/styles.xml b/library/platform/res/values-v21/styles.xml
index 4a5ae8d..36b3d57 100644
--- a/library/platform/res/values-v21/styles.xml
+++ b/library/platform/res/values-v21/styles.xml
@@ -41,6 +41,7 @@
         <item name="android:windowSoftInputMode">adjustResize</item>
 
         <item name="suwCardBackground">@drawable/suw_card_bg</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
         <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item>
         <item name="suwNavBarTheme">@style/SuwNavBarThemeDark</item>
@@ -63,6 +64,7 @@
         <item name="android:windowSoftInputMode">adjustResize</item>
 
         <item name="suwCardBackground">@drawable/suw_card_bg</item>
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
         <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item>
         <item name="suwNavBarTheme">@style/SuwNavBarThemeLight</item>
@@ -70,8 +72,8 @@
 
     <!-- Placeholder for GLIF dark theme, colors are not updated yet -->
     <style name="SuwThemeGlif" parent="android:Theme.Material.NoActionBar">
-        <item name="android:colorAccent">@color/suw_color_accent_light</item>
-        <item name="android:colorPrimary">@color/suw_color_accent_light</item>
+        <item name="android:colorAccent">@color/suw_color_accent_glif_dark</item>
+        <item name="android:colorPrimary">@color/suw_color_accent_glif_light</item>
         <item name="android:indeterminateTint">?android:attr/colorPrimary</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode">src_in</item>
@@ -87,13 +89,14 @@
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
         <item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
     </style>
 
     <style name="SuwThemeGlif.Light" parent="android:Theme.Material.Light.NoActionBar">
-        <item name="android:colorAccent">@color/suw_color_accent_light</item>
-        <item name="android:colorPrimary">@color/suw_color_accent_light</item>
+        <item name="android:colorAccent">@color/suw_color_accent_glif_dark</item>
+        <item name="android:colorPrimary">@color/suw_color_accent_glif_light</item>
         <item name="android:indeterminateTint">?android:attr/colorPrimary</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode">src_in</item>
@@ -109,6 +112,7 @@
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
+        <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
         <item name="suwMarginSides">@dimen/suw_glif_margin_sides</item>
     </style>
diff --git a/library/proguard.flags b/library/proguard.flags
new file mode 100644
index 0000000..c3a49d5
--- /dev/null
+++ b/library/proguard.flags
@@ -0,0 +1,8 @@
+#########################################
+## Keep rules
+#########################################
+
+# Allow inflating ItemHierarchies from XML
+-keep public class * extends com.android.setupwizardlib.items.ItemHierarchy {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
diff --git a/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
index 45342d0..3490d4d 100644
--- a/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java
@@ -21,6 +21,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
 import com.android.setupwizardlib.R;
@@ -66,6 +67,18 @@
         assertTrue("Default button text should be empty", TextUtils.isEmpty(button.getText()));
     }
 
+    public void testCreateButtonTwice() {
+        TestButtonItem item = new TestButtonItem();
+        final Button button = item.createButton(mParent);
+
+        FrameLayout frameLayout = new FrameLayout(getContext());
+        frameLayout.addView(button);
+
+        final Button button2 = item.createButton(mParent);
+        assertSame("createButton should be reused", button, button2);
+        assertNull("Should be removed from parent after createButton", button2.getParent());
+    }
+
     public void testSetEnabledTrue() {
         TestButtonItem item = new TestButtonItem();
         item.setEnabled(true);
diff --git a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
index 1ae620d..8100eda 100644
--- a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java
@@ -114,6 +114,27 @@
         }
     }
 
+    @SmallTest
+    public void testPeekProgressBarNull() {
+        GlifLayout layout = new GlifLayout(mContext);
+        assertNull("PeekProgressBar should return null initially", layout.peekProgressBar());
+    }
+
+    @SmallTest
+    public void testPeekProgressBar() {
+        GlifLayout layout = new GlifLayout(mContext);
+        layout.setProgressBarShown(true);
+        assertNotNull("Peek progress bar should return the bar after setProgressBarShown(true)",
+                layout.peekProgressBar());
+    }
+
+    @SmallTest
+    public void testSetProgressBarShownInvalid() {
+        GlifLayout layout = new GlifLayout(mContext, R.layout.test_template);
+        layout.setProgressBarShown(true);
+        // This is a no-op because there is no progress bar stub
+    }
+
     private void assertDefaultTemplateInflated(GlifLayout layout) {
         View title = layout.findViewById(R.id.suw_layout_title);
         assertNotNull("@id/suw_layout_title should not be null", title);
diff --git a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
index 44aff73..19b4144 100644
--- a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
+++ b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
@@ -21,13 +21,25 @@
 import android.graphics.Color;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.os.Debug;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
 
 import com.android.setupwizardlib.GlifPatternDrawable;
 
+import junit.framework.AssertionFailedError;
+
 public class GlifPatternDrawableTest extends AndroidTestCase {
 
+    private static final String TAG = "GlifPatternDrawableTest";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        GlifPatternDrawable.invalidatePattern();
+    }
+
     @SmallTest
     public void testDraw() {
         final Bitmap bitmap = Bitmap.createBitmap(1366, 768, Bitmap.Config.ARGB_8888);
@@ -37,9 +49,9 @@
         drawable.setBounds(0, 0, 1366, 768);
         drawable.draw(canvas);
 
-        assertEquals("Top left pixel should be #ed0000", 0xffed0000, bitmap.getPixel(0, 0));
-        assertEquals("Center pixel should be #d30000", 0xffd30000, bitmap.getPixel(683, 384));
-        assertEquals("Bottom right pixel should be #c70000", 0xffc70000,
+        assertSameColor("Top left pixel should be #e61a1a", 0xffe61a1a, bitmap.getPixel(0, 0));
+        assertSameColor("Center pixel should be #d90d0d", 0xffd90d0d, bitmap.getPixel(683, 384));
+        assertSameColor("Bottom right pixel should be #d40808", 0xffd40808,
                 bitmap.getPixel(1365, 767));
     }
 
@@ -61,9 +73,9 @@
 
         drawable.draw(canvas);
 
-        assertEquals("Top left pixel should be #ed0000", 0xffed0000, bitmap.getPixel(0, 0));
-        assertEquals("Center pixel should be #d30000", 0xffd30000, bitmap.getPixel(683, 384));
-        assertEquals("Bottom right pixel should be #c70000", 0xffc70000,
+        assertSameColor("Top left pixel should be #e61a1a", 0xffe61a1a, bitmap.getPixel(0, 0));
+        assertSameColor("Center pixel should be #d90d0d", 0xffd90d0d, bitmap.getPixel(683, 384));
+        assertSameColor("Bottom right pixel should be #d40808", 0xffd40808,
                 bitmap.getPixel(1365, 767));
     }
 
@@ -72,9 +84,11 @@
         final Canvas canvas = new Canvas();
         Matrix expected = new Matrix(canvas.getMatrix());
 
+        Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
         final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
         drawable.setBounds(0, 0, 683, 384);  // half each side of the view box
-        drawable.scaleCanvasToBounds(canvas);
+        drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
 
         expected.postScale(0.5f, 0.5f);
 
@@ -86,12 +100,14 @@
         final Canvas canvas = new Canvas();
         final Matrix expected = new Matrix(canvas.getMatrix());
 
+        Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
         final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
         drawable.setBounds(0, 0, 683, 768);  // half the width only
-        drawable.scaleCanvasToBounds(canvas);
+        drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
 
         expected.postScale(1f, 1f);
-        expected.postTranslate(-100f, 0f);
+        expected.postTranslate(-99.718f, 0f);
 
         assertEquals("Matrices should match", expected, canvas.getMatrix());
     }
@@ -101,13 +117,57 @@
         final Canvas canvas = new Canvas();
         final Matrix expected = new Matrix(canvas.getMatrix());
 
+        Bitmap mockBitmapCache = Bitmap.createBitmap(1366, 768, Bitmap.Config.ALPHA_8);
+
         final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
         drawable.setBounds(0, 0, 1366, 384);  // half the height only
-        drawable.scaleCanvasToBounds(canvas);
+        drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
 
         expected.postScale(1f, 1f);
-        expected.postTranslate(0f, -87.5f);
+        expected.postTranslate(0f, -87.552f);
 
         assertEquals("Matrices should match", expected, canvas.getMatrix());
     }
+
+    @SmallTest
+    public void testScaleToCanvasMaxSize() {
+        final Canvas canvas = new Canvas();
+        final Matrix expected = new Matrix(canvas.getMatrix());
+
+        Bitmap mockBitmapCache = Bitmap.createBitmap(2049, 1152, Bitmap.Config.ALPHA_8);
+
+        final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+        drawable.setBounds(0, 0, 1366, 768);  // original viewbox size
+        drawable.scaleCanvasToBounds(canvas, mockBitmapCache, drawable.getBounds());
+
+        expected.postScale(1 / 1.5f, 1 / 1.5f);
+        expected.postTranslate(0f, 0f);
+
+        assertEquals("Matrices should match", expected, canvas.getMatrix());
+    }
+
+    @SmallTest
+    public void testMemoryAllocation() {
+        Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo();
+        Debug.getMemoryInfo(memoryInfo);
+        final long memoryBefore = memoryInfo.getTotalPss();  // Get memory usage in KB
+
+        final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED);
+        drawable.setBounds(0, 0, 1366, 768);
+        drawable.createBitmapCache(2049, 1152);
+
+        Debug.getMemoryInfo(memoryInfo);
+        final long memoryAfter = memoryInfo.getTotalPss();
+        Log.i(TAG, "Memory allocated for bitmap cache: " + (memoryAfter - memoryBefore));
+        assertTrue("Memory allocation should not exceed 5MB", memoryAfter < memoryBefore + 5000);
+    }
+
+    private void assertSameColor(String message, int expected, int actual) {
+        try {
+            assertEquals(expected, actual);
+        } catch (AssertionFailedError e) {
+            throw new AssertionFailedError(message + " expected <#" + Integer.toHexString(expected)
+                    + "> but found <#" + Integer.toHexString(actual) + "> instead");
+        }
+    }
 }