[automerger skipped] Get Support Library resources from prebuilts am: 9420375022
am: 7774172763  -s ours

Change-Id: I7f101e980b78dd7cae894e182cd1bf2d02b9b741
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..5245206
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,4 @@
+russellbrenner@google.com
+ajayns@google.com
+iofir@google.com
+yukl@google.com
diff --git a/library/Android.mk b/library/Android.mk
index b0fa9f0..ff92194 100644
--- a/library/Android.mk
+++ b/library/Android.mk
@@ -26,11 +26,9 @@
 
 include $(CLEAR_VARS)
 
-ifeq ($(TARGET_BUILD_APPS),)
-# Use AAPT2 only when TARGET_BUILD_APPS is empty because AAPT2 is not compatible with the current
-# setup of prebuilt support libs used in unbundled builds. b/29836407
 LOCAL_USE_AAPT2 := true
-endif
+
+LOCAL_AAPT2_ONLY := true
 
 LOCAL_MANIFEST_FILE := main/AndroidManifest.xml
 LOCAL_MODULE := setup-wizard-lib-gingerbread-compat
@@ -41,8 +39,6 @@
 LOCAL_SDK_VERSION := current
 LOCAL_SRC_FILES := $(call all-java-files-under, main/src gingerbread/src recyclerview/src)
 
-ifdef LOCAL_USE_AAPT2
-
 LOCAL_JAVA_LIBRARIES := \
     android-support-annotations
 
@@ -52,25 +48,4 @@
     android-support-v7-appcompat \
     android-support-v7-recyclerview
 
-else
-
-LOCAL_AAPT_FLAGS := --auto-add-overlay \
-    --extra-packages android.support.compat \
-    --extra-packages android.support.v7.appcompat \
-    --extra-packages android.support.v7.recyclerview
-
-LOCAL_RESOURCE_DIR += \
-    prebuilts/sdk/current/supportcompat/res \
-    prebuilts/sdk/current/supportv7/appcompat/res \
-    prebuilts/sdk/current/supportv7/recyclerview/res
-
-LOCAL_JAVA_LIBRARIES := \
-    android-support-annotations \
-    android-support-compat \
-    android-support-core-ui \
-    android-support-v7-appcompat \
-    android-support-v7-recyclerview
-
-endif
-
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/library/common-gingerbread.mk b/library/common-gingerbread.mk
index 02642f2..70231df 100644
--- a/library/common-gingerbread.mk
+++ b/library/common-gingerbread.mk
@@ -15,46 +15,6 @@
 # Path to directory of setup wizard lib (e.g. frameworks/opt/setupwizard/library)
 suwlib_dir := $(dir $(lastword $(MAKEFILE_LIST)))
 
-ifneq ($(LOCAL_USE_AAPT2),true)
-
-# Check that LOCAL_RESOURCE_DIR is defined
-ifeq (,$(LOCAL_RESOURCE_DIR))
-$(error LOCAL_RESOURCE_DIR must be defined)
-endif
-
-# Add --auto-add-overlay flag if not present
-ifeq (,$(findstring --auto-add-overlay, $(LOCAL_AAPT_FLAGS)))
-LOCAL_AAPT_FLAGS += --auto-add-overlay
-endif
-
-# Include setup wizard library, if not already included
-ifeq (,$(findstring setup-wizard-lib-gingerbread-compat,$(LOCAL_STATIC_JAVA_LIBRARIES)))
-LOCAL_RESOURCE_DIR += \
-    $(suwlib_dir)/main/res \
-    $(suwlib_dir)/gingerbread/res \
-    $(suwlib_dir)/recyclerview/res
-LOCAL_AAPT_FLAGS += --extra-packages com.android.setupwizardlib
-LOCAL_STATIC_JAVA_LIBRARIES += setup-wizard-lib-gingerbread-compat
-endif
-
-## Include transitive dependencies below
-
-# Include support-v7-appcompat, if not already included
-ifeq (,$(findstring android-support-v7-appcompat,$(LOCAL_STATIC_JAVA_LIBRARIES)))
-LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/appcompat/res
-LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.appcompat
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-appcompat
-endif
-
-# Include support-v7-recyclerview, if not already included
-ifeq (,$(findstring android-support-v7-recyclerview,$(LOCAL_STATIC_JAVA_LIBRARIES)))
-LOCAL_RESOURCE_DIR += prebuilts/sdk/current/support/v7/recyclerview/res
-LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.recyclerview
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-v7-recyclerview
-endif
-
-else # LOCAL_USE_AAPT2 := true
-
 ifeq (,$(findstring setup-wizard-lib-gingerbread-compat,$(LOCAL_STATIC_ANDROID_LIBRARIES)))
   LOCAL_STATIC_ANDROID_LIBRARIES += setup-wizard-lib-gingerbread-compat
 endif
@@ -66,5 +26,3 @@
 ifeq (,$(findstring android-support-v7-recyclerview,$(LOCAL_STATIC_ANDROID_LIBRARIES)))
   LOCAL_STATIC_ANDROID_LIBRARIES += android-support-v7-recyclerview
 endif
-
-endif # LOCAL_USE_AAPT2
diff --git a/library/common.mk b/library/common-platform-deprecated.mk
similarity index 83%
rename from library/common.mk
rename to library/common-platform-deprecated.mk
index 3372cf2..bc190d0 100644
--- a/library/common.mk
+++ b/library/common-platform-deprecated.mk
@@ -1,3 +1,4 @@
+# DEPRECATED: This variant is no longer maintained. Use common-gingerbread instead
 #
 # Include this make file to build your application against this module.
 #
@@ -9,7 +10,7 @@
 #   LOCAL_RESOURCE_DIR := \
 #        $(LOCAL_PATH)/res
 #
-#   include frameworks/opt/setupwizard/library/common.mk
+#   include frameworks/opt/setupwizard/library/common-platform-deprecated.mk
 #
 
 # Path to directory of setup wizard lib (e.g. frameworks/opt/setupwizard/library)
diff --git a/library/gingerbread/res/layout/suw_items_expandable_switch.xml b/library/gingerbread/res/layout/suw_items_expandable_switch.xml
index 2b98a9f..21c2c22 100644
--- a/library/gingerbread/res/layout/suw_items_expandable_switch.xml
+++ b/library/gingerbread/res/layout/suw_items_expandable_switch.xml
@@ -20,7 +20,6 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="?android:colorBackground"
-    android:descendantFocusability="blocksDescendants"
     android:orientation="horizontal"
     android:tag="noBackground">
 
diff --git a/library/gingerbread/res/layout/suw_items_switch.xml b/library/gingerbread/res/layout/suw_items_switch.xml
index af326b2..3f101d7 100644
--- a/library/gingerbread/res/layout/suw_items_switch.xml
+++ b/library/gingerbread/res/layout/suw_items_switch.xml
@@ -20,7 +20,6 @@
     style="@style/SuwItemContainer.Verbose"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:descendantFocusability="blocksDescendants"
     android:orientation="horizontal">
 
     <FrameLayout
diff --git a/library/gingerbread/res/values-v27/styles.xml b/library/gingerbread/res/values-v27/styles.xml
new file mode 100644
index 0000000..305a55e
--- /dev/null
+++ b/library/gingerbread/res/values-v27/styles.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2018 Google Inc.
+
+    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 xmlns:tools="http://schemas.android.com/tools">
+
+    <!-- Not needed for dark theme, as default nav bar bg color is black. We need a separate style
+         override here since windowLightNavigationBar is new in v27, and these two styles need to be
+         applied together as a unit. -->
+    <style name="SuwThemeGlifV3.Light" parent="SuwBaseThemeGlifV3.Light">
+        <item name="android:navigationBarColor">@color/suw_glif_v3_nav_bar_color_light</item>
+        <!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
+        <item name="android:navigationBarDividerColor" tools:ignore="NewApi">@color/suw_glif_v3_nav_bar_divider_color_light</item>
+        <!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
+        <item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
+    </style>
+</resources>
diff --git a/library/gingerbread/res/values/styles.xml b/library/gingerbread/res/values/styles.xml
index 6e525ef..241f037 100644
--- a/library/gingerbread/res/values/styles.xml
+++ b/library/gingerbread/res/values/styles.xml
@@ -20,6 +20,7 @@
     <!-- General styles -->
 
     <style name="SuwThemeMaterial" parent="Theme.AppCompat.NoActionBar">
+        <item name="android:colorBackground">@color/suw_color_background_dark</item>
         <item name="android:indeterminateTint" tools:ignore="NewApi">@color/suw_progress_bar_color_dark</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
@@ -38,6 +39,8 @@
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwCardBackground">@drawable/suw_card_bg_dark</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_icon_divider_inset</item>
@@ -51,6 +54,7 @@
     </style>
 
     <style name="SuwThemeMaterial.Light" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:colorBackground">@color/suw_color_background_light</item>
         <item name="android:indeterminateTint" tools:ignore="NewApi">@color/suw_progress_bar_color_light</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode" tools:ignore="NewApi">src_in</item>
@@ -69,6 +73,8 @@
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwCardBackground">@drawable/suw_card_bg_light</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_icon_divider_inset</item>
@@ -98,15 +104,19 @@
         <item name="android:windowSoftInputMode">adjustResize</item>
 
         <item name="colorAccent">@color/suw_color_accent_glif_dark</item>
-        <item name="colorPrimary">@color/suw_color_accent_glif_dark</item>
+        <item name="colorPrimary">?attr/colorAccent</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwColorPrimary">?attr/colorPrimary</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
         <item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
         <item name="suwGlifHeaderGravity">start</item>
+        <item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
         <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
@@ -133,15 +143,19 @@
         <item name="android:windowSoftInputMode">adjustResize</item>
 
         <item name="colorAccent">@color/suw_color_accent_glif_light</item>
-        <item name="colorPrimary">@color/suw_color_accent_glif_light</item>
+        <item name="colorPrimary">?attr/colorAccent</item>
         <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item>
         <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item>
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwColorPrimary">?attr/colorPrimary</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
         <item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
         <item name="suwGlifHeaderGravity">start</item>
+        <item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
         <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
@@ -151,6 +165,21 @@
         <item name="textAppearanceListItemSmall">@style/TextAppearance.SuwGlifItemSummary</item>
     </style>
 
+    <style name="SuwThemeGlifV3" parent="SuwThemeGlifV2">
+        <item name="colorAccent">@color/suw_color_accent_glif_v3</item>
+        <item name="suwButtonAllCaps">false</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
+        <item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
+    </style>
+
+    <style name="SuwBaseThemeGlifV3.Light" parent="SuwThemeGlifV2.Light">
+        <item name="colorAccent">@color/suw_color_accent_glif_v3</item>
+        <item name="suwButtonAllCaps">false</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
+        <item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
+    </style>
+    <style name="SuwThemeGlifV3.Light" parent="SuwBaseThemeGlifV3.Light" />
+
     <!-- Content styles -->
 
     <style name="TextAppearance.SuwDescription" parent="TextAppearance.AppCompat.Medium">
@@ -197,11 +226,18 @@
              ContextThemeWrapper. These self-referencing attributes make sure this is applied as
              both to the button. -->
         <item name="android:buttonStyle">@style/SuwGlifButton.Primary</item>
+        <item name="android:theme">@style/SuwGlifButton.Primary</item>
         <item name="buttonStyle">@style/SuwGlifButton.Primary</item>
 
         <!-- Values used in styles -->
+        <item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/suwButtonFontFamily</item>
         <item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
         <item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
+        <item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">?attr/suwButtonAllCaps</item>
+        <item name="textAllCaps">?attr/suwButtonAllCaps</item>
+
+        <!-- Values used in themes -->
+        <item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
     </style>
 
     <style name="SuwGlifButton.Secondary" parent="Widget.AppCompat.Button.Borderless.Colored">
@@ -213,11 +249,15 @@
         <item name="buttonStyle">@style/SuwGlifButton.Secondary</item>
 
         <!-- Values used in styles -->
+        <item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/suwButtonFontFamily</item>
         <item name="android:minWidth">0dp</item>
         <item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
         <item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
+        <item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">?attr/suwButtonAllCaps</item>
+        <item name="textAllCaps">?attr/suwButtonAllCaps</item>
 
         <!-- Values used in themes -->
+        <item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
         <item name="android:colorControlHighlight" tools:targetApi="lollipop">@color/suw_flat_button_highlight</item>
         <item name="colorControlHighlight">@color/suw_flat_button_highlight</item>
     </style>
@@ -234,6 +274,13 @@
         <item name="android:background">?attr/colorPrimary</item>
     </style>
 
+    <style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.ProgressBar.Large" />
+
+    <style name="SuwFourColorIndeterminateProgressBar" parent="SuwBase.ProgressBarLarge">
+        <item name="android:layout_gravity">center</item>
+        <item name="android:indeterminate">true</item>
+    </style>
+
     <!-- Navigation bar styles -->
 
     <style name="SuwNavBarButtonStyle" parent="@android:style/Widget.Button">
@@ -258,4 +305,8 @@
         <item name="suwNavBarButtonBackground">@drawable/suw_navbar_btn_bg_light</item>
     </style>
 
+
+    <style name="SuwAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert" />
+
+    <style name="SuwAlertDialogTheme.Light" parent="Theme.AppCompat.Light.Dialog.Alert" />
 </resources>
diff --git a/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java b/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java
index be9916e..71d1bb6 100644
--- a/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java
+++ b/library/gingerbread/src/com/android/setupwizardlib/items/ExpandableSwitchItem.java
@@ -138,6 +138,10 @@
         }
 
         tintCompoundDrawables(view);
+
+        // Expandable switch item has focusability on the expandable layout on the left, and the
+        // switch on the right, but not the item itself.
+        view.setFocusable(false);
     }
 
     @Override
diff --git a/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java b/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
index 1e663d6..9965aa0 100644
--- a/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
+++ b/library/gingerbread/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java
@@ -19,6 +19,8 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
 import android.support.v4.view.AccessibilityDelegateCompat;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
@@ -38,12 +40,11 @@
 /**
  * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
  * clicked by accessibility services.
- * <p>
- * <strong>Note: </strong> From Android O on, there is native support for ClickableSpan
- * accessibility, so this class is not needed (and indeed has no effect.)
- * </p>
  *
- * <p />Sample usage:
+ * <p><strong>Note:</strong> This class is a no-op on Android O or above since there is native
+ * support for ClickableSpan accessibility.
+ *
+ * <p>Sample usage:
  * <pre>
  * LinkAccessibilityHelper mAccessibilityHelper;
  *
@@ -68,294 +69,260 @@
 
     private static final String TAG = "LinkAccessibilityHelper";
 
-    private final TextView mView;
-    private final Rect mTempRect = new Rect();
-    private final ExploreByTouchHelper mExploreByTouchHelper;
+    private final AccessibilityDelegateCompat mDelegate;
 
     public LinkAccessibilityHelper(TextView view) {
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
-            // Pre-O, we essentially extend ExploreByTouchHelper to expose a virtual view hierarchy
-            mExploreByTouchHelper = new ExploreByTouchHelper(view) {
-                @Override
-                protected int getVirtualViewAt(float x, float y) {
-                    return LinkAccessibilityHelper.this.getVirtualViewAt(x, y);
-                }
+        this(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                // Platform support was added in O. This helper will be no-op
+                ? new AccessibilityDelegateCompat()
+                // Pre-O, we extend ExploreByTouchHelper to expose a virtual view hierarchy
+                : new PreOLinkAccessibilityHelper(view));
+    }
 
-                @Override
-                protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-                    LinkAccessibilityHelper.this.getVisibleVirtualViews(virtualViewIds);
-                }
-
-                @Override
-                protected void onPopulateEventForVirtualView(int virtualViewId,
-                        AccessibilityEvent event) {
-                    LinkAccessibilityHelper
-                            .this.onPopulateEventForVirtualView(virtualViewId, event);
-                }
-
-                @Override
-                protected void onPopulateNodeForVirtualView(int virtualViewId,
-                        AccessibilityNodeInfoCompat infoCompat) {
-                    LinkAccessibilityHelper
-                            .this.onPopulateNodeForVirtualView(virtualViewId, infoCompat);
-
-                }
-
-                @Override
-                protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
-                        Bundle arguments) {
-                    return LinkAccessibilityHelper.this
-                            .onPerformActionForVirtualView(virtualViewId, action, arguments);
-                }
-            };
-        } else {
-            mExploreByTouchHelper = null;
-        }
-        mView = view;
+    @VisibleForTesting
+    LinkAccessibilityHelper(@NonNull AccessibilityDelegateCompat delegate) {
+        mDelegate = delegate;
     }
 
     @Override
     public void sendAccessibilityEvent(View host, int eventType) {
-        if (mExploreByTouchHelper != null) {
-            mExploreByTouchHelper.sendAccessibilityEvent(host, eventType);
-        } else {
-            super.sendAccessibilityEvent(host, eventType);
-        }
+        mDelegate.sendAccessibilityEvent(host, eventType);
     }
 
     @Override
     public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) {
-        if (mExploreByTouchHelper != null) {
-            mExploreByTouchHelper.sendAccessibilityEventUnchecked(host, event);
-        } else {
-            super.sendAccessibilityEventUnchecked(host, event);
-        }
+        mDelegate.sendAccessibilityEventUnchecked(host, event);
     }
 
     @Override
     public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
-        return (mExploreByTouchHelper != null)
-                ? mExploreByTouchHelper.dispatchPopulateAccessibilityEvent(host, event)
-                : super.dispatchPopulateAccessibilityEvent(host, event);
+        return mDelegate.dispatchPopulateAccessibilityEvent(host, event);
     }
 
     @Override
     public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
-        if (mExploreByTouchHelper != null) {
-            mExploreByTouchHelper.onPopulateAccessibilityEvent(host, event);
-        } else {
-            super.onPopulateAccessibilityEvent(host, event);
-        }
+        mDelegate.onPopulateAccessibilityEvent(host, event);
     }
 
     @Override
     public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
-        if (mExploreByTouchHelper != null) {
-            mExploreByTouchHelper.onInitializeAccessibilityEvent(host, event);
-        } else {
-            super.onInitializeAccessibilityEvent(host, event);
-        }
+        mDelegate.onInitializeAccessibilityEvent(host, event);
     }
 
     @Override
     public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
-        if (mExploreByTouchHelper != null) {
-            mExploreByTouchHelper.onInitializeAccessibilityNodeInfo(host, info);
-        } else {
-            super.onInitializeAccessibilityNodeInfo(host, info);
-        }
+        mDelegate.onInitializeAccessibilityNodeInfo(host, info);
     }
 
     @Override
     public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
             AccessibilityEvent event) {
-        return (mExploreByTouchHelper != null)
-                ? mExploreByTouchHelper.onRequestSendAccessibilityEvent(host, child, event)
-                : super.onRequestSendAccessibilityEvent(host, child, event);
+        return mDelegate.onRequestSendAccessibilityEvent(host, child, event);
     }
 
     @Override
     public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) {
-        return (mExploreByTouchHelper != null)
-                ? mExploreByTouchHelper.getAccessibilityNodeProvider(host)
-                : super.getAccessibilityNodeProvider(host);
+        return mDelegate.getAccessibilityNodeProvider(host);
     }
 
     @Override
     public boolean performAccessibilityAction(View host, int action, Bundle args) {
-        return (mExploreByTouchHelper != null)
-                ? mExploreByTouchHelper.performAccessibilityAction(host, action, args)
-                : super.performAccessibilityAction(host, action, args);
+        return mDelegate.performAccessibilityAction(host, action, args);
     }
 
     /**
-     * Delegated to {@link ExploreByTouchHelper}
+     * Dispatches hover event to the virtual view hierarchy. This method should be called in
+     * {@link View#dispatchHoverEvent(MotionEvent)}.
+     *
+     * @see ExploreByTouchHelper#dispatchHoverEvent(MotionEvent)
      */
     public final boolean dispatchHoverEvent(MotionEvent event) {
-        return (mExploreByTouchHelper != null) ? mExploreByTouchHelper.dispatchHoverEvent(event)
-                : false;
+        return mDelegate instanceof ExploreByTouchHelper
+                && ((ExploreByTouchHelper) mDelegate).dispatchHoverEvent(event);
     }
 
-    protected int getVirtualViewAt(float x, float y) {
-        final CharSequence text = mView.getText();
-        if (text instanceof Spanned) {
-            final Spanned spannedText = (Spanned) text;
-            final int offset = getOffsetForPosition(mView, x, y);
-            ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
-            if (linkSpans.length == 1) {
-                ClickableSpan linkSpan = linkSpans[0];
-                return spannedText.getSpanStart(linkSpan);
+    @VisibleForTesting
+    static class PreOLinkAccessibilityHelper extends ExploreByTouchHelper {
+
+        private final Rect mTempRect = new Rect();
+        private final TextView mView;
+
+        PreOLinkAccessibilityHelper(TextView view) {
+            super(view);
+            mView = view;
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            final CharSequence text = mView.getText();
+            if (text instanceof Spanned) {
+                final Spanned spannedText = (Spanned) text;
+                final int offset = getOffsetForPosition(mView, x, y);
+                ClickableSpan[] linkSpans =
+                        spannedText.getSpans(offset, offset, ClickableSpan.class);
+                if (linkSpans.length == 1) {
+                    ClickableSpan linkSpan = linkSpans[0];
+                    return spannedText.getSpanStart(linkSpan);
+                }
+            }
+            return ExploreByTouchHelper.INVALID_ID;
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
+            final CharSequence text = mView.getText();
+            if (text instanceof Spanned) {
+                final Spanned spannedText = (Spanned) text;
+                ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
+                        ClickableSpan.class);
+                for (ClickableSpan span : linkSpans) {
+                    virtualViewIds.add(spannedText.getSpanStart(span));
+                }
             }
         }
-        return ExploreByTouchHelper.INVALID_ID;
-    }
 
-    protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
-        final CharSequence text = mView.getText();
-        if (text instanceof Spanned) {
-            final Spanned spannedText = (Spanned) text;
-            ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
-                    ClickableSpan.class);
-            for (ClickableSpan span : linkSpans) {
-                virtualViewIds.add(spannedText.getSpanStart(span));
-            }
-        }
-    }
-
-    protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
-        final ClickableSpan span = getSpanForOffset(virtualViewId);
-        if (span != null) {
-            event.setContentDescription(getTextForSpan(span));
-        } else {
-            Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
-            event.setContentDescription(mView.getText());
-        }
-    }
-
-    protected void onPopulateNodeForVirtualView(int virtualViewId,
-            AccessibilityNodeInfoCompat info) {
-        final ClickableSpan span = getSpanForOffset(virtualViewId);
-        if (span != null) {
-            info.setContentDescription(getTextForSpan(span));
-        } else {
-            Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
-            info.setContentDescription(mView.getText());
-        }
-        info.setFocusable(true);
-        info.setClickable(true);
-        getBoundsForSpan(span, mTempRect);
-        if (mTempRect.isEmpty()) {
-            Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
-            mTempRect.set(0, 0, 1, 1);
-        }
-        info.setBoundsInParent(mTempRect);
-        info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
-    }
-
-    protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
-            Bundle arguments) {
-        if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
-            ClickableSpan span = getSpanForOffset(virtualViewId);
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            final ClickableSpan span = getSpanForOffset(virtualViewId);
             if (span != null) {
-                span.onClick(mView);
-                return true;
+                event.setContentDescription(getTextForSpan(span));
             } else {
                 Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
+                event.setContentDescription(mView.getText());
             }
         }
-        return false;
-    }
 
-    private ClickableSpan getSpanForOffset(int offset) {
-        CharSequence text = mView.getText();
-        if (text instanceof Spanned) {
-            Spanned spannedText = (Spanned) text;
-            ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
-            if (spans.length == 1) {
-                return spans[0];
+        @Override
+        protected void onPopulateNodeForVirtualView(
+                int virtualViewId,
+                AccessibilityNodeInfoCompat info) {
+            final ClickableSpan span = getSpanForOffset(virtualViewId);
+            if (span != null) {
+                info.setContentDescription(getTextForSpan(span));
+            } else {
+                Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
+                info.setContentDescription(mView.getText());
             }
+            info.setFocusable(true);
+            info.setClickable(true);
+            getBoundsForSpan(span, mTempRect);
+            if (mTempRect.isEmpty()) {
+                Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
+                mTempRect.set(0, 0, 1, 1);
+            }
+            info.setBoundsInParent(mTempRect);
+            info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
         }
-        return null;
-    }
 
-    private CharSequence getTextForSpan(ClickableSpan span) {
-        CharSequence text = mView.getText();
-        if (text instanceof Spanned) {
-            Spanned spannedText = (Spanned) text;
-            return spannedText.subSequence(spannedText.getSpanStart(span),
-                    spannedText.getSpanEnd(span));
-        }
-        return text;
-    }
-
-    // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
-    // section on the first line.
-    private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
-        CharSequence text = mView.getText();
-        outRect.setEmpty();
-        if (text instanceof Spanned) {
-            final Layout layout = mView.getLayout();
-            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);
+        @Override
+        protected boolean onPerformActionForVirtualView(
+                int virtualViewId,
+                int action,
+                Bundle arguments) {
+            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
+                ClickableSpan span = getSpanForOffset(virtualViewId);
+                if (span != null) {
+                    span.onClick(mView);
+                    return true;
                 } 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;
-                    }
+                    Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
                 }
-
-                // Offset for padding
-                outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
             }
+            return false;
         }
-        return outRect;
-    }
 
-    // Compat implementation of TextView#getOffsetForPosition().
+        private ClickableSpan getSpanForOffset(int offset) {
+            CharSequence text = mView.getText();
+            if (text instanceof Spanned) {
+                Spanned spannedText = (Spanned) text;
+                ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
+                if (spans.length == 1) {
+                    return spans[0];
+                }
+            }
+            return null;
+        }
 
-    private static int getOffsetForPosition(TextView view, float x, float y) {
-        if (view.getLayout() == null) return -1;
-        final int line = getLineAtCoordinate(view, y);
-        return getOffsetAtCoordinate(view, line, x);
-    }
+        private CharSequence getTextForSpan(ClickableSpan span) {
+            CharSequence text = mView.getText();
+            if (text instanceof Spanned) {
+                Spanned spannedText = (Spanned) text;
+                return spannedText.subSequence(
+                        spannedText.getSpanStart(span),
+                        spannedText.getSpanEnd(span));
+            }
+            return text;
+        }
 
-    private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
-        x -= view.getTotalPaddingLeft();
-        // Clamp the position to inside of the view.
-        x = Math.max(0.0f, x);
-        x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
-        x += view.getScrollX();
-        return x;
-    }
+        // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for
+        // the section on the first line.
+        private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
+            CharSequence text = mView.getText();
+            outRect.setEmpty();
+            if (text instanceof Spanned) {
+                final Layout layout = mView.getLayout();
+                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;
+                        }
+                    }
 
-    private static int getLineAtCoordinate(TextView view, float y) {
-        y -= view.getTotalPaddingTop();
-        // Clamp the position to inside of the view.
-        y = Math.max(0.0f, y);
-        y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
-        y += view.getScrollY();
-        return view.getLayout().getLineForVertical((int) y);
-    }
+                    // Offset for padding
+                    outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
+                }
+            }
+            return outRect;
+        }
 
-    private static int getOffsetAtCoordinate(TextView view, int line, float x) {
-        x = convertToLocalHorizontalCoordinate(view, x);
-        return view.getLayout().getOffsetForHorizontal(line, x);
+        // Compat implementation of TextView#getOffsetForPosition().
+
+        private static int getOffsetForPosition(TextView view, float x, float y) {
+            if (view.getLayout() == null) return -1;
+            final int line = getLineAtCoordinate(view, y);
+            return getOffsetAtCoordinate(view, line, x);
+        }
+
+        private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
+            x -= view.getTotalPaddingLeft();
+            // Clamp the position to inside of the view.
+            x = Math.max(0.0f, x);
+            x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
+            x += view.getScrollX();
+            return x;
+        }
+
+        private static int getLineAtCoordinate(TextView view, float y) {
+            y -= view.getTotalPaddingTop();
+            // Clamp the position to inside of the view.
+            y = Math.max(0.0f, y);
+            y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
+            y += view.getScrollY();
+            return view.getLayout().getLineForVertical((int) y);
+        }
+
+        private static int getOffsetAtCoordinate(TextView view, int line, float x) {
+            x = convertToLocalHorizontalCoordinate(view, x);
+            return view.getLayout().getOffsetForHorizontal(line, x);
+        }
     }
 }
diff --git a/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java b/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java
index 5172c47..d7a3c2e 100644
--- a/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java
+++ b/library/gingerbread/src/com/android/setupwizardlib/view/NavigationBarButton.java
@@ -16,6 +16,7 @@
 
 package com.android.setupwizardlib.view;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
@@ -30,6 +31,7 @@
  * Button for navigation bar, which includes tinting of its compound drawables to be used for dark
  * and light themes.
  */
+@SuppressLint("AppCompatCustomView")
 public class NavigationBarButton extends Button {
 
     public NavigationBarButton(Context context) {
diff --git a/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java b/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java
index e6bc9da..1b1f82e 100644
--- a/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java
+++ b/library/gingerbread/src/com/android/setupwizardlib/view/RichTextView.java
@@ -25,7 +25,7 @@
 import android.text.Annotation;
 import android.text.SpannableString;
 import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.AttributeSet;
@@ -36,6 +36,7 @@
 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
 import com.android.setupwizardlib.span.SpanHelper;
 import com.android.setupwizardlib.util.LinkAccessibilityHelper;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
 
 /**
  * An extension of TextView that automatically replaces the annotation tags as specified in
@@ -121,7 +122,7 @@
             // 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());
+            setMovementMethod(TouchableLinkMovementMethod.getInstance());
         } else {
             setMovementMethod(null);
         }
@@ -130,6 +131,17 @@
         // as individual TextViews consume touch events and thereby reducing the focus window
         // shown by Talkback. Disable focus if there are no links
         setFocusable(hasLinks);
+        // Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is
+        // focusable in touch mode, we may be focused when the screen is first shown, and starting
+        // a screen halfway scrolled down is confusing to the user.
+        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+            setRevealOnFocusHint(false);
+            // setRevealOnFocusHint is a new API added in SDK 25. For lower SDK versions, do not
+            // call setFocusableInTouchMode. We won't get touch effect on those earlier versions,
+            // but the link will still work, and will prevent the scroll view from starting halfway
+            // down the page.
+            setFocusableInTouchMode(hasLinks);
+        }
     }
 
     private boolean hasLinks(CharSequence text) {
@@ -142,6 +154,25 @@
     }
 
     @Override
+    @SuppressWarnings("ClickableViewAccessibility")  // super.onTouchEvent is called
+    public boolean onTouchEvent(MotionEvent event) {
+        // Since View#onTouchEvent always return true if the view is clickable (which is the case
+        // when a TextView has a movement method), override the implementation to allow the movement
+        // method, if it implements TouchableMovementMethod, to say that the touch is not handled,
+        // allowing the event to bubble up to the parent view.
+        boolean superResult = super.onTouchEvent(event);
+        MovementMethod movementMethod = getMovementMethod();
+        if (movementMethod instanceof TouchableMovementMethod) {
+            TouchableMovementMethod touchableMovementMethod =
+                    (TouchableMovementMethod) movementMethod;
+            if (touchableMovementMethod.getLastTouchEvent() == event) {
+                return touchableMovementMethod.isLastTouchEventHandled();
+            }
+        }
+        return superResult;
+    }
+
+    @Override
     protected boolean dispatchHoverEvent(MotionEvent event) {
         if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {
             return true;
diff --git a/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/items/ButtonItemDrawingTest.java b/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
index 74d3be6..b97905c 100644
--- a/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
+++ b/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/items/ButtonItemDrawingTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertTrue;
 
+import android.support.annotation.StyleRes;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.SmallTest;
 import android.support.test.rule.UiThreadTestRule;
@@ -29,7 +30,6 @@
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.test.util.DrawingTestHelper;
 
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,40 +38,61 @@
 @RunWith(AndroidJUnit4.class)
 public class ButtonItemDrawingTest {
 
-    private static final int GOOGLE_BLUE = 0xff4285f4;
+    private static final int GLIF_ACCENT_COLOR = 0xff4285f4;
+    private static final int GLIF_V3_ACCENT_COLOR = 0xff1a73e8;
 
     // These tests need to be run on UI thread because button uses ValueAnimator
     @Rule
     public UiThreadTestRule mUiThreadTestRule = new UiThreadTestRule();
 
-    private ViewGroup mParent;
-
-    @Before
-    public void setUp() throws Exception {
-        mParent = new LinearLayout(
-                DrawingTestHelper.createCanvasActivity(R.style.SuwThemeGlif_Light));
-    }
-
     @Test
     @UiThreadTest
-    public void testColoredButtonTheme() {
-        TestButtonItem item = new TestButtonItem();
-        item.setTheme(R.style.SuwButtonItem_Colored);
-        item.setText("foobar");
-
-        final Button button = item.createButton(mParent);
+    public void drawButton_glif_shouldHaveAccentColoredButton()
+            throws InstantiationException, IllegalAccessException {
+        Button button = createButton(R.style.SuwThemeGlif_Light);
 
         DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
         drawingTestHelper.drawView(button);
 
-        int googleBluePixelCount = 0;
-        for (int pixel : drawingTestHelper.getPixels()) {
-            if (pixel == GOOGLE_BLUE) {
-                googleBluePixelCount++;
+        int accentPixelCount =
+                countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_ACCENT_COLOR);
+        assertTrue("> 10 pixels should be #4285f4. Found " + accentPixelCount,
+                accentPixelCount > 10);
+    }
+
+    @Test
+    @UiThreadTest
+    public void drawButton_glifV3_shouldHaveAccentColoredButton()
+            throws InstantiationException, IllegalAccessException {
+        Button button = createButton(R.style.SuwThemeGlifV3_Light);
+
+        DrawingTestHelper drawingTestHelper = new DrawingTestHelper(50, 50);
+        drawingTestHelper.drawView(button);
+
+        int accentPixelCount =
+                countPixelsWithColor(drawingTestHelper.getPixels(), GLIF_V3_ACCENT_COLOR);
+        assertTrue("> 10 pixels should be #1a73e8. Found " + accentPixelCount,
+                accentPixelCount > 10);
+    }
+
+    private Button createButton(@StyleRes int theme)
+            throws InstantiationException, IllegalAccessException {
+        final ViewGroup parent = new LinearLayout(DrawingTestHelper.createCanvasActivity(theme));
+        TestButtonItem item = new TestButtonItem();
+        item.setTheme(R.style.SuwButtonItem_Colored);
+        item.setText("foobar");
+
+        return item.createButton(parent);
+    }
+
+    private int countPixelsWithColor(int[] pixels, int color) {
+        int count = 0;
+        for (int pixel : pixels) {
+            if (pixel == color) {
+                count++;
             }
         }
-        assertTrue("> 10 pixels should be Google blue. Found " + googleBluePixelCount,
-                googleBluePixelCount > 10);
+        return count;
     }
 
     private static class TestButtonItem extends ButtonItem {
diff --git a/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java b/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/util/LinkAccessibilityHelperTest.java
similarity index 75%
rename from library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
rename to library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/util/LinkAccessibilityHelperTest.java
index 844e73e..6228e6f 100644
--- a/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java
+++ b/library/gingerbread/test/instrumentation/src/com/android/setupwizardlib/util/LinkAccessibilityHelperTest.java
@@ -14,29 +14,35 @@
  * limitations under the License.
  */
 
-package com.android.setupwizardlib.test;
+package com.android.setupwizardlib.util;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 import android.graphics.Rect;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v4.text.BidiFormatter;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
 import android.support.v4.widget.ExploreByTouchHelper;
 import android.text.SpannableStringBuilder;
 import android.util.DisplayMetrics;
 import android.util.TypedValue;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
+import android.widget.FrameLayout;
 import android.widget.TextView;
 
 import com.android.setupwizardlib.span.LinkSpan;
-import com.android.setupwizardlib.util.LinkAccessibilityHelper;
+import com.android.setupwizardlib.util.LinkAccessibilityHelper.PreOLinkAccessibilityHelper;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,13 +58,12 @@
     private static final LinkSpan LINK_SPAN = new LinkSpan("foobar");
 
     private TextView mTextView;
-    private TestLinkAccessibilityHelper mHelper;
+    private TestPreOLinkAccessibilityHelper mHelper;
 
     private DisplayMetrics mDisplayMetrics;
 
     @Test
     public void testGetVirtualViewAt() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(15), dp2Px(10));
         assertEquals("Virtual view ID should be 1", 1, virtualViewId);
@@ -66,7 +71,6 @@
 
     @Test
     public void testGetVirtualViewAtHost() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(100), dp2Px(100));
         assertEquals("Virtual view ID should be INVALID_ID",
@@ -75,7 +79,6 @@
 
     @Test
     public void testGetVisibleVirtualViews() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         List<Integer> virtualViewIds = new ArrayList<>();
         mHelper.getVisibleVirtualViews(virtualViewIds);
@@ -86,7 +89,6 @@
 
     @Test
     public void testOnPopulateEventForVirtualView() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         AccessibilityEvent event = AccessibilityEvent.obtain();
         mHelper.onPopulateEventForVirtualView(1, event);
@@ -100,7 +102,6 @@
 
     @Test
     public void testOnPopulateEventForVirtualViewHost() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         AccessibilityEvent event = AccessibilityEvent.obtain();
         mHelper.onPopulateEventForVirtualView(ExploreByTouchHelper.INVALID_ID, event);
@@ -113,7 +114,6 @@
 
     @Test
     public void testOnPopulateNodeForVirtualView() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
         mHelper.onPopulateNodeForVirtualView(1, info);
@@ -132,7 +132,6 @@
 
     @Test
     public void testNullLayout() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         initTextView();
         // Setting the padding will cause the layout to be null-ed out.
         mTextView.setPadding(1, 1, 1, 1);
@@ -150,7 +149,6 @@
 
     @Test
     public void testRtlLayout() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         SpannableStringBuilder ssb = new SpannableStringBuilder("מכונה בתרגום");
         ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
         initTextView(ssb);
@@ -170,7 +168,6 @@
 
     @Test
     public void testMultilineLink() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         SpannableStringBuilder ssb = new SpannableStringBuilder(
                 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
                 + "Praesent accumsan efficitur eros eu porttitor.");
@@ -192,7 +189,6 @@
 
     @Test
     public void testRtlMultilineLink() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
                 + "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
                 + "דפים המחשב מיזמים ב.";
@@ -216,7 +212,6 @@
 
     @Test
     public void testBidiMultilineLink() {
-        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return;
         String iwLoremIpsum = "אחר על רביעי אקטואליה. לוח דת אחרות המקובל רומנית, מיזמים מועמדים "
                 + "האנציקלופדיה בה צ'ט. מתן מה שנורו לערוך ייִדיש, בקר או החול אנתרופולוגיה, עוד "
                 + "דפים המחשב מיזמים ב.";
@@ -243,6 +238,70 @@
         info.recycle();
     }
 
+    @Test
+    public void testMethodDelegation() {
+        initTextView();
+        ExploreByTouchHelper delegate = mock(TestPreOLinkAccessibilityHelper.class);
+        LinkAccessibilityHelper helper = new LinkAccessibilityHelper(delegate);
+
+        AccessibilityEvent accessibilityEvent =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+        helper.sendAccessibilityEvent(mTextView, AccessibilityEvent.TYPE_VIEW_CLICKED);
+        verify(delegate).sendAccessibilityEvent(
+                same(mTextView),
+                eq(AccessibilityEvent.TYPE_VIEW_CLICKED));
+
+        helper.sendAccessibilityEventUnchecked(mTextView, accessibilityEvent);
+        verify(delegate).sendAccessibilityEventUnchecked(same(mTextView), same(accessibilityEvent));
+
+        helper.performAccessibilityAction(
+                mTextView,
+                AccessibilityActionCompat.ACTION_CLICK.getId(),
+                Bundle.EMPTY);
+        verify(delegate).performAccessibilityAction(
+                same(mTextView),
+                eq(AccessibilityActionCompat.ACTION_CLICK.getId()),
+                eq(Bundle.EMPTY));
+
+        helper.dispatchPopulateAccessibilityEvent(
+                mTextView,
+                accessibilityEvent);
+        verify(delegate).dispatchPopulateAccessibilityEvent(
+                same(mTextView),
+                same(accessibilityEvent));
+
+        MotionEvent motionEvent = MotionEvent.obtain(0, 0, 0, 0, 0, 0);
+        helper.dispatchHoverEvent(motionEvent);
+        verify(delegate).dispatchHoverEvent(eq(motionEvent));
+
+        helper.getAccessibilityNodeProvider(mTextView);
+        verify(delegate).getAccessibilityNodeProvider(same(mTextView));
+
+        helper.onInitializeAccessibilityEvent(mTextView, accessibilityEvent);
+        verify(delegate).onInitializeAccessibilityEvent(
+                same(mTextView),
+                eq(accessibilityEvent));
+
+        AccessibilityNodeInfoCompat accessibilityNodeInfo = AccessibilityNodeInfoCompat.obtain();
+        helper.onInitializeAccessibilityNodeInfo(mTextView, accessibilityNodeInfo);
+        verify(delegate).onInitializeAccessibilityNodeInfo(
+                same(mTextView),
+                same(accessibilityNodeInfo));
+
+        helper.onPopulateAccessibilityEvent(mTextView, accessibilityEvent);
+        verify(delegate).onPopulateAccessibilityEvent(
+                same(mTextView),
+                same(accessibilityEvent));
+
+        FrameLayout parent = new FrameLayout(InstrumentationRegistry.getTargetContext());
+        helper.onRequestSendAccessibilityEvent(parent, mTextView, accessibilityEvent);
+        verify(delegate).onRequestSendAccessibilityEvent(
+                same(parent),
+                same(mTextView),
+                same(accessibilityEvent));
+    }
+
     private void initTextView() {
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(LINK_SPAN, 1, 2, 0 /* flags */);
@@ -254,7 +313,7 @@
         mTextView.setSingleLine(false);
         mTextView.setText(text);
         mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
-        mHelper = new TestLinkAccessibilityHelper(mTextView);
+        mHelper = new TestPreOLinkAccessibilityHelper(mTextView);
 
         int measureExactly500dp = View.MeasureSpec.makeMeasureSpec(dp2Px(500),
                 View.MeasureSpec.EXACTLY);
@@ -270,9 +329,9 @@
         return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics);
     }
 
-    private static class TestLinkAccessibilityHelper extends LinkAccessibilityHelper {
+    public static class TestPreOLinkAccessibilityHelper extends PreOLinkAccessibilityHelper {
 
-        TestLinkAccessibilityHelper(TextView view) {
+        TestPreOLinkAccessibilityHelper(TextView view) {
             super(view);
         }
 
diff --git a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
index 0ae0737..6192061 100644
--- a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
+++ b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/ExpandableSwitchItemTest.java
@@ -16,35 +16,29 @@
 
 package com.android.setupwizardlib.items;
 
-import static org.hamcrest.Matchers.hasItem;
-import static org.hamcrest.Matchers.not;
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.robolectric.RuntimeEnvironment.application;
 
-import android.support.v7.widget.SwitchCompat;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
+import com.android.setupwizardlib.view.CheckableLinearLayout;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.annotation.Config;
 
-import java.util.ArrayList;
-
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class ExpandableSwitchItemTest {
 
     private TextView mSummaryView;
@@ -71,6 +65,14 @@
         assertEquals("Should be collapsed initially", "TestSummary", mItem.getSummary());
         assertEquals("Summary view should display collapsed summary",
                 "TestSummary", mSummaryView.getText());
+
+        assertFalse("Expandable switch item itself should not be focusable", view.isFocusable());
+
+        View switchContent = view.findViewById(R.id.suw_items_expandable_switch_content);
+        assertThat(switchContent).isInstanceOf(CheckableLinearLayout.class);
+        assertThat(switchContent.isFocusable())
+                .named("expandable content focusable")
+                .isTrue();
     }
 
     @Test
@@ -132,57 +134,25 @@
         mItem.onBindView(view);
 
         final View titleView = view.findViewById(R.id.suw_items_title);
-        assertThat("state_checked should not be set initially",
-                toArrayList(titleView.getDrawableState()),
-                not(hasItem(android.R.attr.state_checked)));
+        assertThat(titleView.getDrawableState()).asList().named("Drawable state")
+                .doesNotContain(android.R.attr.state_checked);
 
         mItem.setExpanded(true);
         mItem.onBindView(view);
-        assertThat("state_checked should not be set initially",
-                toArrayList(titleView.getDrawableState()),
-                hasItem(android.R.attr.state_checked));
+        assertThat(titleView.getDrawableState()).asList().named("Drawable state")
+                .contains(android.R.attr.state_checked);
 
         mItem.setExpanded(false);
         mItem.onBindView(view);
-        assertThat("state_checked should not be set initially",
-                toArrayList(titleView.getDrawableState()),
-                not(hasItem(android.R.attr.state_checked)));
-    }
-
-    private ArrayList<Integer> toArrayList(int[] array) {
-        ArrayList<Integer> arrayList = new ArrayList<>(array.length);
-        for (int i : array) {
-            arrayList.add(i);
-        }
-        return arrayList;
+        assertThat(titleView.getDrawableState()).asList().named("Drawable state")
+                .doesNotContain(android.R.attr.state_checked);
     }
 
     private ViewGroup createLayout() {
-        ViewGroup root = new FrameLayout(application);
-
-        ViewGroup content = new FrameLayout(application);
-        content.setId(R.id.suw_items_expandable_switch_content);
-        root.addView(content);
-
-        TextView titleView = new TextView(application);
-        titleView.setId(R.id.suw_items_title);
-        content.addView(titleView);
-
-        mSummaryView = new TextView(application);
-        mSummaryView.setId(R.id.suw_items_summary);
-        content.addView(mSummaryView);
-
-        FrameLayout iconContainer = new FrameLayout(application);
-        iconContainer.setId(R.id.suw_items_icon_container);
-        content.addView(iconContainer);
-
-        ImageView iconView = new ImageView(application);
-        iconView.setId(R.id.suw_items_icon);
-        iconContainer.addView(iconView);
-
-        SwitchCompat switchView = new SwitchCompat(application);
-        switchView.setId(R.id.suw_items_switch);
-        root.addView(switchView);
+        ViewGroup root =
+                (ViewGroup) LayoutInflater.from(application)
+                        .inflate(R.layout.suw_items_expandable_switch, null);
+        mSummaryView = root.findViewById(R.id.suw_items_summary);
 
         return root;
     }
diff --git a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/SwitchItemTest.java b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/SwitchItemTest.java
index d391d80..fa5bbba 100644
--- a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/SwitchItemTest.java
+++ b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/items/SwitchItemTest.java
@@ -31,7 +31,6 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
@@ -40,7 +39,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class SwitchItemTest {
 
     private SwitchCompat mSwitch;
diff --git a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/util/DimensionConsistencyTest.java b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/util/DimensionConsistencyTest.java
index 43e7f03..7a08235 100644
--- a/library/gingerbread/test/robotest/src/com/android/setupwizardlib/util/DimensionConsistencyTest.java
+++ b/library/gingerbread/test/robotest/src/com/android/setupwizardlib/util/DimensionConsistencyTest.java
@@ -25,7 +25,6 @@
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
@@ -35,7 +34,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
+@Config(sdk = Config.ALL_SDKS)
 public class DimensionConsistencyTest {
 
     // Visual height of the framework switch widget
diff --git a/library/lint.xml b/library/lint.xml
index ca22c65..625b20d 100644
--- a/library/lint.xml
+++ b/library/lint.xml
@@ -1,7 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <lint>
+    <!-- SUW lib prefixes styleable resources -->
+    <issue id="CustomViewStyleable" severity="ignore" />
     <issue id="ExtraTranslation" severity="ignore" />
     <issue id="GradleDependency" severity="ignore" />
     <issue id="MissingTranslation" severity="ignore" />
+    <!-- Stop lint from complaining about SDK version checks in the "platform" variant -->
+    <issue id="ObsoleteSdkInt" severity="ignore" />
     <issue id="RtlEnabled" severity="ignore" />
 </lint>
diff --git a/library/main/res/drawable-v21/suw_edit_text_bg_shape.xml b/library/main/res/drawable-v21/suw_edit_text_bg_shape.xml
new file mode 100644
index 0000000..ad55ec6
--- /dev/null
+++ b/library/main/res/drawable-v21/suw_edit_text_bg_shape.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners android:topLeftRadius="4dp" android:topRightRadius="4dp"/>
+    <solid android:color="?attr/suwEditTextBackgroundColor"/>
+</shape>
\ No newline at end of file
diff --git a/library/main/res/drawable-v21/suw_edittext_bg.xml b/library/main/res/drawable-v21/suw_edittext_bg.xml
new file mode 100644
index 0000000..b69c10b
--- /dev/null
+++ b/library/main/res/drawable-v21/suw_edittext_bg.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_enabled="false">
+        <layer-list>
+            <item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="1dp"/>
+            <item android:gravity="bottom">
+                <shape>
+                    <size android:height="1dp"/>
+                    <solid android:color="?android:attr/textColorSecondary"/>
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+    <item android:state_focused="false" android:state_pressed="false">
+        <layer-list>
+            <item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="1dp" />
+            <item android:gravity="bottom">
+                <shape>
+                    <size android:height="1dp"/>
+                    <solid android:color="?android:attr/textColorSecondary"/>
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+    <item>
+        <layer-list>
+            <item android:drawable="@drawable/suw_edit_text_bg_shape" android:bottom="2dp" />
+            <item android:gravity="bottom">
+                <shape>
+                    <size android:height="2dp"/>
+                    <solid android:color="?android:attr/colorAccent"/>
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+</selector>
diff --git a/library/main/res/drawable-v21/suw_fourcolor_progress_bar.xml b/library/main/res/drawable-v21/suw_fourcolor_progress_bar.xml
new file mode 100644
index 0000000..2ac35ee
--- /dev/null
+++ b/library/main/res/drawable-v21/suw_fourcolor_progress_bar.xml
@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+
+<!-- Asset for 4 color indeterminate progress bar, which is a ring with 4 shades of blue -->
+<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="MissingPrefix">
+    <!-- Ignore MissingPrefix: aapt:attr tags take the name attribute without "android:" prefix -->
+    <!-- TODO(yukl): Update to a newer version of lint which properly handles this case -->
+    <aapt:attr name="android:drawable">
+        <vector android:width="823dp" android:height="823dp" android:viewportHeight="823"
+            android:viewportWidth="823">
+            <group android:name="blue1" android:translateX="411.5" android:translateY="411.5">
+                <path android:name="blue1_path"
+                    android:pathData="M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
+                    android:strokeAlpha="1" android:strokeColor="#4688f1"
+                    android:strokeLineCap="round" android:strokeLineJoin="round"
+                    android:strokeWidth="27" />
+            </group>
+            <group android:name="blue2" android:translateX="411.5" android:translateY="411.5">
+                <path android:name="blue2_path"
+                    android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
+                    android:strokeAlpha="1" android:strokeColor="#7dacf4"
+                    android:strokeLineCap="round" android:strokeLineJoin="round"
+                    android:strokeWidth="28" />
+            </group>
+            <group android:name="blue3" android:translateX="411.5" android:translateY="411.5">
+                <path android:name="blue3_path"
+                    android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
+                    android:strokeAlpha="1" android:strokeColor="#c7dbfb"
+                    android:strokeLineCap="round" android:strokeLineJoin="round"
+                    android:strokeWidth="29" />
+            </group>
+            <group android:name="blue4" android:translateX="411.5" android:translateY="411.5">
+                <path android:name="blue4_path"
+                    android:pathData=" M0 -395 C218,-395 395,-218 395,0 C395,218 218,395 0,395 C-218,395 -395,218 -395,0 C-395,-218 -218,-395 0,-395c "
+                    android:strokeAlpha="1" android:strokeColor="#e8f0fd"
+                    android:strokeLineCap="round" android:strokeLineJoin="round"
+                    android:strokeWidth="30" />
+            </group>
+        </vector>
+    </aapt:attr>
+    <target android:name="blue1_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathStart"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 l.21,0 c.571,0 .194,.755 .79,1" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue1_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue1">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="rotation"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.829,0.228 0.2,0.915 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue2_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathStart"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 l.21,0 c.571,0 .145,.831 .79,1" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue2_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue2">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="rotation"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.792,0.233 0.2,0.915 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue3_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathStart"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 l.21,0 c.6138,0 .007,.883 .79,1" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue3_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue3">
+        <aapt:attr name="android:animation">
+            <set android:ordering="together">
+                <objectAnimator android:duration="1983" android:propertyName="rotation"
+                    android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
+                    android:valueType="floatType">
+                    <aapt:attr name="android:interpolator">
+                        <pathInterpolator
+                            android:pathData="M0,0 c0.762,0.225 0.2,0.915 1.0,1.0" />
+                    </aapt:attr>
+                </objectAnimator>
+            </set>
+        </aapt:attr>
+    </target>
+    <target android:name="blue4_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathStart"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 l.21,0 c.572,0 0,1 .79,1" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue4_path">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="trimPathEnd"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="1"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.606,0.315 0.2,1 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+    <target android:name="blue4">
+        <aapt:attr name="android:animation">
+            <objectAnimator android:duration="1983" android:propertyName="rotation"
+                android:repeatCount="infinite" android:valueFrom="0" android:valueTo="355"
+                android:valueType="floatType">
+                <aapt:attr name="android:interpolator">
+                    <pathInterpolator android:pathData="M0,0 c0.606,0.172 0.2,0.915 1.0,1.0" />
+                </aapt:attr>
+            </objectAnimator>
+        </aapt:attr>
+    </target>
+</animated-vector>
diff --git a/library/main/res/layout/suw_glif_blank_template_content.xml b/library/main/res/layout/suw_glif_blank_template_content.xml
index ed81126..6d864cd 100644
--- a/library/main/res/layout/suw_glif_blank_template_content.xml
+++ b/library/main/res/layout/suw_glif_blank_template_content.xml
@@ -21,6 +21,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
+    <ViewStub
+        android:id="@+id/suw_layout_sticky_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
     <FrameLayout
         android:id="@+id/suw_layout_content"
         android:layout_width="match_parent"
diff --git a/library/main/res/layout/suw_glif_header.xml b/library/main/res/layout/suw_glif_header.xml
index b090f79..420e989 100644
--- a/library/main/res/layout/suw_glif_header.xml
+++ b/library/main/res/layout/suw_glif_header.xml
@@ -23,10 +23,11 @@
 
     <ImageView
         android:id="@+id/suw_layout_icon"
-        style="@style/SuwGlifIcon"
+        style="?attr/suwGlifIconStyle"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:contentDescription="@null" />
+        android:contentDescription="@null"
+        android:visibility="gone" />
 
     <TextView
         android:id="@+id/suw_layout_title"
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 d0c5cc4..58ca178 100644
--- a/library/main/res/layout/suw_glif_list_template_content.xml
+++ b/library/main/res/layout/suw_glif_list_template_content.xml
@@ -22,6 +22,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
+    <ViewStub
+        android:id="@+id/suw_layout_sticky_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
     <!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
          versions. -->
     <com.android.setupwizardlib.view.StickyHeaderListView
diff --git a/library/main/res/layout/suw_glif_loading_screen.xml b/library/main/res/layout/suw_glif_loading_screen.xml
new file mode 100644
index 0000000..676ab34
--- /dev/null
+++ b/library/main/res/layout/suw_glif_loading_screen.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<com.android.setupwizardlib.GlifLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/setup_wizard_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:ignore="UnusedResources">
+    <!-- Ignore UnusedResources: can be used by clients -->
+
+    <com.android.setupwizardlib.view.FillContentLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1">
+
+        <ProgressBar
+            android:id="@+id/suw_large_progress_bar"
+            style="@style/SuwFourColorIndeterminateProgressBar"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </com.android.setupwizardlib.view.FillContentLayout>
+
+</com.android.setupwizardlib.GlifLayout>
diff --git a/library/main/res/layout/suw_glif_template_content.xml b/library/main/res/layout/suw_glif_template_content.xml
index 0fe35a0..5226f63 100644
--- a/library/main/res/layout/suw_glif_template_content.xml
+++ b/library/main/res/layout/suw_glif_template_content.xml
@@ -22,6 +22,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
+    <ViewStub
+        android:id="@+id/suw_layout_sticky_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
     <!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
          versions. -->
     <com.android.setupwizardlib.view.BottomScrollView
diff --git a/library/main/res/values-en-rCA/strings.xml b/library/main/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..5260500
--- /dev/null
+++ b/library/main/res/values-en-rCA/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+    Copyright (C) 2015 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="suw_next_button_label" msgid="7269625133873553978">"Next"</string>
+    <string name="suw_back_button_label" msgid="1460929053642711025">"Back"</string>
+    <string name="suw_more_button_label" msgid="7769076059705546563">"More"</string>
+</resources>
diff --git a/library/main/res/values-en-rXC/strings.xml b/library/main/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..693af6b
--- /dev/null
+++ b/library/main/res/values-en-rXC/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+    Copyright (C) 2015 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="suw_next_button_label" msgid="7269625133873553978">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‎‏‎‏‏‏‎‎‏‎‏‏‎‎‎‏‎‏‏‎‎‎‎‏‎‏‎‏‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎Next‎‏‎‎‏‎"</string>
+    <string name="suw_back_button_label" msgid="1460929053642711025">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‎‏‏‎‎‏‏‏‎‏‏‏‏‏‎‎‏‎‎‎‏‏‎‏‎‏‏‏‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎Back‎‏‎‎‏‎"</string>
+    <string name="suw_more_button_label" msgid="7769076059705546563">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‎‎‏‎‏‎‎‏‏‎‏‏‎‎‎‎‎‏‏‏‎‎‏‎‎‏‏‏‎‏‏‏‏‎‏‏‎‎‏‎‏‏‏‎‏‎‎‎‎‏‏‎More‎‏‎‎‏‎"</string>
+</resources>
diff --git a/library/main/res/values-v11/styles.xml b/library/main/res/values-v11/styles.xml
new file mode 100644
index 0000000..6903577
--- /dev/null
+++ b/library/main/res/values-v11/styles.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<resources>
+
+    <style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.Holo.ProgressBar.Large" />
+
+</resources>
diff --git a/library/main/res/values-v21/styles.xml b/library/main/res/values-v21/styles.xml
index ab6f887..d2c27f6 100644
--- a/library/main/res/values-v21/styles.xml
+++ b/library/main/res/values-v21/styles.xml
@@ -42,6 +42,16 @@
         <item name="android:fontFamily">sans-serif-medium</item>
     </style>
 
+    <style name="SuwBase.ProgressBarLarge" parent="@android:style/Widget.Material.ProgressBar.Large" />
+
+    <style name="SuwFourColorIndeterminateProgressBar" parent="SuwBase.ProgressBarLarge">
+        <item name="android:layout_gravity">center</item>
+        <item name="android:indeterminate">true</item>
+        <item name="android:indeterminateDrawable">@drawable/suw_fourcolor_progress_bar</item>
+        <item name="android:indeterminateTint">@null</item>
+        <item name="android:indeterminateTintMode">@null</item>
+    </style>
+
     <!-- Items styles -->
 
     <style name="SuwItemContainer">
@@ -83,4 +93,11 @@
         <item name="suwNavBarButtonBackground">@drawable/suw_navbar_btn_bg</item>
     </style>
 
+    <style name="SuwEditText" parent="@android:style/Widget.Material.EditText">
+        <item name="android:background">@drawable/suw_edittext_bg</item>
+        <item name="android:minHeight">@dimen/suw_edit_text_min_height</item>
+        <item name="android:paddingLeft">@dimen/suw_edit_text_padding_horizontal</item>
+        <item name="android:paddingRight">@dimen/suw_edit_text_padding_horizontal</item>
+    </style>
+
 </resources>
diff --git a/library/main/res/values-v22/styles.xml b/library/main/res/values-v22/styles.xml
new file mode 100644
index 0000000..7d2d219
--- /dev/null
+++ b/library/main/res/values-v22/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2017 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<resources>
+
+    <style name="SuwAlertDialogTheme" parent="android:Theme.DeviceDefault.Dialog.Alert" />
+
+    <style name="SuwAlertDialogTheme.Light" parent="android:Theme.DeviceDefault.Light.Dialog.Alert" />
+</resources>
\ No newline at end of file
diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml
index 36d5fb7..b3fcfe9 100644
--- a/library/main/res/values/attrs.xml
+++ b/library/main/res/values/attrs.xml
@@ -20,6 +20,7 @@
     <!-- Theme attributes -->
     <attr name="suwLayoutTheme" format="reference" />
     <attr name="suwMarginSides" format="dimension|reference" />
+    <attr name="suwEditTextBackgroundColor" format="color" />
 
     <!-- Subset of values in "gravity" in frameworks/base/core/res/res/values/attrs.xml. Only
          horizontal values are listed here as the header does not support vertical gravity. -->
@@ -37,13 +38,17 @@
         <!-- Push object to the end of its container, not changing its size. -->
         <flag name="end" value="0x00800005" />
     </attr>
+    <attr name="suwGlifIconStyle" format="reference" />
 
+    <attr name="suwButtonAllCaps" format="boolean" />
+    <attr name="suwButtonCornerRadius" format="dimension" />
+    <attr name="suwButtonFontFamily" format="string|reference" />
     <attr name="suwCardBackground" format="color|reference" />
-    <attr name="suwFillContentLayoutStyle" format="reference" />
     <attr name="suwDividerCondition">
         <enum name="either" value="0" />
         <enum name="both" value="1" />
     </attr>
+    <attr name="suwFillContentLayoutStyle" format="reference" />
     <attr name="suwListItemIconColor" format="color" />
     <attr name="suwNavBarBackgroundColor" format="color" />
     <attr name="suwNavBarButtonBackground" format="color|reference" />
@@ -102,6 +107,8 @@
         <attr name="suwBackgroundBaseColor" format="color" />
         <attr name="suwColorPrimary" />
         <attr name="suwFooter" format="reference" />
+        <attr name="suwLayoutFullscreen" format="boolean" />
+        <attr name="suwStickyHeader" format="reference" />
     </declare-styleable>
 
     <declare-styleable name="SuwStatusBarBackgroundLayout">
diff --git a/library/main/res/values/colors.xml b/library/main/res/values/colors.xml
index cd57a8a..005d1c6 100644
--- a/library/main/res/values/colors.xml
+++ b/library/main/res/values/colors.xml
@@ -21,6 +21,8 @@
 
     <color name="suw_color_accent_dark">#ff448aff</color>
     <color name="suw_color_accent_light">#ff3367d6</color>
+    <color name="suw_color_background_dark">#ff303030</color>
+    <color name="suw_color_background_light">#fffafafa</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">#b3ffffff</color>
@@ -40,7 +42,11 @@
     <!-- GLIF colors -->
     <color name="suw_color_accent_glif_dark">#ff4285f4</color>
     <color name="suw_color_accent_glif_light">#ff4285f4</color>
+    <color name="suw_color_accent_glif_v3">#ff1a73e8</color>
     <color name="suw_glif_background_color_dark">#ff000000</color>
     <color name="suw_glif_background_color_light">#ffffffff</color>
+    <color name="suw_glif_edit_text_bg_light_color">#fff1f3f4</color>
+    <color name="suw_glif_v3_nav_bar_color_light">#ffffffff</color>
+    <color name="suw_glif_v3_nav_bar_divider_color_light">#1f000000</color>
 
 </resources>
diff --git a/library/main/res/values/config.xml b/library/main/res/values/config.xml
index a81b177..c11bf41 100644
--- a/library/main/res/values/config.xml
+++ b/library/main/res/values/config.xml
@@ -27,4 +27,8 @@
          ButtonBarLayout -->
     <item name="suw_original_weight" type="id" />
 
+    <!-- Secondary font for use with headings, title, and other non-body text -->
+    <string name="suwFontSecondary" translatable="false">google-sans</string>
+    <string name="suwFontSecondaryMedium" translatable="false">google-sans-medium</string>
+
 </resources>
diff --git a/library/main/res/values/dimens.xml b/library/main/res/values/dimens.xml
index f29484a..1a8b516 100644
--- a/library/main/res/values/dimens.xml
+++ b/library/main/res/values/dimens.xml
@@ -20,16 +20,21 @@
     <!-- General -->
     <dimen name="suw_layout_margin_sides">40dp</dimen>
 
+    <dimen name="suw_glif_button_corner_radius">2dp</dimen>
     <!-- Calculated by (suw_glif_margin_sides - 4dp internal padding of button) -->
     <dimen name="suw_glif_button_margin_end">20dp</dimen>
     <!-- Calculated by (suw_glif_margin_sides - suw_glif_button_padding) -->
     <dimen name="suw_glif_button_margin_start">8dp</dimen>
     <dimen name="suw_glif_button_padding">16dp</dimen>
+    <!-- Negative of suw_glif_button_padding -->
+    <dimen name="suw_glif_negative_button_padding">-16dp</dimen>
     <dimen name="suw_glif_footer_padding_vertical">8dp</dimen>
-    <dimen name="suw_glif_footer_min_height">80dp</dimen>
+    <dimen name="suw_glif_footer_min_height">72dp</dimen>
     <dimen name="suw_glif_margin_sides">24dp</dimen>
     <dimen name="suw_glif_margin_top">48dp</dimen>
 
+    <dimen name="suw_glif_v3_button_corner_radius">4dp</dimen>
+
     <!-- Content styles -->
     <dimen name="suw_check_box_line_spacing_extra">4sp</dimen>
     <dimen name="suw_check_box_margin_bottom">12dp</dimen>
@@ -135,4 +140,8 @@
     <dimen name="suw_progress_bar_margin_vertical">-7dp</dimen>
     <dimen name="suw_glif_progress_bar_margin_vertical">7dp</dimen>
 
+    <!-- Edit Text dimensions -->
+    <dimen name="suw_edit_text_min_height">56dp</dimen>
+    <dimen name="suw_edit_text_padding_horizontal">12dp</dimen>
+
 </resources>
diff --git a/library/main/res/values/styles.xml b/library/main/res/values/styles.xml
index bd3b60f..fa2a080 100644
--- a/library/main/res/values/styles.xml
+++ b/library/main/res/values/styles.xml
@@ -30,11 +30,11 @@
         <item name="suwDividerInsetStartNoIcon">?attr/suwMarginSides</item>
         <item name="suwGlifHeaderGravity">center_horizontal</item>
         <item name="suwScrollIndicators">top|bottom</item>
+        <item name="suwEditTextBackgroundColor">@color/suw_glif_edit_text_bg_light_color</item> <!-- TODO: Change color -->
+        <item name="android:editTextStyle">@style/SuwEditText</item>
+        <item name="android:alertDialogTheme" tools:targetApi="honeycomb">@style/SuwAlertDialogTheme</item>
     </style>
 
-    <!-- Deprecated. Use SuwThemeGlifV2 instead -->
-    <style name="SuwThemeGlifPixel" parent="SuwThemeGlifV2" />
-
     <style name="SuwThemeGlifV2.Light" parent="SuwThemeGlif.Light">
         <item name="android:colorBackground">@color/suw_glif_background_color_light</item>
         <item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
@@ -46,11 +46,11 @@
         <item name="suwDividerInsetStartNoIcon">?attr/suwMarginSides</item>
         <item name="suwGlifHeaderGravity">center_horizontal</item>
         <item name="suwScrollIndicators">top|bottom</item>
+        <item name="suwEditTextBackgroundColor">@color/suw_glif_edit_text_bg_light_color</item>
+        <item name="android:editTextStyle">@style/SuwEditText</item>
+        <item name="android:alertDialogTheme" tools:targetApi="honeycomb">@style/SuwAlertDialogTheme.Light</item>
     </style>
 
-    <!-- Deprecated. Use SuwThemeGlifV2.Light instead -->
-    <style name="SuwThemeGlifPixel.Light" parent="SuwThemeGlifV2.Light" />
-
     <style name="Animation.SuwWindowAnimation" parent="@android:style/Animation.Activity">
         <item name="android:activityOpenEnterAnimation">@anim/suw_slide_next_in</item>
         <item name="android:activityOpenExitAnimation">@anim/suw_slide_next_out</item>
@@ -86,11 +86,10 @@
         <item name="android:textAlignment" tools:targetApi="jelly_bean_mr1">gravity</item>
     </style>
 
-    <style name="TextAppearance.SuwDescription.Light" parent="TextAppearance.SuwDescription">
-        <item name="android:fontFamily" tools:ignore="NewApi">sans-serif-light</item>
-    </style>
-
-    <style name="TextAppearance.SuwDescription.Secondary" parent="TextAppearance.SuwDescription">
+    <!-- Ignore UnusedResources: Used by clients -->
+    <style name="TextAppearance.SuwDescription.Secondary"
+        parent="TextAppearance.SuwDescription"
+        tools:ignore="UnusedResources">
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
 
@@ -196,14 +195,19 @@
         <item name="android:buttonStyle">@style/SuwGlifButton.Tertiary</item>
         <item name="android:theme">@style/SuwGlifButton.Tertiary</item>
 
-        <item name="android:background">@null</item>
         <item name="android:fontFamily" tools:targetApi="jelly_bean">sans-serif</item>
         <item name="android:layout_gravity">?attr/suwGlifHeaderGravity</item>
-        <item name="android:padding">0dp</item>
+        <item name="android:layout_marginLeft">@dimen/suw_glif_negative_button_padding</item>
+        <item name="android:layout_marginRight">@dimen/suw_glif_negative_button_padding</item>
+        <!-- Always lowercase instead of reading attr/suwButtonAllCaps, since this is a tertiary
+             button -->
         <item name="android:textAllCaps" tools:targetApi="ice_cream_sandwich">false</item>
     </style>
 
-    <style name="SuwGlifButton.Tertiary" parent="SuwGlifButton.BaseTertiary" />
+    <!-- Ignore UnusedResources: used by clients -->
+    <style name="SuwGlifButton.Tertiary"
+        parent="SuwGlifButton.BaseTertiary"
+        tools:ignore="UnusedResources" />
 
     <!-- The start and end paddings are asymmetric because start buttons are borderless buttons
          which aligns the text label. -->
@@ -266,7 +270,7 @@
         <item name="android:layout_marginLeft">?attr/suwMarginSides</item>
         <item name="android:layout_marginRight">?attr/suwMarginSides</item>
         <item name="android:layout_marginTop">@dimen/suw_glif_header_title_margin_top</item>
-        <item name="android:fontFamily" tools:targetApi="jelly_bean">google-sans</item>
+        <item name="android:fontFamily" tools:targetApi="jelly_bean">@string/suwFontSecondary</item>
         <item name="android:textAlignment" tools:targetApi="jelly_bean_mr1">gravity</item>
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
@@ -309,4 +313,9 @@
         <item name="suwNavBarTextColor">?android:attr/textColorPrimary</item>
     </style>
 
+
+    <style name="SuwEditText" parent="@android:style/Widget.EditText">
+        <item name="android:minHeight">@dimen/suw_edit_text_min_height</item>
+    </style>
+
 </resources>
diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java
index f4d52a5..e1d9d70 100644
--- a/library/main/src/com/android/setupwizardlib/GlifLayout.java
+++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java
@@ -77,6 +77,8 @@
     @Nullable
     private ColorStateList mBackgroundBaseColor;
 
+    private boolean mLayoutFullscreen = true;
+
     public GlifLayout(Context context) {
         this(context, 0, 0);
     }
@@ -139,7 +141,18 @@
             inflateFooter(footer);
         }
 
+        final int stickyHeader = a.getResourceId(R.styleable.SuwGlifLayout_suwStickyHeader, 0);
+        if (stickyHeader != 0) {
+            inflateStickyHeader(stickyHeader);
+        }
+
+        mLayoutFullscreen = a.getBoolean(R.styleable.SuwGlifLayout_suwLayoutFullscreen, true);
+
         a.recycle();
+
+        if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && mLayoutFullscreen) {
+            setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+        }
     }
 
     @Override
@@ -160,17 +173,31 @@
 
     /**
      * Sets the footer of the layout, which is at the bottom of the content area outside the
-     * scrolling container. The footer can only be inflated once per layout.
+     * scrolling container. The footer can only be inflated once per instance of this layout.
      *
      * @param footer The layout to be inflated as footer.
      * @return The root of the inflated footer view.
      */
     public View inflateFooter(@LayoutRes int footer) {
-        ViewStub footerStub = (ViewStub) findManagedViewById(R.id.suw_layout_footer);
+        ViewStub footerStub = findManagedViewById(R.id.suw_layout_footer);
         footerStub.setLayoutResource(footer);
         return footerStub.inflate();
     }
 
+    /**
+     * Sets the sticky header (i.e. header that doesn't scroll) of the layout, which is at the top
+     * of the content area outside of the scrolling container. The header can only be inflated once
+     * per instance of this layout.
+     *
+     * @param header The layout to be inflated as the header.
+     * @return The root of the inflated header view.
+     */
+    public View inflateStickyHeader(@LayoutRes int header) {
+        ViewStub stickyHeaderStub = findManagedViewById(R.id.suw_layout_sticky_header);
+        stickyHeaderStub.setLayoutResource(header);
+        return stickyHeaderStub.inflate();
+    }
+
     public ScrollView getScrollView() {
         final View view = findManagedViewById(R.id.suw_scroll_view);
         return view instanceof ScrollView ? (ScrollView) view : null;
@@ -280,9 +307,6 @@
                 patternBg.setBackgroundDrawable(background);
             }
         }
-        if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
-            setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-        }
     }
 
     public boolean isProgressBarShown() {
diff --git a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
index 51c1a49..c1d968a 100644
--- a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
+++ b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java
@@ -23,7 +23,6 @@
 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.PixelFormat;
@@ -96,7 +95,6 @@
 
     private int mColor;
     private Paint mTempPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-    private ColorFilter mColorFilter;
 
     public GlifPatternDrawable(int color) {
         setColor(color);
@@ -140,17 +138,10 @@
         canvas.clipRect(bounds);
 
         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.drawColor(Color.BLACK);
+        mTempPaint.setColor(Color.WHITE);
+        canvas.drawBitmap(bitmap, 0, 0, mTempPaint);
+        canvas.drawColor(mColor);
 
         canvas.restore();
     }
@@ -299,12 +290,6 @@
         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();
     }
 
diff --git a/library/main/src/com/android/setupwizardlib/TemplateLayout.java b/library/main/src/com/android/setupwizardlib/TemplateLayout.java
index 771592f..d270091 100644
--- a/library/main/src/com/android/setupwizardlib/TemplateLayout.java
+++ b/library/main/src/com/android/setupwizardlib/TemplateLayout.java
@@ -103,7 +103,9 @@
      * by this view but not currently added to the view hierarchy. e.g. recycler view or list view
      * headers that are not currently shown.
      */
-    public View findManagedViewById(int id) {
+    // Returning generic type is the common pattern used for findViewBy* methods
+    @SuppressWarnings("TypeParameterUnusedInFormals")
+    public <T extends View> T findManagedViewById(int id) {
         return findViewById(id);
     }
 
diff --git a/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java b/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java
index 8325232..f438691 100644
--- a/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java
+++ b/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java
@@ -39,6 +39,7 @@
     private final View mView;
     private final OnConsecutiveTapsListener mListener;
     private final int mConsecutiveTapTouchSlopSquare;
+    private final int mConsecutiveTapTimeout;
 
     private int mConsecutiveTapsCounter = 0;
     private MotionEvent mPreviousTapEvent;
@@ -54,6 +55,7 @@
         mView = view;
         int doubleTapSlop = ViewConfiguration.get(mView.getContext()).getScaledDoubleTapSlop();
         mConsecutiveTapTouchSlopSquare = doubleTapSlop * doubleTapSlop;
+        mConsecutiveTapTimeout = ViewConfiguration.getDoubleTapTimeout();
     }
 
     /**
@@ -109,6 +111,8 @@
 
         double deltaX = mPreviousTapEvent.getX() - currentTapEvent.getX();
         double deltaY = mPreviousTapEvent.getY() - currentTapEvent.getY();
-        return (deltaX * deltaX + deltaY * deltaY <= mConsecutiveTapTouchSlopSquare);
+        long deltaTime = currentTapEvent.getEventTime() - mPreviousTapEvent.getEventTime();
+        return (deltaX * deltaX + deltaY * deltaY <= mConsecutiveTapTouchSlopSquare)
+                && deltaTime < mConsecutiveTapTimeout;
     }
 }
diff --git a/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java b/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java
index 55bbe75..06ce4ac 100644
--- a/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java
+++ b/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java
@@ -83,6 +83,7 @@
         return mVisible;
     }
 
+    @Override
     public int getViewId() {
         return getId();
     }
diff --git a/library/main/src/com/android/setupwizardlib/span/LinkSpan.java b/library/main/src/com/android/setupwizardlib/span/LinkSpan.java
index a5f0424..26a3d16 100644
--- a/library/main/src/com/android/setupwizardlib/span/LinkSpan.java
+++ b/library/main/src/com/android/setupwizardlib/span/LinkSpan.java
@@ -21,10 +21,13 @@
 import android.graphics.Typeface;
 import android.os.Build;
 import android.support.annotation.Nullable;
+import android.text.Selection;
+import android.text.Spannable;
 import android.text.TextPaint;
 import android.text.style.ClickableSpan;
 import android.util.Log;
 import android.view.View;
+import android.widget.TextView;
 
 /**
  * A clickable span that will listen for click events and send it back to the context. To use this
@@ -86,11 +89,19 @@
     public void onClick(View view) {
         if (dispatchClick(view)) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                // Prevent the touch event from bubbling up to the parent views.
                 view.cancelPendingInputEvents();
             }
         } else {
             Log.w(TAG, "Dropping click event. No listener attached.");
         }
+        if (view instanceof TextView) {
+            // Remove the highlight effect when the click happens by clearing the selection
+            CharSequence text = ((TextView) view).getText();
+            if (text instanceof Spannable) {
+                Selection.setSelection((Spannable) text, 0);
+            }
+        }
     }
 
     private boolean dispatchClick(View view) {
diff --git a/library/main/src/com/android/setupwizardlib/template/IconMixin.java b/library/main/src/com/android/setupwizardlib/template/IconMixin.java
index 46c23f0..5386c92 100644
--- a/library/main/src/com/android/setupwizardlib/template/IconMixin.java
+++ b/library/main/src/com/android/setupwizardlib/template/IconMixin.java
@@ -19,7 +19,9 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
 import android.util.AttributeSet;
+import android.view.View;
 import android.widget.ImageView;
 
 import com.android.setupwizardlib.R;
@@ -44,8 +46,8 @@
         final TypedArray a =
                 context.obtainStyledAttributes(attrs, R.styleable.SuwIconMixin, defStyleAttr, 0);
 
-        final Drawable icon = a.getDrawable(R.styleable.SuwIconMixin_android_icon);
-        if (icon != null) {
+        final @DrawableRes int icon = a.getResourceId(R.styleable.SuwIconMixin_android_icon, 0);
+        if (icon != 0) {
             setIcon(icon);
         }
 
@@ -61,6 +63,22 @@
         final ImageView iconView = getView();
         if (iconView != null) {
             iconView.setImageDrawable(icon);
+            iconView.setVisibility(icon != null ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    /**
+     * Sets the icon on this layout. The icon can also be set in XML using {@code android:icon}.
+     *
+     * @param icon A drawable icon resource.
+     */
+    public void setIcon(@DrawableRes int icon) {
+        final ImageView iconView = getView();
+        if (iconView != null) {
+            // Note: setImageResource on the ImageView is overridden in AppCompatImageView for
+            // support lib users, which enables vector drawable compat to work on versions pre-L.
+            iconView.setImageResource(icon);
+            iconView.setVisibility(icon != 0 ? View.VISIBLE : View.GONE);
         }
     }
 
@@ -73,6 +91,24 @@
     }
 
     /**
+     * Sets the content description of the icon view
+     */
+    public void setContentDescription(CharSequence description) {
+        final ImageView iconView = getView();
+        if (iconView != null) {
+            iconView.setContentDescription(description);
+        }
+    }
+
+    /**
+     * @return The content description of the icon view
+     */
+    public CharSequence getContentDescription() {
+        final ImageView iconView = getView();
+        return iconView != null ? iconView.getContentDescription() : null;
+    }
+
+    /**
      * @return The ImageView responsible for displaying the icon.
      */
     protected ImageView getView() {
diff --git a/library/main/src/com/android/setupwizardlib/util/Partner.java b/library/main/src/com/android/setupwizardlib/util/Partner.java
index 67f5546..3a603ee 100644
--- a/library/main/src/com/android/setupwizardlib/util/Partner.java
+++ b/library/main/src/com/android/setupwizardlib/util/Partner.java
@@ -27,6 +27,7 @@
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.support.annotation.AnyRes;
+import android.support.annotation.ColorRes;
 import android.support.annotation.DrawableRes;
 import android.support.annotation.StringRes;
 import android.support.annotation.VisibleForTesting;
@@ -76,6 +77,15 @@
     }
 
     /**
+     * Convenience method to get color from partner overlay, or if not available, the color from
+     * the original context.
+     */
+    public static int getColor(Context context, @ColorRes int id) {
+        final ResourceEntry resourceEntry = getResourceEntry(context, id);
+        return resourceEntry.resources.getColor(resourceEntry.id);
+    }
+
+    /**
      * Convenience method to get a CharSequence from partner overlay, or if not available, the text
      * from the original context.
      */
diff --git a/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java
index 1c5f3d3..b31e82e 100644
--- a/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java
+++ b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java
@@ -24,6 +24,7 @@
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 import android.os.Handler;
+import android.support.annotation.RequiresPermission;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -183,12 +184,25 @@
         }
     }
 
+    /**
+     * Sets whether the back button on the software navigation bar is visible. This only works if
+     * you have the STATUS_BAR permission. Otherwise framework will filter out this flag and this
+     * method call will not have any effect.
+     *
+     * <p>IMPORTANT: Do not assume that users have no way to go back when the back button is hidden.
+     * Many devices have physical back buttons, and accessibility services like TalkBack may have
+     * gestures mapped to back. Please use onBackPressed, onKeyDown, or other similar ways to
+     * make sure back button events are still handled (or ignored) properly.
+     */
+    @RequiresPermission("android.permission.STATUS_BAR")
     public static void setBackButtonVisible(final Window window, final boolean visible) {
         if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
             if (visible) {
                 removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
+                removeImmersiveFlagsFromDecorView(window, STATUS_BAR_DISABLE_BACK);
             } else {
                 addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
+                addImmersiveFlagsToDecorView(window, STATUS_BAR_DISABLE_BACK);
             }
         }
     }
@@ -217,7 +231,7 @@
      * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view
      * instead of the window.
      */
-    @TargetApi(VERSION_CODES.LOLLIPOP)
+    @TargetApi(VERSION_CODES.HONEYCOMB)
     private static void addImmersiveFlagsToDecorView(final Window window, final int vis) {
         getDecorView(window, new OnDecorViewInstalledListener() {
             @Override
@@ -227,7 +241,7 @@
         });
     }
 
-    @TargetApi(VERSION_CODES.LOLLIPOP)
+    @TargetApi(VERSION_CODES.HONEYCOMB)
     private static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) {
         getDecorView(window, new OnDecorViewInstalledListener() {
             @Override
diff --git a/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java
index a93694c..cf9ddac 100644
--- a/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java
+++ b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java
@@ -27,6 +27,8 @@
 
 import com.android.setupwizardlib.R;
 
+import java.util.Arrays;
+
 public class WizardManagerHelper {
 
     private static final String ACTION_NEXT = "com.android.wizard.NEXT";
@@ -45,6 +47,8 @@
     static final String EXTRA_IS_FIRST_RUN = "firstRun";
     @VisibleForTesting
     static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup";
+    @VisibleForTesting
+    static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup";
 
     public static final String EXTRA_THEME = "theme";
     public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
@@ -76,22 +80,22 @@
     public static final String THEME_GLIF_V2 = "glif_v2";
 
     /**
-     * @deprecated Use {@link #THEME_GLIF_V2} instead.
-     */
-    @Deprecated
-    public static final String THEME_GLIF_PIXEL = THEME_GLIF_V2;
-
-    /**
      * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
      * setup wizard for O DR.
      */
     public static final String THEME_GLIF_V2_LIGHT = "glif_v2_light";
 
     /**
-     * @deprecated Use {@link #THEME_GLIF_V2_LIGHT} instead.
+     * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the dark variant of the
+     * theme used in setup wizard for P.
      */
-    @Deprecated
-    public static final String THEME_GLIF_PIXEL_LIGHT = THEME_GLIF_V2_LIGHT;
+    public static final String THEME_GLIF_V3 = "glif_v3";
+
+    /**
+     * Passed in a setup wizard intent as {@link #EXTRA_THEME}. This is the default theme used in
+     * setup wizard for P.
+     */
+    public static final String THEME_GLIF_V3_LIGHT = "glif_v3_light";
 
     /**
      * Get an intent that will invoke the next step of setup wizard.
@@ -140,13 +144,14 @@
      */
     public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) {
         dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
-        dstIntent.putExtra(EXTRA_THEME, srcIntent.getStringExtra(EXTRA_THEME));
-        dstIntent.putExtra(EXTRA_IS_FIRST_RUN,
-                srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false));
-        dstIntent.putExtra(EXTRA_IS_DEFERRED_SETUP,
-                srcIntent.getBooleanExtra(EXTRA_IS_DEFERRED_SETUP, false));
-        dstIntent.putExtra(EXTRA_SCRIPT_URI, srcIntent.getStringExtra(EXTRA_SCRIPT_URI));
-        dstIntent.putExtra(EXTRA_ACTION_ID, srcIntent.getStringExtra(EXTRA_ACTION_ID));
+        for (String key : Arrays.asList(
+                EXTRA_IS_FIRST_RUN, EXTRA_IS_DEFERRED_SETUP, EXTRA_IS_PRE_DEFERRED_SETUP)) {
+            dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false));
+        }
+
+        for (String key : Arrays.asList(EXTRA_THEME, EXTRA_SCRIPT_URI, EXTRA_ACTION_ID)) {
+            dstIntent.putExtra(key, srcIntent.getStringExtra(key));
+        }
     }
 
     /**
@@ -213,6 +218,18 @@
     }
 
     /**
+     * Checks whether an intent is running in "pre-deferred" setup wizard flow.
+     *
+     * @param originalIntent The original intent that was used to start the step, usually via
+     *                       {@link android.app.Activity#getIntent()}.
+     * @return true if the intent passed in was running in "pre-deferred" setup wizard.
+     */
+    public static boolean isPreDeferredSetupWizard(Intent originalIntent) {
+        return originalIntent != null
+                && originalIntent.getBooleanExtra(EXTRA_IS_PRE_DEFERRED_SETUP, false);
+    }
+
+    /**
      * Checks the intent whether the extra indicates that the light theme should be used or not. If
      * the theme is not specified in the intent, or the theme specified is unknown, the value def
      * will be returned.
@@ -236,10 +253,12 @@
      */
     public static boolean isLightTheme(String theme, boolean def) {
         if (THEME_HOLO_LIGHT.equals(theme) || THEME_MATERIAL_LIGHT.equals(theme)
-                || THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)) {
+                || THEME_GLIF_LIGHT.equals(theme) || THEME_GLIF_V2_LIGHT.equals(theme)
+                || THEME_GLIF_V3_LIGHT.equals(theme)) {
             return true;
         } else if (THEME_HOLO.equals(theme) || THEME_MATERIAL.equals(theme)
-                || THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)) {
+                || THEME_GLIF.equals(theme) || THEME_GLIF_V2.equals(theme)
+                || THEME_GLIF_V3.equals(theme)) {
             return false;
         } else {
             return def;
@@ -284,6 +303,10 @@
     public static @StyleRes int getThemeRes(String theme, @StyleRes int defaultTheme) {
         if (theme != null) {
             switch (theme) {
+                case THEME_GLIF_V3_LIGHT:
+                    return R.style.SuwThemeGlifV3_Light;
+                case THEME_GLIF_V3:
+                    return R.style.SuwThemeGlifV3;
                 case THEME_GLIF_V2_LIGHT:
                     return R.style.SuwThemeGlifV2_Light;
                 case THEME_GLIF_V2:
diff --git a/library/main/src/com/android/setupwizardlib/view/CheckableLinearLayout.java b/library/main/src/com/android/setupwizardlib/view/CheckableLinearLayout.java
index 3c678f8..bd0aead 100644
--- a/library/main/src/com/android/setupwizardlib/view/CheckableLinearLayout.java
+++ b/library/main/src/com/android/setupwizardlib/view/CheckableLinearLayout.java
@@ -58,6 +58,10 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
+    {
+        setFocusable(true);
+    }
+
     @Override
     protected int[] onCreateDrawableState(int extraSpace) {
         if (mChecked) {
diff --git a/library/main/src/com/android/setupwizardlib/view/Illustration.java b/library/main/src/com/android/setupwizardlib/view/Illustration.java
index b576174..c6968f8 100644
--- a/library/main/src/com/android/setupwizardlib/view/Illustration.java
+++ b/library/main/src/com/android/setupwizardlib/view/Illustration.java
@@ -137,7 +137,8 @@
         if (mAspectRatio != 0.0f) {
             int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
             int illustrationHeight = (int) (parentWidth / mAspectRatio);
-            illustrationHeight -= illustrationHeight % mBaselineGridSize;
+            illustrationHeight =
+                    (int) (illustrationHeight - (illustrationHeight % mBaselineGridSize));
             setPadding(0, illustrationHeight, 0, 0);
         }
         if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
diff --git a/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java b/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java
index 9c79eb5..e5c2fb1 100644
--- a/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java
+++ b/library/main/src/com/android/setupwizardlib/view/IllustrationVideoView.java
@@ -23,9 +23,11 @@
 import android.graphics.drawable.Animatable;
 import android.media.MediaPlayer;
 import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
 import android.support.annotation.RawRes;
 import android.support.annotation.VisibleForTesting;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.view.Surface;
 import android.view.TextureView;
 import android.view.View;
@@ -51,8 +53,11 @@
         MediaPlayer.OnSeekCompleteListener,
         MediaPlayer.OnInfoListener {
 
+    private static final String TAG = "IllustrationVideoView";
+
     protected float mAspectRatio = 1.0f; // initial guess until we know
 
+    @Nullable // Can be null when media player fails to initialize
     protected MediaPlayer mMediaPlayer;
 
     private @RawRes int mVideoResId = 0;
@@ -129,15 +134,20 @@
 
         mMediaPlayer = MediaPlayer.create(getContext(), mVideoResId);
 
-        mMediaPlayer.setSurface(mSurface);
-        mMediaPlayer.setOnPreparedListener(this);
-        mMediaPlayer.setOnSeekCompleteListener(this);
-        mMediaPlayer.setOnInfoListener(this);
+        if (mMediaPlayer != null) {
+            mMediaPlayer.setSurface(mSurface);
+            mMediaPlayer.setOnPreparedListener(this);
+            mMediaPlayer.setOnSeekCompleteListener(this);
+            mMediaPlayer.setOnInfoListener(this);
 
-        float aspectRatio = (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth();
-        if (mAspectRatio != aspectRatio) {
-            mAspectRatio = aspectRatio;
-            requestLayout();
+            float aspectRatio =
+                    (float) mMediaPlayer.getVideoHeight() / mMediaPlayer.getVideoWidth();
+            if (mAspectRatio != aspectRatio) {
+                mAspectRatio = aspectRatio;
+                requestLayout();
+            }
+        } else {
+            Log.wtf(TAG, "Unable to initialize media player for video view");
         }
         if (getWindowVisibility() == View.VISIBLE) {
             start();
diff --git a/library/main/src/com/android/setupwizardlib/view/TouchableMovementMethod.java b/library/main/src/com/android/setupwizardlib/view/TouchableMovementMethod.java
new file mode 100644
index 0000000..10e91f4
--- /dev/null
+++ b/library/main/src/com/android/setupwizardlib/view/TouchableMovementMethod.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.text.Selection;
+import android.text.Spannable;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
+import android.view.MotionEvent;
+import android.widget.TextView;
+
+/**
+ * A movement method that tracks the last result of whether touch events are handled. This is
+ * used to patch the return value of {@link TextView#onTouchEvent} so that it consumes the touch
+ * events only when the movement method says the event is consumed.
+ */
+public interface TouchableMovementMethod {
+
+    /**
+     * @return The last touch event received in {@link MovementMethod#onTouchEvent}
+     */
+    MotionEvent getLastTouchEvent();
+
+    /**
+     * @return The return value of the last {@link MovementMethod#onTouchEvent}, or whether the
+     * last touch event should be considered handled by the text view
+     */
+    boolean isLastTouchEventHandled();
+
+    /**
+     * An extension of LinkMovementMethod that tracks whether the event is handled when it is
+     * touched.
+     */
+    class TouchableLinkMovementMethod extends LinkMovementMethod
+            implements TouchableMovementMethod {
+
+        public static TouchableLinkMovementMethod getInstance() {
+            return new TouchableLinkMovementMethod();
+        }
+
+        boolean mLastEventResult = false;
+        MotionEvent mLastEvent;
+
+        @Override
+        public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+            mLastEvent = event;
+            boolean result = super.onTouchEvent(widget, buffer, event);
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                // Unfortunately, LinkMovementMethod extends ScrollMovementMethod, and it always
+                // consume the down event. So here we use the selection instead as a hint of whether
+                // the down event landed on a link.
+                mLastEventResult = Selection.getSelectionStart(buffer) != -1;
+            } else {
+                mLastEventResult = result;
+            }
+            return result;
+        }
+
+        @Override
+        public MotionEvent getLastTouchEvent() {
+            return mLastEvent;
+        }
+
+        @Override
+        public boolean isLastTouchEventHandled() {
+            return mLastEventResult;
+        }
+    }
+}
diff --git a/library/platform/res/values-v23/styles.xml b/library/platform/res/values-v27/styles.xml
similarity index 77%
rename from library/platform/res/values-v23/styles.xml
rename to library/platform/res/values-v27/styles.xml
index 9fff5f1..6e36919 100644
--- a/library/platform/res/values-v23/styles.xml
+++ b/library/platform/res/values-v27/styles.xml
@@ -15,9 +15,10 @@
     limitations under the License.
 -->
 
+<!-- TODO(yukl): Bump this file to v28 once we can properly test that -->
 <!-- These styles are only included in the platform build, to make sure that they do not
      override the corresponding styles in the compatibility build. -->
-<resources>
+<resources xmlns:tools="http://schemas.android.com/tools">
 
     <!-- General styles -->
 
@@ -26,6 +27,7 @@
          Theme.AppCompat. -->
     <style name="SuwThemeMaterial" parent="android:Theme.Material.NoActionBar">
         <item name="android:colorAccent">@color/suw_color_accent_dark</item>
+        <item name="android:colorBackground">@color/suw_color_background_dark</item>
         <item name="android:indeterminateTint">@color/suw_progress_bar_color_dark</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode">src_in</item>
@@ -34,12 +36,14 @@
         <item name="android:listPreferredItemPaddingStart">?attr/suwMarginSides</item>
         <item name="android:navigationBarColor">@android:color/black</item>
         <item name="android:statusBarColor">@android:color/black</item>
-        <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
+        <item name="android:textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</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="suwButtonAllCaps">true</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwCardBackground">@drawable/suw_card_bg</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
@@ -54,6 +58,7 @@
 
     <style name="SuwThemeMaterial.Light" parent="android:Theme.Material.Light.NoActionBar">
         <item name="android:colorAccent">@color/suw_color_accent_light</item>
+        <item name="android:colorBackground">@color/suw_color_background_light</item>
         <item name="android:indeterminateTint">@color/suw_progress_bar_color_light</item>
         <!-- Specify the indeterminateTintMode to work around a bug in Lollipop -->
         <item name="android:indeterminateTintMode">src_in</item>
@@ -62,12 +67,14 @@
         <item name="android:listPreferredItemPaddingStart">?attr/suwMarginSides</item>
         <item name="android:navigationBarColor">@android:color/black</item>
         <item name="android:statusBarColor">@android:color/black</item>
-        <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.Material.Body1</item>
+        <item name="android:textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item>
         <item name="android:textColorLink">@color/suw_link_color_light</item>
         <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item>
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwCardBackground">@drawable/suw_card_bg</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
@@ -84,7 +91,7 @@
     <style name="SuwThemeGlif" parent="android:Theme.Material.NoActionBar">
         <item name="android:colorAccent">@color/suw_color_accent_glif_dark</item>
         <item name="android:colorBackground">@color/suw_glif_background_color_dark</item>
-        <item name="android:colorPrimary">@color/suw_color_accent_glif_dark</item>
+        <item name="android:colorPrimary">?android:attr/colorAccent</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>
@@ -100,12 +107,16 @@
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwColorPrimary">?android:attr/colorPrimary</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
         <item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
         <item name="suwGlifHeaderGravity">start</item>
+        <item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
         <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item>
@@ -116,7 +127,7 @@
     <style name="SuwThemeGlif.Light" parent="android:Theme.Material.Light.NoActionBar">
         <item name="android:colorAccent">@color/suw_color_accent_glif_light</item>
         <item name="android:colorBackground">@color/suw_glif_background_color_light</item>
-        <item name="android:colorPrimary">@color/suw_color_accent_glif_light</item>
+        <item name="android:colorPrimary">?android:attr/colorAccent</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>
@@ -132,12 +143,16 @@
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowSoftInputMode">adjustResize</item>
 
+        <item name="suwButtonAllCaps">true</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_button_corner_radius</item>
+        <item name="suwButtonFontFamily">sans-serif</item>
         <item name="suwColorPrimary">?android:attr/colorPrimary</item>
         <item name="suwFillContentLayoutStyle">@style/SuwFillContentLayout</item>
         <item name="suwDividerInsetEnd">0dp</item>
         <item name="suwDividerInsetStart">@dimen/suw_items_glif_icon_divider_inset</item>
         <item name="suwDividerInsetStartNoIcon">@dimen/suw_items_glif_text_divider_inset</item>
         <item name="suwGlifHeaderGravity">start</item>
+        <item name="suwGlifIconStyle">@style/SuwGlifIcon</item>
         <item name="suwItemDescriptionStyle">@style/SuwItemContainer.Description.Glif</item>
         <item name="suwItemDescriptionTitleStyle">@style/SuwItemTitle.GlifDescription</item>
         <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item>
@@ -145,6 +160,27 @@
         <item name="suwScrollIndicators">bottom</item>
     </style>
 
+    <style name="SuwThemeGlifV3" parent="SuwThemeGlifV2">
+        <item name="android:colorAccent">@color/suw_color_accent_glif_v3</item>
+
+        <item name="suwButtonAllCaps">false</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
+        <item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
+    </style>
+
+    <style name="SuwThemeGlifV3.Light" parent="SuwThemeGlifV2.Light">
+        <item name="android:colorAccent">@color/suw_color_accent_glif_v3</item>
+        <item name="android:navigationBarColor">@color/suw_glif_v3_nav_bar_color_light</item>
+        <!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
+        <item name="android:navigationBarDividerColor" tools:ignore="NewApi">@color/suw_glif_v3_nav_bar_divider_color_light</item>
+        <!-- Ignore NewApi: For some reason lint seems to think this API is new in v28 (b/73514594) -->
+        <item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
+
+        <item name="suwButtonAllCaps">false</item>
+        <item name="suwButtonCornerRadius">@dimen/suw_glif_v3_button_corner_radius</item>
+        <item name="suwButtonFontFamily">@string/suwFontSecondaryMedium</item>
+    </style>
+
     <!-- Button styles -->
 
     <style name="SuwGlifButton.Primary" parent="android:Widget.Material.Button.Colored">
@@ -154,8 +190,13 @@
         <item name="android:buttonStyle">@style/SuwGlifButton.Primary</item>
 
         <!-- Values used in styles -->
+        <item name="android:fontFamily">?attr/suwButtonFontFamily</item>
         <item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
         <item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
+        <item name="android:textAllCaps">?attr/suwButtonAllCaps</item>
+
+        <!-- Values used in themes -->
+        <item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
     </style>
 
     <style name="SuwGlifButton.Secondary" parent="android:Widget.Material.Button.Borderless.Colored">
@@ -166,11 +207,14 @@
         <item name="android:theme">@style/SuwGlifButton.Secondary</item>
 
         <!-- Values used in styles -->
+        <item name="android:fontFamily">?attr/suwButtonFontFamily</item>
         <item name="android:minWidth">0dp</item>
         <item name="android:paddingLeft">@dimen/suw_glif_button_padding</item>
         <item name="android:paddingRight">@dimen/suw_glif_button_padding</item>
+        <item name="android:textAllCaps">?attr/suwButtonAllCaps</item>
 
         <!-- Values used in themes -->
+        <item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/suwButtonCornerRadius</item>
         <item name="android:colorControlHighlight">@color/suw_flat_button_highlight</item>
     </style>
 
diff --git a/library/platform/src/com/android/setupwizardlib/view/RichTextView.java b/library/platform/src/com/android/setupwizardlib/view/RichTextView.java
index 5a78561..fa68a68 100644
--- a/library/platform/src/com/android/setupwizardlib/view/RichTextView.java
+++ b/library/platform/src/com/android/setupwizardlib/view/RichTextView.java
@@ -20,16 +20,18 @@
 import android.text.Annotation;
 import android.text.SpannableString;
 import android.text.Spanned;
-import android.text.method.LinkMovementMethod;
+import android.text.method.MovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.TextAppearanceSpan;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.widget.TextView;
 
 import com.android.setupwizardlib.span.LinkSpan;
 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
 import com.android.setupwizardlib.span.SpanHelper;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
 
 /**
  * An extension of TextView that automatically replaces the annotation tags as specified in
@@ -112,7 +114,7 @@
             // 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());
+            setMovementMethod(TouchableLinkMovementMethod.getInstance());
         } else {
             setMovementMethod(null);
         }
@@ -121,6 +123,11 @@
         // as individual TextViews consume touch events and thereby reducing the focus window
         // shown by Talkback. Disable focus if there are no links
         setFocusable(hasLinks);
+        // Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is
+        // focusable in touch mode, we may be focused when the screen is first shown, and starting
+        // a screen halfway scrolled down is confusing to the user.
+        setRevealOnFocusHint(false);
+        setFocusableInTouchMode(hasLinks);
     }
 
     private boolean hasLinks(CharSequence text) {
@@ -132,6 +139,25 @@
         return false;
     }
 
+    @Override
+    @SuppressWarnings("ClickableViewAccessibility")  // super.onTouchEvent is called
+    public boolean onTouchEvent(MotionEvent event) {
+        // Since View#onTouchEvent always return true if the view is clickable (which is the case
+        // when a TextView has a movement method), override the implementation to allow the movement
+        // method, if it implements TouchableMovementMethod, to say that the touch is not handled,
+        // allowing the event to bubble up to the parent view.
+        boolean superResult = super.onTouchEvent(event);
+        MovementMethod movementMethod = getMovementMethod();
+        if (movementMethod instanceof TouchableMovementMethod) {
+            TouchableMovementMethod touchableMovementMethod =
+                    (TouchableMovementMethod) movementMethod;
+            if (touchableMovementMethod.getLastTouchEvent() == event) {
+                return touchableMovementMethod.isLastTouchEventHandled();
+            }
+        }
+        return superResult;
+    }
+
     public void setOnLinkClickListener(OnLinkClickListener listener) {
         mOnLinkClickListener = listener;
     }
diff --git a/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml b/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml
index e8d209b..c16f85a 100644
--- a/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml
+++ b/library/recyclerview/res/layout/suw_glif_recycler_template_content.xml
@@ -23,6 +23,11 @@
     android:layout_height="match_parent"
     android:orientation="vertical">
 
+    <ViewStub
+        android:id="@+id/suw_layout_sticky_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
     <!-- Ignore UnusedAttribute: scrollIndicators is new in M. Default to no indicators in older
          versions. -->
     <com.android.setupwizardlib.view.HeaderRecyclerView
diff --git a/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java
index 75b1c7a..e8a021d 100644
--- a/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java
+++ b/library/recyclerview/src/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -107,10 +107,12 @@
     }
 
     @Override
-    public View findManagedViewById(int id) {
+    // Returning generic type is the common pattern used for findViewBy* methods
+    @SuppressWarnings("TypeParameterUnusedInFormals")
+    public <T extends View> T findManagedViewById(int id) {
         final View header = mRecyclerMixin.getHeader();
         if (header != null) {
-            final View view = header.findViewById(id);
+            final T view = header.findViewById(id);
             if (view != null) {
                 return view;
             }
diff --git a/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
index 5ff825d..6f3aed4 100644
--- a/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
+++ b/library/recyclerview/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
@@ -127,10 +127,12 @@
     }
 
     @Override
-    public View findManagedViewById(int id) {
+    // Returning generic type is the common pattern used for findViewBy* methods
+    @SuppressWarnings("TypeParameterUnusedInFormals")
+    public <T extends View> T findManagedViewById(int id) {
         final View header = mRecyclerMixin.getHeader();
         if (header != null) {
-            final View view = header.findViewById(id);
+            final T view = header.findViewById(id);
             if (view != null) {
                 return view;
             }
diff --git a/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java b/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
index 78280a6..0703c17 100644
--- a/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
+++ b/library/recyclerview/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java
@@ -143,9 +143,9 @@
     @Override
     public void onBindViewHolder(ItemViewHolder holder, int position) {
         final IItem item = getItem(position);
-        item.onBindView(holder.itemView);
         holder.setEnabled(item.isEnabled());
         holder.setItem(item);
+        item.onBindView(holder.itemView);
     }
 
     @Override
diff --git a/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java b/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
index b509389..5912f7f 100644
--- a/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
+++ b/library/recyclerview/test/robotest/src/com/android/setupwizardlib/template/RecyclerViewScrollHandlingDelegateTest.java
@@ -27,7 +27,6 @@
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.OnScrollListener;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
 import org.junit.Before;
@@ -38,7 +37,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 @RunWith(SuwLibRobolectricTestRunner.class)
 public class RecyclerViewScrollHandlingDelegateTest {
 
diff --git a/library/rules.gradle b/library/rules.gradle
index 6b0acce..9baa390 100644
--- a/library/rules.gradle
+++ b/library/rules.gradle
@@ -13,6 +13,23 @@
 
     publishNonDefault true
 
+    flavorDimensions 'compat'
+
+    productFlavors {
+        // DEPRECATED: Platform version that will not include the compatibility libraries
+        platformDeprecated {
+            dimension 'compat'
+            // TODO(yukl): Bump this file to v28 once we can properly test that
+            minSdkVersion 27
+        }
+
+        // Provides backwards compatibility for Gingerbread or above, using support libraries.
+        gingerbreadCompat {
+            dimension 'compat'
+            minSdkVersion 9
+        }
+    }
+
     sourceSets {
         main {
             manifest.srcFile 'main/AndroidManifest.xml'
@@ -21,43 +38,7 @@
             res.srcDirs = ['main/res']
         }
 
-        productFlavors {
-            // Platform version that will not include the compatibility libraries
-            platform {
-                minSdkVersion 23
-
-                dependencies {
-                    // Read the dependencies from the "deps" map in the extra properties.
-                    //
-                    // For builds in the Android tree we want to build the dependencies from source
-                    // for reproducible builds, for example in build.gradle define something like
-                    // this:
-                    //      ext {
-                    //          deps = ['project-name': project(':project-path')]
-                    //      }
-                    //
-                    // For standalone project clients, since the source may not be available, we
-                    // fetch the dependencies from maven. For example in standalone.gradle define
-                    // something like this:
-                    //      ext {
-                    //          deps = ['project-name': 'com.example.group:project-name:1.0.0']
-                    //      }
-                    //
-                    platformCompile deps['support-annotations']
-                }
-            }
-
-            // Provides backwards compatibility for Gingerbread or above, using support libraries.
-            gingerbreadCompat {
-                minSdkVersion 9
-                dependencies {
-                    gingerbreadCompatCompile deps['support-appcompat-v7']
-                    gingerbreadCompatCompile deps['support-recyclerview-v7']
-                }
-            }
-        }
-
-        platform {
+        platformDeprecated {
             java.srcDirs = ['platform/src']
             res.srcDirs = ['platform/res']
         }
@@ -68,3 +49,26 @@
         }
     }
 }
+
+dependencies {
+    // Read the dependencies from the "deps" map in the extra properties.
+    //
+    // For builds in the Android tree we want to build the dependencies from source
+    // for reproducible builds, for example in build.gradle define something like
+    // this:
+    //      ext {
+    //          deps = ['project-name': project(':project-path')]
+    //      }
+    //
+    // For standalone project clients, since the source may not be available, we
+    // fetch the dependencies from maven. For example in standalone.gradle define
+    // something like this:
+    //      ext {
+    //          deps = ['project-name': 'com.example.group:project-name:1.0.0']
+    //      }
+    //
+    platformDeprecatedCompile deps['support-annotations']
+
+    gingerbreadCompatCompile deps['support-appcompat-v7']
+    gingerbreadCompatCompile deps['support-recyclerview-v7']
+}
diff --git a/library/self.gradle b/library/self.gradle
index 6a405e2..a4bff4e 100644
--- a/library/self.gradle
+++ b/library/self.gradle
@@ -5,6 +5,18 @@
 apply from: '../tools/gradle/dist-library-instrumentation-tests.gradle'
 apply from: '../tools/gradle/dist-unit-tests.gradle'
 
+apply plugin: 'net.ltgt.errorprone'
+
+buildscript {
+    repositories {
+        maven { url "$rootDir/prebuilts/tools/common/m2/repository" }
+    }
+
+    dependencies {
+        classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13"
+    }
+}
+
 // Add targets for tests
 android.sourceSets {
     androidTest {
@@ -13,16 +25,17 @@
         res.srcDirs = ['test/instrumentation/res']
 
         dependencies {
-            androidTestCompile 'com.android.support.test:rules:0.5'
-            androidTestCompile 'com.android.support.test:runner:0.5'
-            androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
-            androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
-            androidTestCompile 'junit:junit:4.+'
-            androidTestCompile 'org.mockito:mockito-core:1.9.5'
+            androidTestImplementation 'com.android.support.test:rules:1.0.0'
+            androidTestImplementation 'com.android.support.test:runner:1.0.0'
+            androidTestImplementation 'com.google.dexmaker:dexmaker:1.2'
+            androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:1.2'
+            androidTestImplementation 'com.google.truth:truth:0.31'
+            androidTestImplementation 'junit:junit:4.+'
+            androidTestImplementation 'org.mockito:mockito-core:1.9.5'
         }
     }
 
-    androidTestPlatform {
+    androidTestPlatformDeprecated {
         java.srcDirs = ['platform/test/src']
     }
 
@@ -38,12 +51,13 @@
         java.srcDirs = ['test/robotest/src']
 
         dependencies {
-            testCompile 'org.robolectric:robolectric:3.+'
-            testCompile 'org.robolectric:shadows-core:3.+'
-            testCompile 'junit:junit:4.+'
-            testCompile 'org.mockito:mockito-core:1.9.5'
+            testImplementation 'org.robolectric:robolectric:3.6.1'
+            testImplementation 'org.robolectric:shadows-framework:3.6.1'
+            testImplementation 'junit:junit:4.+'
+            testImplementation 'com.google.truth:truth:0.31'
+            testImplementation 'org.mockito:mockito-core:1.9.5'
             // Workaround for https://github.com/robolectric/robolectric/issues/2566
-            testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
+            testImplementation 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
         }
     }
 
@@ -51,6 +65,9 @@
         java.srcDirs = ['gingerbread/test/robotest/src', 'recyclerview/test/robotest/src']
     }
 }
+
+android.testOptions.unitTests.includeAndroidResources = true
+
 android.defaultConfig.testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
 android.lintOptions {
     abortOnError true
@@ -64,14 +81,3 @@
 android.libraryVariants.all { variant ->
     variant.assemble.dependsOn(tasks.findByName('lint'))
 }
-
-// For compatibility with existing continuous test configurations, copy the file to
-// setup-wizard-libTest.apk
-// TODO: Remove this once continuous test configurations are updated to handle the new file name
-task createLegacyTestApk(type: Copy) {
-    from "${project.ext.distDir}/setup-wizard-lib-gingerbreadCompat-debug-androidTest.apk"
-    into "${project.ext.distDir}"
-    rename ('setup-wizard-lib-gingerbreadCompat-debug-androidTest.apk', 'setup-wizard-libTest.apk')
-}
-
-tasks.dist.finalizedBy createLegacyTestApk
diff --git a/library/standalone.gradle b/library/standalone.gradle
index a2119ac..132b908 100644
--- a/library/standalone.gradle
+++ b/library/standalone.gradle
@@ -16,5 +16,5 @@
 
 apply from: 'standalone-rules.gradle'
 
-android.compileSdkVersion 25
-android.buildToolsVersion '25.0.0'
+android.compileSdkVersion 26
+android.buildToolsVersion '26.0.0'
diff --git a/library/test/instrumentation/AndroidManifest.xml b/library/test/instrumentation/AndroidManifest.xml
index 44d923f..0cbc9c1 100644
--- a/library/test/instrumentation/AndroidManifest.xml
+++ b/library/test/instrumentation/AndroidManifest.xml
@@ -19,7 +19,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="com.android.setupwizardlib.test">
 
-    <uses-sdk tools:overrideLibrary="android.support.test,android.app,android.support.test.rule" />
+    <uses-sdk tools:overrideLibrary="android.support.test.rules,android.support.test.runner" />
 
     <application>
         <activity android:name=".util.DrawingTestActivity" />
diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/template/IconMixinTest.java b/library/test/instrumentation/src/com/android/setupwizardlib/template/IconMixinTest.java
index a1f2b54..5a36f4a 100644
--- a/library/test/instrumentation/src/com/android/setupwizardlib/template/IconMixinTest.java
+++ b/library/test/instrumentation/src/com/android/setupwizardlib/template/IconMixinTest.java
@@ -16,6 +16,8 @@
 
 package com.android.setupwizardlib.template;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.mockito.Matchers.eq;
@@ -27,10 +29,12 @@
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 import android.util.Xml;
+import android.view.View;
 import android.widget.ImageView;
 
 import com.android.setupwizardlib.TemplateLayout;
@@ -74,6 +78,26 @@
         mixin.setIcon(drawable);
 
         assertSame(drawable, mIconView.getDrawable());
+        assertEquals(View.VISIBLE, mIconView.getVisibility());
+    }
+
+    @Test
+    public void setIcon_resourceId_shouldSetIcon() {
+        int icon = android.R.drawable.ic_menu_add;
+        IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
+        mixin.setIcon(icon);
+
+        Drawable drawable = mIconView.getDrawable();
+        assertThat(drawable).isInstanceOf(BitmapDrawable.class);
+        assertEquals(View.VISIBLE, mIconView.getVisibility());
+    }
+
+    @Test
+    public void setIcon_shouldSetVisibilityToGone_whenIconIsNull() {
+        IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
+        mixin.setIcon(null);
+
+        assertEquals(View.GONE, mIconView.getVisibility());
     }
 
     @Test
@@ -101,5 +125,20 @@
                 .getDrawable(android.R.drawable.ic_menu_add);
         final BitmapDrawable actual = (BitmapDrawable) mIconView.getDrawable();
         assertEquals(expected.getBitmap(), actual.getBitmap());
+        assertEquals(View.VISIBLE, mIconView.getVisibility());
+    }
+
+    @Test
+    public void setContentDescription_shouldSetContentDescriptionOnIconView() {
+        IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
+        mixin.setContentDescription("hello world");
+        assertThat(mIconView.getContentDescription()).isEqualTo("hello world");
+    }
+
+    @Test
+    public void getContentDescription_shouldReturnContentDescriptionFromView() {
+        IconMixin mixin = new IconMixin(mTemplateLayout, null, 0);
+        mIconView.setContentDescription("aloha");
+        assertThat(mixin.getContentDescription()).isEqualTo("aloha");
     }
 }
diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java b/library/test/instrumentation/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
index 99c87fb..37ac41a 100644
--- a/library/test/instrumentation/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
+++ b/library/test/instrumentation/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
@@ -137,7 +137,7 @@
         assertEquals("Matrices should match", expected, canvas.getMatrix());
     }
 
-    @SmallTest
+    @Test
     public void testScaleToCanvasMaxSize() {
         final Canvas canvas = new Canvas();
         final Matrix expected = new Matrix(canvas.getMatrix());
diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/test/SystemBarHelperTest.java b/library/test/instrumentation/src/com/android/setupwizardlib/test/SystemBarHelperTest.java
index 0ebf7cb..98c28f6 100644
--- a/library/test/instrumentation/src/com/android/setupwizardlib/test/SystemBarHelperTest.java
+++ b/library/test/instrumentation/src/com/android/setupwizardlib/test/SystemBarHelperTest.java
@@ -16,6 +16,8 @@
 
 package com.android.setupwizardlib.test;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 
 import android.annotation.SuppressLint;
@@ -31,6 +33,7 @@
 import android.support.test.filters.SmallTest;
 import android.support.test.rule.UiThreadTestRule;
 import android.support.test.runner.AndroidJUnit4;
+import android.view.ContextThemeWrapper;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
@@ -131,7 +134,9 @@
     @Test
     public void testShowSystemBarsWindow() {
         final Window window = createWindowWithSystemUiVisibility(0x456);
-        SystemBarHelper.showSystemBars(window, InstrumentationRegistry.getContext());
+        Context context = new ContextThemeWrapper(
+                InstrumentationRegistry.getContext(), android.R.style.Theme);
+        SystemBarHelper.showSystemBars(window, context);
         if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
             assertEquals(
                     "DEFAULT_IMMERSIVE_FLAGS should be removed from window's systemUiVisibility",
@@ -191,11 +196,15 @@
     @UiThreadTest
     @Test
     public void testSetBackButtonVisibleTrue() {
-        final Window window = createWindowWithSystemUiVisibility(0x456);
+        final Window window = createWindowWithSystemUiVisibility(STATUS_BAR_DISABLE_BACK | 0x456);
         SystemBarHelper.setBackButtonVisible(window, true);
         if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
-            assertEquals("View visibility should be 0x456", 0x456,
-                    window.getAttributes().systemUiVisibility);
+            assertThat(window.getAttributes().systemUiVisibility)
+                    .named("window sysUiVisibility")
+                    .isEqualTo(0x456);
+            assertThat(window.getDecorView().getSystemUiVisibility())
+                    .named("decor view sysUiVisibility")
+                    .isEqualTo(0x456);
         }
     }
 
@@ -205,8 +214,12 @@
         final Window window = createWindowWithSystemUiVisibility(0x456);
         SystemBarHelper.setBackButtonVisible(window, false);
         if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
-            assertEquals("STATUS_BAR_DISABLE_BACK should be added to systemUiVisibility",
-                    0x456 | STATUS_BAR_DISABLE_BACK, window.getAttributes().systemUiVisibility);
+            assertThat(window.getAttributes().systemUiVisibility)
+                    .named("window sysUiVisibility")
+                    .isEqualTo(0x456 | STATUS_BAR_DISABLE_BACK);
+            assertThat(window.getDecorView().getSystemUiVisibility())
+                    .named("decor view sysUiVisibility")
+                    .isEqualTo(0x456 | STATUS_BAR_DISABLE_BACK);
         }
     }
 
diff --git a/library/test/robotest/src/com/android/setupwizardlib/GlifLayoutTest.java b/library/test/robotest/src/com/android/setupwizardlib/GlifLayoutTest.java
index d46409d..d2d1ee0 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/GlifLayoutTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/GlifLayoutTest.java
@@ -32,6 +32,8 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.support.annotation.IdRes;
 import android.view.ContextThemeWrapper;
 import android.view.View;
@@ -53,7 +55,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class GlifLayoutTest {
 
     private Context mContext;
@@ -266,6 +268,74 @@
         assertNotNull(layout.findViewById(android.R.id.text1));
     }
 
+    @Test
+    public void inflateStickyHeader_shouldAddViewToLayout() {
+        GlifLayout layout = new GlifLayout(mContext);
+
+        final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
+        assertEquals(android.R.id.text1, view.getId());
+        assertNotNull(layout.findViewById(android.R.id.text1));
+    }
+
+    @Config(qualifiers = "sw600dp")
+    @Test
+    public void inflateStickyHeader_whenOnTablets_shouldAddViewToLayout() {
+        inflateStickyHeader_shouldAddViewToLayout();
+    }
+
+    @Test
+    public void inflateStickyHeader_whenInXml_shouldAddViewToLayout() {
+        GlifLayout layout = new GlifLayout(
+                mContext,
+                Robolectric.buildAttributeSet()
+                        .addAttribute(R.attr.suwStickyHeader, "@android:layout/simple_list_item_1")
+                        .build());
+
+        assertNotNull(layout.findViewById(android.R.id.text1));
+    }
+
+    @Test
+    public void inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout() {
+        GlifLayout layout = new GlifLayout(mContext, R.layout.suw_glif_blank_template);
+
+        final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
+        assertEquals(android.R.id.text1, view.getId());
+        assertNotNull(layout.findViewById(android.R.id.text1));
+    }
+
+    @Config(qualifiers = "sw600dp")
+    @Test
+    public void inflateStickyHeader_whenOnBlankTemplateTablet_shouldAddViewToLayout() {
+        inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout();
+    }
+
+    @Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.NEWEST_SDK)
+    @Test
+    public void createFromXml_shouldSetLayoutFullscreen_whenLayoutFullscreenIsNotSet() {
+        GlifLayout layout = new GlifLayout(
+                mContext,
+                Robolectric.buildAttributeSet()
+                        .build());
+        if (VERSION.SDK_INT >= VERSION_CODES.M) {
+            assertEquals(
+                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,
+                    layout.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+        }
+    }
+
+    @Test
+    public void createFromXml_shouldNotSetLayoutFullscreen_whenLayoutFullscreenIsFalse() {
+        GlifLayout layout = new GlifLayout(
+                mContext,
+                Robolectric.buildAttributeSet()
+                        .addAttribute(R.attr.suwLayoutFullscreen, "false")
+                        .build());
+
+        assertEquals(
+                0,
+                layout.getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+    }
+
     private Drawable getPhoneBackground(GlifLayout layout) {
         final StatusBarBackgroundLayout patternBg =
                 (StatusBarBackgroundLayout) layout.findManagedViewById(R.id.suw_pattern_bg);
diff --git a/library/test/robotest/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetectorTest.java b/library/test/robotest/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetectorTest.java
new file mode 100644
index 0000000..aa2cce3
--- /dev/null
+++ b/library/test/robotest/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetectorTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 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.gesture;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@RunWith(SuwLibRobolectricTestRunner.class)
+public class ConsecutiveTapsGestureDetectorTest {
+
+    @Mock
+    private ConsecutiveTapsGestureDetector.OnConsecutiveTapsListener mListener;
+
+    private ConsecutiveTapsGestureDetector mDetector;
+    private int mSlop;
+    private int mTapTimeout;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        View view = new View(application);
+        view.measure(500, 500);
+        view.layout(0, 0, 500, 500);
+        mDetector = new ConsecutiveTapsGestureDetector(mListener, view);
+
+        mSlop = ViewConfiguration.get(application).getScaledDoubleTapSlop();
+        mTapTimeout = ViewConfiguration.getDoubleTapTimeout();
+    }
+
+    @Test
+    public void onTouchEvent_shouldTriggerCallbackOnFourTaps() {
+        InOrder inOrder = inOrder(mListener);
+
+        tap(0, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(1));
+
+        tap(100, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(2));
+
+        tap(200, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(3));
+
+        tap(300, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(4));
+    }
+
+    @Test
+    public void onTouchEvent_tapOnDifferentLocation_shouldResetCounter() {
+        InOrder inOrder = inOrder(mListener);
+
+        tap(0, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(1));
+
+        tap(100, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(2));
+
+        tap(200, 25f + mSlop * 2, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(1));
+
+        tap(300, 25f + mSlop * 2, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(2));
+    }
+
+    @Test
+    public void onTouchEvent_tapAfterTimeout_shouldResetCounter() {
+        InOrder inOrder = inOrder(mListener);
+
+        tap(0, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(1));
+
+        tap(100, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(2));
+
+        tap(200 + mTapTimeout, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(1));
+
+        tap(300 + mTapTimeout, 25f, 25f);
+        inOrder.verify(mListener).onConsecutiveTaps(eq(2));
+    }
+
+    private void tap(int timeMillis, float x, float y) {
+        mDetector.onTouchEvent(
+                MotionEvent.obtain(timeMillis, timeMillis, MotionEvent.ACTION_DOWN, x, y, 0));
+        mDetector.onTouchEvent(
+                MotionEvent.obtain(timeMillis, timeMillis + 10, MotionEvent.ACTION_UP, x, y, 0));
+    }
+}
diff --git a/library/test/robotest/src/com/android/setupwizardlib/items/ButtonItemTest.java b/library/test/robotest/src/com/android/setupwizardlib/items/ButtonItemTest.java
index 93b9a6d..40e5da8 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/items/ButtonItemTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/items/ButtonItemTest.java
@@ -39,7 +39,6 @@
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.items.ButtonItem.OnClickListener;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
@@ -50,9 +49,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(
-        constants = BuildConfig.class,
-        sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class ButtonItemTest {
 
     private ViewGroup mParent;
diff --git a/library/test/robotest/src/com/android/setupwizardlib/items/ItemGroupTest.java b/library/test/robotest/src/com/android/setupwizardlib/items/ItemGroupTest.java
index a61b750..ecaec71 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/items/ItemGroupTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/items/ItemGroupTest.java
@@ -24,7 +24,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
 import org.junit.Before;
@@ -36,9 +35,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(
-        constants = BuildConfig.class,
-        sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class ItemGroupTest {
 
     private static final Item CHILD_1 = new EqualsItem("Child 1");
diff --git a/library/test/robotest/src/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java b/library/test/robotest/src/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java
deleted file mode 100644
index 64c63e7..0000000
--- a/library/test/robotest/src/com/android/setupwizardlib/robolectric/PatchedGradleManifestFactory.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.setupwizardlib.robolectric;
-
-import org.robolectric.annotation.Config;
-import org.robolectric.internal.GradleManifestFactory;
-import org.robolectric.internal.ManifestIdentifier;
-import org.robolectric.res.FileFsFile;
-import org.robolectric.util.Logger;
-import org.robolectric.util.ReflectionHelpers;
-
-import java.io.File;
-import java.net.URL;
-
-/**
- * Modified GradleManifestFactory to patch an issue where some build variants have merged
- * resources under res/merged/variant/type while others have it under bundles/variant/type/res.
- *
- * The change is that in the .exists() checks below we check for specific folders for the build
- * variant rather than checking existence at the parent directory.
- */
-class PatchedGradleManifestFactory extends GradleManifestFactory {
-
-    @Override
-    public ManifestIdentifier identify(Config config) {
-        if (config.constants() == Void.class) {
-            Logger.error("Field 'constants' not specified in @Config annotation");
-            Logger.error("This is required when using Robolectric with Gradle!");
-            throw new RuntimeException("No 'constants' field in @Config annotation!");
-        }
-
-        final String buildOutputDir = getBuildOutputDir(config);
-        final String type = getType(config);
-        final String flavor = getFlavor(config);
-        final String abiSplit = getAbiSplit(config);
-        final String packageName = config.packageName().isEmpty()
-                ? config.constants().getPackage().getName()
-                : config.packageName();
-
-        final FileFsFile res;
-        final FileFsFile assets;
-        final FileFsFile manifest;
-
-        if (FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type).exists()) {
-            // Android gradle plugin 1.5.0+ puts the merged layouts in data-binding-layout-out.
-            // https://github.com/robolectric/robolectric/issues/2143
-            res = FileFsFile.from(buildOutputDir, "data-binding-layout-out", flavor, type);
-        } else if (FileFsFile.from(buildOutputDir, "res", "merged", flavor, type).exists()) {
-            // res/merged added in Android Gradle plugin 1.3-beta1
-            res = FileFsFile.from(buildOutputDir, "res", "merged", flavor, type);
-        } else if (FileFsFile.from(buildOutputDir, "res", flavor, type).exists()) {
-            res = FileFsFile.from(buildOutputDir, "res", flavor, type);
-        } else {
-            res = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "res");
-        }
-
-        if (FileFsFile.from(buildOutputDir, "assets", flavor, type).exists()) {
-            assets = FileFsFile.from(buildOutputDir, "assets", flavor, type);
-        } else {
-            assets = FileFsFile.from(buildOutputDir, "bundles", flavor, type, "assets");
-        }
-
-        String manifestName = config.manifest();
-        URL manifestUrl = getClass().getClassLoader().getResource(manifestName);
-        if (manifestUrl != null && manifestUrl.getProtocol().equals("file")) {
-            manifest = FileFsFile.from(manifestUrl.getPath());
-        } else if (FileFsFile.from(buildOutputDir, "manifests", "full", flavor, abiSplit, type,
-                manifestName).exists()) {
-            manifest = FileFsFile.from(
-                    buildOutputDir, "manifests", "full", flavor, abiSplit, type, manifestName);
-        } else if (FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit, type,
-                manifestName).exists()) {
-            // Android gradle plugin 2.2.0+ can put library manifest files inside of "aapt"
-            // instead of "full"
-            manifest = FileFsFile.from(buildOutputDir, "manifests", "aapt", flavor, abiSplit,
-                    type, manifestName);
-        } else {
-            manifest = FileFsFile.from(buildOutputDir, "bundles", flavor, abiSplit, type,
-                    manifestName);
-        }
-
-        return new ManifestIdentifier(manifest, res, assets, packageName, null);
-    }
-
-    private static String getBuildOutputDir(Config config) {
-        return config.buildDir() + File.separator + "intermediates";
-    }
-
-    private static String getType(Config config) {
-        try {
-            return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
-        } catch (Throwable e) {
-            return null;
-        }
-    }
-
-    private static String getFlavor(Config config) {
-        try {
-            return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
-        } catch (Throwable e) {
-            return null;
-        }
-    }
-
-    private static String getAbiSplit(Config config) {
-        try {
-            return config.abiSplit();
-        } catch (Throwable e) {
-            return null;
-        }
-    }
-}
diff --git a/library/test/robotest/src/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java b/library/test/robotest/src/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
index 509201a..61baa23 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/robolectric/SuwLibRobolectricTestRunner.java
@@ -20,42 +20,13 @@
 import org.junit.runners.model.FrameworkMethod;
 import org.junit.runners.model.InitializationError;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.internal.ManifestFactory;
 
 public class SuwLibRobolectricTestRunner extends RobolectricTestRunner {
 
-    private String mModuleRootPath;
-
     public SuwLibRobolectricTestRunner(Class<?> testClass) throws InitializationError {
         super(testClass);
     }
 
-    // Hack to determine the module root path in the build folder (e.g. out/gradle/setup-wizard-lib)
-    private void updateModuleRootPath(Config config) {
-        String moduleRoot = config.constants().getResource("").toString()
-                .replace("file:", "").replace("jar:", "");
-        mModuleRootPath =
-                moduleRoot.substring(0, moduleRoot.lastIndexOf("/build")) + "/setup-wizard-lib";
-    }
-
-    /**
-     * Return the default config used to run Robolectric tests.
-     */
-    @Override
-    protected Config buildGlobalConfig() {
-        Config parent = super.buildGlobalConfig();
-        updateModuleRootPath(parent);
-        return new Config.Builder(parent)
-                .setBuildDir(mModuleRootPath + "/build")
-                .build();
-    }
-
-    @Override
-    protected ManifestFactory getManifestFactory(Config config) {
-        return new PatchedGradleManifestFactory();
-    }
-
     @Override
     protected void runChild(FrameworkMethod method, RunNotifier notifier) {
         System.out.println("===== Running " + method + " =====");
diff --git a/library/test/robotest/src/com/android/setupwizardlib/shadow/ShadowLog.java b/library/test/robotest/src/com/android/setupwizardlib/shadow/ShadowLog.java
new file mode 100644
index 0000000..f1d37c8
--- /dev/null
+++ b/library/test/robotest/src/com/android/setupwizardlib/shadow/ShadowLog.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.shadow;
+
+import android.util.Log;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(Log.class)
+public class ShadowLog extends org.robolectric.shadows.ShadowLog {
+
+    public static boolean sWtfIsFatal = true;
+
+    public static class TerribleFailure extends RuntimeException {
+
+        public TerribleFailure(String msg, Throwable cause) {
+            super(msg, cause);
+        }
+    }
+
+    @Implementation
+    public static void wtf(String tag, String msg) {
+        org.robolectric.shadows.ShadowLog.wtf(tag, msg);
+        if (sWtfIsFatal) {
+            throw new TerribleFailure(msg, null);
+        }
+    }
+
+    @Implementation
+    public static void wtf(String tag, String msg, Throwable throwable) {
+        org.robolectric.shadows.ShadowLog.wtf(tag, msg, throwable);
+        if (sWtfIsFatal) {
+            throw new TerribleFailure(msg, throwable);
+        }
+    }
+}
diff --git a/library/test/robotest/src/com/android/setupwizardlib/span/LinkSpanTest.java b/library/test/robotest/src/com/android/setupwizardlib/span/LinkSpanTest.java
index f86e057..3aafa7d 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/span/LinkSpanTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/span/LinkSpanTest.java
@@ -16,26 +16,28 @@
 
 package com.android.setupwizardlib.span;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertSame;
 import static org.robolectric.RuntimeEnvironment.application;
 
 import android.content.Context;
 import android.content.ContextWrapper;
+import android.text.Selection;
+import android.text.SpannableStringBuilder;
+import android.text.method.LinkMovementMethod;
 import android.widget.TextView;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class)
 public class LinkSpanTest {
 
     @Test
-    public void testOnClick() {
+    public void onClick_shouldCallListenerOnContext() {
         final TestContext context = new TestContext(application);
         final TextView textView = new TextView(context);
         final LinkSpan linkSpan = new LinkSpan("test_id");
@@ -46,7 +48,7 @@
     }
 
     @Test
-    public void testNonImplementingContext() {
+    public void onClick_contextDoesNotImplementOnClickListener_shouldBeNoOp() {
         final TextView textView = new TextView(application);
         final LinkSpan linkSpan = new LinkSpan("test_id");
 
@@ -57,7 +59,7 @@
     }
 
     @Test
-    public void testWrappedListener() {
+    public void onClick_contextWrapsOnClickListener_shouldCallWrappedListener() {
         final TestContext context = new TestContext(application);
         final Context wrapperContext = new ContextWrapper(context);
         final TextView textView = new TextView(wrapperContext);
@@ -68,6 +70,28 @@
         assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan);
     }
 
+    @Test
+    public void onClick_shouldClearSelection() {
+        final TestContext context = new TestContext(application);
+        final TextView textView = new TextView(context);
+        textView.setMovementMethod(LinkMovementMethod.getInstance());
+        textView.setFocusable(true);
+        textView.setFocusableInTouchMode(true);
+        final LinkSpan linkSpan = new LinkSpan("test_id");
+
+        SpannableStringBuilder text = new SpannableStringBuilder("Lorem ipsum dolor sit");
+        textView.setText(text);
+        text.setSpan(linkSpan, /* start= */ 0, /* end= */ 5, /* flags= */ 0);
+        // Simulate the touch effect set by TextView when touched.
+        Selection.setSelection(text, /* start= */ 0, /* end= */ 5);
+
+        linkSpan.onClick(textView);
+
+        assertThat(Selection.getSelectionStart(textView.getText())).isEqualTo(0);
+        assertThat(Selection.getSelectionEnd(textView.getText())).isEqualTo(0);
+    }
+
+    @SuppressWarnings("deprecation")
     private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
 
         public LinkSpan clickedSpan = null;
diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
index fa81dc0..ec3622d 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/template/ListViewScrollHandlingDelegateTest.java
@@ -31,7 +31,6 @@
 import android.widget.BaseAdapter;
 import android.widget.ListView;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
 import org.junit.Before;
@@ -42,7 +41,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 @RunWith(SuwLibRobolectricTestRunner.class)
 public class ListViewScrollHandlingDelegateTest {
 
diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java
index 8e39c43..c641449 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/template/RequireScrollMixinTest.java
@@ -34,7 +34,6 @@
 import android.view.View.OnClickListener;
 import android.widget.Button;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.TemplateLayout;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 import com.android.setupwizardlib.template.RequireScrollMixin.OnRequireScrollStateChangedListener;
@@ -48,7 +47,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 @RunWith(SuwLibRobolectricTestRunner.class)
 public class RequireScrollMixinTest {
 
diff --git a/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java b/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
index f77e256..429445c 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/template/ScrollViewScrollHandlingDelegateTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.Mockito.verify;
 import static org.robolectric.RuntimeEnvironment.application;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 import com.android.setupwizardlib.view.BottomScrollView;
 import com.android.setupwizardlib.view.BottomScrollView.BottomScrollListener;
@@ -36,7 +35,7 @@
 import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 @RunWith(SuwLibRobolectricTestRunner.class)
 public class ScrollViewScrollHandlingDelegateTest {
 
diff --git a/library/test/robotest/src/com/android/setupwizardlib/util/GlifDimensionTest.java b/library/test/robotest/src/com/android/setupwizardlib/util/GlifDimensionTest.java
index 2be64e1..c10c122 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/util/GlifDimensionTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/util/GlifDimensionTest.java
@@ -26,7 +26,6 @@
 import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
@@ -36,7 +35,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.ALL_SDKS)
+@Config(sdk = Config.ALL_SDKS)
 public class GlifDimensionTest {
 
     private Context mContext;
diff --git a/library/test/robotest/src/com/android/setupwizardlib/util/GlifStyleTest.java b/library/test/robotest/src/com/android/setupwizardlib/util/GlifStyleTest.java
index aea2c03..46df9d6 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/util/GlifStyleTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/util/GlifStyleTest.java
@@ -16,8 +16,10 @@
 
 package com.android.setupwizardlib.util;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.robolectric.RuntimeEnvironment.application;
 
 import android.annotation.TargetApi;
@@ -29,8 +31,8 @@
 import android.support.annotation.Nullable;
 import android.view.ContextThemeWrapper;
 import android.widget.Button;
+import android.widget.ProgressBar;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
@@ -41,7 +43,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
+@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
 public class GlifStyleTest {
 
     private Context mContext;
@@ -58,9 +60,8 @@
                 Robolectric.buildAttributeSet()
                         .setStyleAttribute("@style/SuwGlifButton.Tertiary")
                         .build());
-        assertNull("Background of tertiary button should be null", button.getBackground());
-        assertNull("Tertiary button should have no transformation method",
-                button.getTransformationMethod());
+        assertThat(button.getBackground()).named("background").isNotNull();
+        assertThat(button.getTransformationMethod()).named("transformation method").isNull();
         if (VERSION.SDK_INT < VERSION_CODES.M) {
             // Robolectric resolved the wrong theme attribute on versions >= M
             // https://github.com/robolectric/robolectric/issues/2940
@@ -76,6 +77,15 @@
         assertEquals(0x00000000, activity.getWindow().getStatusBarColor());
     }
 
+    @Test
+    public void glifLoadingScreen_shouldHaveProgressBar() {
+        GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
+        activity.setContentView(R.layout.suw_glif_loading_screen);
+
+        assertTrue("Progress bar should exist",
+                activity.findViewById(R.id.suw_large_progress_bar) instanceof ProgressBar);
+    }
+
     private static class GlifThemeActivity extends Activity {
 
         @Override
diff --git a/library/test/robotest/src/com/android/setupwizardlib/util/GlifV3StyleTest.java b/library/test/robotest/src/com/android/setupwizardlib/util/GlifV3StyleTest.java
new file mode 100644
index 0000000..613f2aa
--- /dev/null
+++ b/library/test/robotest/src/com/android/setupwizardlib/util/GlifV3StyleTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.setupwizardlib.R;
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
+
+@RunWith(SuwLibRobolectricTestRunner.class)
+@Config(minSdk = Config.OLDEST_SDK, maxSdk = Config.NEWEST_SDK)
+public class GlifV3StyleTest {
+
+    @Test
+    public void activityWithGlifV3Theme_shouldUseLightNavBarOnV27OrAbove() {
+        GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
+        if (VERSION.SDK_INT >= VERSION_CODES.O_MR1) {
+            assertEquals(
+                    activity.getWindow().getNavigationBarColor(),
+                    Color.WHITE);
+            int vis = activity.getWindow().getDecorView().getSystemUiVisibility();
+            assertTrue((vis & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0);
+        } else if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+            assertEquals(
+                    activity.getWindow().getNavigationBarColor(),
+                    Color.BLACK);
+        }
+        // Nav bar color is not customizable pre-L
+    }
+
+    @Test
+    public void buttonWithGlifV3_shouldBeGoogleSans() {
+        GlifThemeActivity activity = Robolectric.setupActivity(GlifThemeActivity.class);
+        Button button = new Button(
+                activity,
+                Robolectric.buildAttributeSet()
+                        .setStyleAttribute("@style/SuwGlifButton.Primary")
+                        .build());
+        assertThat(button.getTypeface()).isEqualTo(Typeface.create("google-sans", 0));
+        // Button should not be all caps
+        assertThat(button.getTransformationMethod()).isNull();
+    }
+
+    private static class GlifThemeActivity extends Activity {
+
+        @Override
+        protected void onCreate(@Nullable Bundle savedInstanceState) {
+            setTheme(R.style.SuwThemeGlifV3_Light);
+            super.onCreate(savedInstanceState);
+        }
+    }
+}
diff --git a/library/test/robotest/src/com/android/setupwizardlib/util/PartnerTest.java b/library/test/robotest/src/com/android/setupwizardlib/util/PartnerTest.java
index f47eef1..f8e71be 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/util/PartnerTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/util/PartnerTest.java
@@ -31,29 +31,33 @@
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.os.Build.VERSION;
 import android.os.Build.VERSION_CODES;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 import com.android.setupwizardlib.util.Partner.ResourceEntry;
+import com.android.setupwizardlib.util.PartnerTest.ShadowApplicationPackageManager;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
 import org.robolectric.annotation.Config;
-import org.robolectric.res.builder.DefaultPackageManager;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
 import org.robolectric.shadows.ShadowResources;
 
 import java.util.Arrays;
 import java.util.Collections;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
+@Config(
+        sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK },
+        shadows = ShadowApplicationPackageManager.class)
 public class PartnerTest {
 
     private static final String ACTION_PARTNER_CUSTOMIZATION =
@@ -62,7 +66,7 @@
     private Context mContext;
     private Resources mPartnerResources;
 
-    private TestPackageManager mPackageManager;
+    private ShadowApplicationPackageManager mPackageManager;
 
     @Before
     public void setUp() throws Exception {
@@ -71,8 +75,9 @@
         mContext = spy(application);
         mPartnerResources = spy(ShadowResources.getSystem());
 
-        mPackageManager = new TestPackageManager();
-        RuntimeEnvironment.setRobolectricPackageManager(mPackageManager);
+        mPackageManager =
+                (ShadowApplicationPackageManager) Shadows.shadowOf(application.getPackageManager());
+        mPackageManager.partnerResources = mPartnerResources;
     }
 
     @Test
@@ -127,6 +132,20 @@
     }
 
     @Test
+    public void getColor_shouldReturnPartnerValueIfPresent() {
+        final int expectedPartnerColor = 1111;
+        doReturn(12345).when(mPartnerResources)
+                .getIdentifier(eq("suw_color_accent_dark"), eq("color"), anyString());
+        doReturn(expectedPartnerColor).when(mPartnerResources).getColor(eq(12345));
+        mPackageManager.addResolveInfoForIntent(
+                new Intent(ACTION_PARTNER_CUSTOMIZATION),
+                Arrays.asList(createResolveInfo("test.partner.package", true, true)));
+        final int foundColor = Partner.getColor(mContext, R.color.suw_color_accent_dark);
+        assertEquals("Partner color should be overlayed to: " + expectedPartnerColor,
+                expectedPartnerColor, foundColor);
+    }
+
+    @Test
     public void testLoadDefaultValue() {
         mPackageManager.addResolveInfoForIntent(
                 new Intent(ACTION_PARTNER_CUSTOMIZATION),
@@ -173,13 +192,18 @@
         return info;
     }
 
-    private class TestPackageManager extends DefaultPackageManager {
+    @Implements(className = "android.app.ApplicationPackageManager")
+    public static class ShadowApplicationPackageManager extends
+            org.robolectric.shadows.ShadowApplicationPackageManager {
 
+        public Resources partnerResources;
+
+        @Implementation
         @Override
         public Resources getResourcesForApplication(ApplicationInfo app)
                 throws NameNotFoundException {
             if (app != null && "test.partner.package".equals(app.packageName)) {
-                return mPartnerResources;
+                return partnerResources;
             } else {
                 return super.getResourcesForApplication(app);
             }
diff --git a/library/test/robotest/src/com/android/setupwizardlib/util/WizardManagerHelperTest.java b/library/test/robotest/src/com/android/setupwizardlib/util/WizardManagerHelperTest.java
index 4c460c8..0d15ef4 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/util/WizardManagerHelperTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/util/WizardManagerHelperTest.java
@@ -31,7 +31,6 @@
 import android.provider.Settings.Secure;
 import android.support.annotation.StyleRes;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
@@ -39,8 +38,12 @@
 import org.junit.runner.RunWith;
 import org.robolectric.annotation.Config;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = Config.NEWEST_SDK)
+@Config(sdk = Config.NEWEST_SDK)
 public class WizardManagerHelperTest {
 
     @Test
@@ -88,6 +91,14 @@
     }
 
     @Test
+    public void testIsPreDeferredSetupTrue() {
+        final Intent intent = new Intent();
+        intent.putExtra("preDeferredSetup", true);
+        assertTrue("Is pre-deferred setup wizard should be true",
+                WizardManagerHelper.isPreDeferredSetupWizard(intent));
+    }
+
+    @Test
     public void testIsSetupWizardFalse() {
         final Intent intent = new Intent();
         intent.putExtra("firstRun", false);
@@ -96,75 +107,57 @@
     }
 
     @Test
-    public void testHoloIsNotLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "holo");
-        assertFalse("Theme holo should not be light theme",
-                WizardManagerHelper.isLightTheme(intent, true));
+    public void isLightTheme_shouldReturnTrue_whenThemeIsLight() {
+        List<String> lightThemes = Arrays.asList(
+                "holo_light",
+                "material_light",
+                "glif_light",
+                "glif_v2_light",
+                "glif_v3_light"
+        );
+        ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
+        ArrayList<String> unexpectedStringThemes = new ArrayList<>();
+        for (final String theme : lightThemes) {
+            Intent intent = new Intent();
+            intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
+            if (!WizardManagerHelper.isLightTheme(intent, false)) {
+                unexpectedIntentThemes.add(theme);
+            }
+            if (!WizardManagerHelper.isLightTheme(theme, false)) {
+                unexpectedStringThemes.add(theme);
+            }
+        }
+        assertTrue("Intent themes " + unexpectedIntentThemes + " should be light",
+                unexpectedIntentThemes.isEmpty());
+        assertTrue("String themes " + unexpectedStringThemes + " should be light",
+                unexpectedStringThemes.isEmpty());
     }
 
     @Test
-    public void testHoloLightIsLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "holo_light");
-        assertTrue("Theme holo_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-    }
-
-    @Test
-    public void testMaterialIsNotLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "material");
-        assertFalse("Theme material should not be light theme",
-                WizardManagerHelper.isLightTheme(intent, true));
-    }
-
-    @Test
-    public void testMaterialLightIsLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "material_light");
-        assertTrue("Theme material_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-    }
-
-    @Test
-    public void testGlifIsDarkTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "glif");
-        assertFalse("Theme glif should be dark theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-        assertFalse("Theme glif should be dark theme",
-                WizardManagerHelper.isLightTheme(intent, true));
-    }
-
-    @Test
-    public void testGlifLightIsLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "glif_light");
-        assertTrue("Theme glif_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-        assertTrue("Theme glif_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, true));
-    }
-
-    @Test
-    public void testGlifV2IsDarkTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "glif_v2");
-        assertFalse("Theme glif_v2 should be dark theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-        assertFalse("Theme glif_v2 should be dark theme",
-                WizardManagerHelper.isLightTheme(intent, true));
-    }
-
-    @Test
-    public void testGlifV2LightIsLightTheme() {
-        final Intent intent = new Intent();
-        intent.putExtra("theme", "glif_v2_light");
-        assertTrue("Theme glif_v2_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, false));
-        assertTrue("Theme glif_v2_light should be light theme",
-                WizardManagerHelper.isLightTheme(intent, true));
+    public void isLightTheme_shouldReturnFalse_whenThemeIsNotLight() {
+        List<String> lightThemes = Arrays.asList(
+                "holo",
+                "material",
+                "glif",
+                "glif_v2",
+                "glif_v3"
+        );
+        ArrayList<String> unexpectedIntentThemes = new ArrayList<>();
+        ArrayList<String> unexpectedStringThemes = new ArrayList<>();
+        for (final String theme : lightThemes) {
+            Intent intent = new Intent();
+            intent.putExtra(WizardManagerHelper.EXTRA_THEME, theme);
+            if (WizardManagerHelper.isLightTheme(intent, true)) {
+                unexpectedIntentThemes.add(theme);
+            }
+            if (WizardManagerHelper.isLightTheme(theme, true)) {
+                unexpectedStringThemes.add(theme);
+            }
+        }
+        assertTrue("Intent themes " + unexpectedIntentThemes + " should not be light",
+                unexpectedIntentThemes.isEmpty());
+        assertTrue("String themes " + unexpectedStringThemes + " should not be light",
+                unexpectedStringThemes.isEmpty());
     }
 
     @Test
@@ -187,19 +180,15 @@
     }
 
     @Test
-    public void testIsLightThemeString() {
-        assertTrue("isLightTheme should return true for material_light",
-                WizardManagerHelper.isLightTheme("material_light", false));
-        assertFalse("isLightTheme should return false for material",
-                WizardManagerHelper.isLightTheme("material", false));
-        assertTrue("isLightTheme should return true for holo_light",
-                WizardManagerHelper.isLightTheme("holo_light", false));
-        assertFalse("isLightTheme should return false for holo",
-                WizardManagerHelper.isLightTheme("holo", false));
-        assertTrue("isLightTheme should return default value true",
-                WizardManagerHelper.isLightTheme("abracadabra", true));
-        assertFalse("isLightTheme should return default value false",
-                WizardManagerHelper.isLightTheme("abracadabra", false));
+    public void testGetThemeResGlifV3Light() {
+        assertEquals(R.style.SuwThemeGlifV3_Light,
+                WizardManagerHelper.getThemeRes("glif_v3_light", 0));
+    }
+
+    @Test
+    public void testGetThemeResGlifV3() {
+        assertEquals(R.style.SuwThemeGlifV3,
+                WizardManagerHelper.getThemeRes("glif_v3", 0));
     }
 
     @Test
@@ -266,6 +255,7 @@
                 .putExtra(WizardManagerHelper.EXTRA_WIZARD_BUNDLE, wizardBundle)
                 .putExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, true)
                 .putExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, true)
+                .putExtra(WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP, true)
                 // Script URI and Action ID are kept for backwards compatibility
                 .putExtra(WizardManagerHelper.EXTRA_SCRIPT_URI, "test_script_uri")
                 .putExtra(WizardManagerHelper.EXTRA_ACTION_ID, "test_action_id");
@@ -284,6 +274,8 @@
                 intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, false));
         assertTrue("EXTRA_IS_DEFERRED_SETUP should be copied",
                 intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, false));
+        assertTrue("EXTRA_IS_PRE_DEFERRED_SETUP should be copied",
+                intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP, false));
 
         // Script URI and Action ID are replaced by Wizard Bundle in M, but are kept for backwards
         // compatibility
diff --git a/library/test/robotest/src/com/android/setupwizardlib/view/FillContentLayoutTest.java b/library/test/robotest/src/com/android/setupwizardlib/view/FillContentLayoutTest.java
index f1332c0..ae4f3d1 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/view/FillContentLayoutTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/view/FillContentLayoutTest.java
@@ -22,7 +22,6 @@
 import android.view.View;
 import android.view.View.MeasureSpec;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 
 import org.junit.Test;
@@ -31,7 +30,7 @@
 import org.robolectric.annotation.Config;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
-@Config(constants = BuildConfig.class, sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
+@Config(sdk = {Config.OLDEST_SDK, Config.NEWEST_SDK})
 public class FillContentLayoutTest {
 
     @Test
diff --git a/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java b/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
index ffa228d..21822a4 100644
--- a/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/view/IllustrationVideoViewTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -31,9 +32,10 @@
 import android.support.annotation.RawRes;
 import android.view.Surface;
 
-import com.android.setupwizardlib.BuildConfig;
 import com.android.setupwizardlib.R;
 import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
+import com.android.setupwizardlib.shadow.ShadowLog;
+import com.android.setupwizardlib.shadow.ShadowLog.TerribleFailure;
 import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowMockMediaPlayer;
 import com.android.setupwizardlib.view.IllustrationVideoViewTest.ShadowSurface;
 
@@ -48,15 +50,15 @@
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
 import org.robolectric.annotation.RealObject;
-import org.robolectric.internal.Shadow;
+import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowMediaPlayer;
 import org.robolectric.util.ReflectionHelpers;
 
 @RunWith(SuwLibRobolectricTestRunner.class)
 @Config(
-        constants = BuildConfig.class,
         sdk = Config.NEWEST_SDK,
         shadows = {
+                ShadowLog.class,
                 ShadowMockMediaPlayer.class,
                 ShadowSurface.class
         })
@@ -78,6 +80,17 @@
     }
 
     @Test
+    public void nullMediaPlayer_shouldThrowWtf() {
+        ShadowMockMediaPlayer.sMediaPlayer = null;
+        try {
+            createDefaultView();
+            fail("WTF should be thrown for null media player");
+        } catch (TerribleFailure e) {
+            // pass
+        }
+    }
+
+    @Test
     public void testPausedWhenWindowFocusLost() {
         createDefaultView();
         mView.start();
diff --git a/library/test/instrumentation/src/com/android/setupwizardlib/test/RichTextViewTest.java b/library/test/robotest/src/com/android/setupwizardlib/view/RichTextViewTest.java
similarity index 66%
rename from library/test/instrumentation/src/com/android/setupwizardlib/test/RichTextViewTest.java
rename to library/test/robotest/src/com/android/setupwizardlib/view/RichTextViewTest.java
index 5f3eb9f..f77de68 100644
--- a/library/test/instrumentation/src/com/android/setupwizardlib/test/RichTextViewTest.java
+++ b/library/test/robotest/src/com/android/setupwizardlib/view/RichTextViewTest.java
@@ -14,39 +14,45 @@
  * limitations under the License.
  */
 
-package com.android.setupwizardlib.test;
+package com.android.setupwizardlib.view;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.ContextWrapper;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
 import android.text.Annotation;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.style.TextAppearanceSpan;
+import android.view.MotionEvent;
 
+import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
 import com.android.setupwizardlib.span.LinkSpan;
 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
-import com.android.setupwizardlib.view.RichTextView;
+import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
 
 import java.util.Arrays;
 
-@RunWith(AndroidJUnit4.class)
-@SmallTest
+@RunWith(SuwLibRobolectricTestRunner.class)
+@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
 public class RichTextViewTest {
 
     @Test
@@ -55,12 +61,14 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         final CharSequence text = textView.getText();
         assertTrue("Text should be spanned", text instanceof Spanned);
 
+        assertThat(textView.getMovementMethod()).isInstanceOf(TouchableLinkMovementMethod.class);
+
         Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
         assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
 
@@ -77,7 +85,7 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         OnLinkClickListener listener = mock(OnLinkClickListener.class);
@@ -99,7 +107,7 @@
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        TestContext context = spy(new TestContext(InstrumentationRegistry.getTargetContext()));
+        TestContext context = spy(new TestContext(application));
         RichTextView textView = new RichTextView(context);
         textView.setText(ssb);
 
@@ -111,12 +119,50 @@
     }
 
     @Test
+    public void onTouchEvent_clickOnLinks_shouldReturnTrue() {
+        Annotation link = new Annotation("link", "foobar");
+        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+        ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+        RichTextView textView = new RichTextView(application);
+        textView.setText(ssb);
+
+        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+        textView.setMovementMethod(mockMovementMethod);
+
+        MotionEvent motionEvent =
+                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+        doReturn(true).when(mockMovementMethod).isLastTouchEventHandled();
+        assertThat(textView.onTouchEvent(motionEvent)).isTrue();
+    }
+
+    @Test
+    public void onTouchEvent_clickOutsideLinks_shouldReturnFalse() {
+        Annotation link = new Annotation("link", "foobar");
+        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
+        ssb.setSpan(link, 0, 2, 0 /* flags */);
+
+        RichTextView textView = new RichTextView(application);
+        textView.setText(ssb);
+
+        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
+        textView.setMovementMethod(mockMovementMethod);
+
+        MotionEvent motionEvent =
+                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
+        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
+        doReturn(false).when(mockMovementMethod).isLastTouchEventHandled();
+        assertThat(textView.onTouchEvent(motionEvent)).isFalse();
+    }
+
+    @Test
     public void testTextStyle() {
         Annotation link = new Annotation("textAppearance", "foobar");
         SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
         ssb.setSpan(link, 1, 2, 0 /* flags */);
 
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText(ssb);
 
         final CharSequence text = textView.getText();
@@ -137,7 +183,7 @@
         SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
         spannableStringBuilder.setSpan(testLink, 0, 3, 0);
 
-        RichTextView view = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView view = new RichTextView(application);
         view.setText(spannableStringBuilder);
 
         assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
@@ -147,7 +193,7 @@
     @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
     @Test
     public void testTextContainingNoLinksAreNotFocusable() {
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText("Thou shall not be focusable!");
 
         assertFalse("TextView should not be focusable since it does not contain any span",
@@ -160,16 +206,23 @@
     @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
     @Test
     public void testRichTextViewFocusChangesWithTextChange() {
-        RichTextView textView = new RichTextView(InstrumentationRegistry.getContext());
+        RichTextView textView = new RichTextView(application);
         textView.setText("Thou shall not be focusable!");
 
         assertFalse(textView.isFocusable());
+        assertFalse(textView.isFocusableInTouchMode());
 
         SpannableStringBuilder spannableStringBuilder =
                 new SpannableStringBuilder("I am focusable");
         spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
         textView.setText(spannableStringBuilder);
         assertTrue(textView.isFocusable());
+        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+            assertTrue(textView.isFocusableInTouchMode());
+            assertFalse(textView.getRevealOnFocusHint());
+        } else {
+            assertFalse(textView.isFocusableInTouchMode());
+        }
     }
 
     public static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
diff --git a/navigationbar/res/values-en-rCA/strings.xml b/navigationbar/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..b06dc86
--- /dev/null
+++ b/navigationbar/res/values-en-rCA/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="setup_wizard_next_button_label" msgid="6681282266022780599">"Next"</string>
+    <string name="setup_wizard_back_button_label" msgid="2863826823307023546">"Back"</string>
+</resources>
diff --git a/navigationbar/res/values-en-rXC/strings.xml b/navigationbar/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..5c7c658
--- /dev/null
+++ b/navigationbar/res/values-en-rXC/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="setup_wizard_next_button_label" msgid="6681282266022780599">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‏‏‎‎‎‏‎‏‎‏‏‏‎‏‏‎‏‎‎‏‎‎‏‏‎‏‏‏‎‎‏‏‎‎‏‏‏‏‎‎‏‏‏‏‎‏‎‏‏‎‏‏‏‎Next‎‏‎‎‏‎"</string>
+    <string name="setup_wizard_back_button_label" msgid="2863826823307023546">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‎‎‏‎‏‎‎‏‎‏‎‏‎‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‏‎‏‏‏‎‏‎‎Back‎‏‎‎‏‎"</string>
+</resources>
diff --git a/tools/build_for_build_server.sh b/tools/build_for_build_server.sh
index a90ae67..7a8c942 100755
--- a/tools/build_for_build_server.sh
+++ b/tools/build_for_build_server.sh
@@ -6,4 +6,4 @@
 export TARGET_BUILD_TYPE="release"
 export TARGET_BUILD_APPS="setup-wizard-lib"
 
-./gradlew buildProjectFull test
+./gradlew buildProjectFull test coverage
diff --git a/tools/gradle/android.properties b/tools/gradle/android.properties
index 75de660..f62de66 100644
--- a/tools/gradle/android.properties
+++ b/tools/gradle/android.properties
@@ -1,6 +1,6 @@
 // Set the default SDK and build tools version for all apps
-compileSdkVersion 25
-buildToolsVersion = '25.0.0'
+compileSdkVersion 28
+buildToolsVersion = '28.0.0'
 
 // enable Java7
 compileOptions.sourceCompatibility JavaVersion.VERSION_1_7
diff --git a/tools/gradle/dist-unit-tests.gradle b/tools/gradle/dist-unit-tests.gradle
index faae260..aaecd35 100644
--- a/tools/gradle/dist-unit-tests.gradle
+++ b/tools/gradle/dist-unit-tests.gradle
@@ -10,6 +10,7 @@
  */
 
 apply plugin: 'dist'
+apply plugin: 'jacoco'
 
 // If unit tests are run as part of the build, dist the test XML reports to host-test-reports/*.zip
 android.unitTestVariants.all { variant ->
@@ -28,11 +29,80 @@
             archiveName = task.name + 'Result.zip'
             destinationDir = junitReport.destination.parentFile
         }
-        zipTask.mustRunAfter task
+        task.finalizedBy zipTask
 
         // Copy the test reports to dist/host-test-reports
         // The file path and format should match GradleHostBasedTest class in TradeFed.
-        tasks.dist.dependsOn zipTask
+        tasks.dist.mustRunAfter zipTask
         dist.file zipTask.archivePath.path, "host-test-reports/${zipTask.archiveName}"
     }
 }
+
+/*
+ * The section below adds code coverage to all the unitTest targets. By default, the jacoco plugin
+ * only adds to the 'java' plugin and not the Android ones.
+ *
+ * For each unitTest task "fooUnitTest", a new target "fooUnitTestCoverage" will be generated for
+ * to generate the jacoco report.
+ */
+android.testOptions.unitTests.all {
+    // Fix robolectric tests reporting 0 coverage on newer versions of the plugin.
+    jacoco {
+        includeNoLocationClasses = true
+    }
+}
+
+// Define the main coverage task if it does not exist. This task generates coverage report for all
+// unit tests.
+def coverageTask = tasks.findByName('coverage') ?: tasks.create('coverage') {
+    group = "Reporting"
+    description = "Generate Jacoco coverage reports"
+}
+
+android.unitTestVariants.all { variant ->
+    def testTaskName = "test${variant.name.capitalize()}"
+    def testTask = tasks.findByName(testTaskName)
+
+    // Create coverage task of form 'testFlavorCoverageUnitTestCoverage' depending on
+    // 'testFlavorCoverageUnitTest'
+    def jacocoTask = tasks.create("${testTaskName}Coverage", JacocoReport) {
+        group = "Reporting"
+        description = "Generate a Jacoco coverage report for robolectric tests on ${variant.name}."
+
+        classDirectories = fileTree(
+            dir: "${project.buildDir}/intermediates/classes/" +
+                "${variant.productFlavors[0].name}/${variant.buildType.name}",
+            excludes: ['**/R.class',
+                       '**/R$*.class',
+                       '**/BuildConfig.*',
+                       '**/Manifest*.*']
+        )
+
+        sourceDirectories = files(variant.testedVariant.sourceSets.collect { it.java.srcDirs })
+        executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
+
+        reports {
+            xml.enabled = true
+            html.enabled = true
+        }
+    }
+    jacocoTask.dependsOn testTask
+
+    // Create a zip file of the HTML coverage reports
+    def zipTask = tasks.create("zipResultsOf${jacocoTask.name.capitalize()}", Zip) {
+        from jacocoTask.reports.html.destination
+        archiveName = "${testTaskName}HtmlCoverage.zip"
+        destinationDir = jacocoTask.reports.html.destination.parentFile
+    }
+    jacocoTask.finalizedBy zipTask
+
+    // Copy the coverage reports to dist/host-test-coverage
+    // The file path and format should match JacocoLogForwarder class in TradeFed.
+    tasks.dist.mustRunAfter jacocoTask
+    dist.file jacocoTask.reports.xml.destination.path, "host-test-coverage/${jacocoTask.name}.xml"
+
+    tasks.dist.mustRunAfter zipTask
+    dist.file zipTask.archivePath.path, "host-test-coverage/${zipTask.archiveName}"
+
+    coverageTask.dependsOn(jacocoTask)
+}
diff --git a/tools/root.mk b/tools/root.mk
new file mode 100644
index 0000000..11597ea
--- /dev/null
+++ b/tools/root.mk
@@ -0,0 +1,2 @@
+default:
+	TARGET_BUILD_APPS=setup-wizard-lib ./gradlew