ThemePickerLeaf: Import removed customization code from android-14.0.0_r27

* https://android.googlesource.com/platform/packages/apps/ThemePicker/+/f434693af971ad88669f9a63d7ced3ba92b651c3
* https://android.googlesource.com/platform/packages/apps/ThemePicker/+/8c4b2e31310e647ee10da14d7887828b9d17de1f

Change-Id: I7d224fab35bfda3c7c04ed68db7c7e06dbfe0020
diff --git a/res/layout-land/activity_custom_theme.xml b/res/layout-land/activity_custom_theme.xml
new file mode 100644
index 0000000..59296df
--- /dev/null
+++ b/res/layout-land/activity_custom_theme.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.android.customization.picker.theme.CustomThemeActivity">
+
+    <FrameLayout
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom"
+        android:orientation="horizontal">
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:layout_gravity="bottom"/>
+
+        <FrameLayout
+            android:id="@+id/custom_theme_nav"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/custom_theme_nav_height"
+            android:layout_weight="1"
+            android:paddingHorizontal="12dp">
+            <Button
+                android:id="@+id/previous_button"
+                style="@style/ActionSecondaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="start|center_vertical"
+                android:text="@string/custom_theme_previous"/>
+            <Button
+                android:id="@+id/next_button"
+                style="@style/ActionPrimaryButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end|center_vertical"
+                android:text="@string/custom_theme_next"/>
+        </FrameLayout>
+    </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout-land/fragment_custom_theme_component.xml b/res/layout-land/fragment_custom_theme_component.xml
new file mode 100644
index 0000000..2679bdf
--- /dev/null
+++ b/res/layout-land/fragment_custom_theme_component.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="?android:colorPrimary">
+    <include layout="@layout/section_header"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+
+        <FrameLayout
+            android:id="@+id/component_preview_container"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:background="?android:colorPrimary">
+            <include
+                android:id="@+id/component_preview_content"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_marginHorizontal="@dimen/preview_page_horizontal_margin"
+                android:layout_marginTop="@dimen/preview_page_top_margin"
+                android:layout_marginBottom="@dimen/component_preview_page_bottom_margin"
+                layout="@layout/theme_component_preview"/>
+        </FrameLayout>
+        <View
+            android:layout_width="1dp"
+            android:layout_height="match_parent"
+            android:background="?android:colorForeground"/>
+        <LinearLayout
+            android:id="@+id/options_section"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:paddingTop="10dp"
+            android:paddingBottom="@dimen/custom_theme_nav_height"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/component_options_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_margin="10dp"
+                android:textAlignment="center"
+                android:textAppearance="@style/TitleTextAppearance"/>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/options_container"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"/>
+
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout-land/fragment_custom_theme_name.xml b/res/layout-land/fragment_custom_theme_name.xml
new file mode 100644
index 0000000..a60b9c2
--- /dev/null
+++ b/res/layout-land/fragment_custom_theme_name.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="?android:colorPrimary">
+    <include layout="@layout/section_header"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+        <FrameLayout
+            android:id="@+id/component_preview_container"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:paddingTop="@dimen/preview_content_padding_top"
+            android:paddingBottom="@dimen/preview_content_padding_bottom"
+            android:clipToPadding="false"
+            android:background="?android:colorSecondary">
+            <include layout="@layout/theme_preview_card"/>
+        </FrameLayout>
+        <LinearLayout
+            android:id="@+id/options_section"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:paddingTop="10dp"
+            android:paddingBottom="@dimen/custom_theme_nav_height"
+            android:paddingVertical="10dp"
+            android:clipToPadding="false"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/component_options_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_margin="10dp"
+                android:textAlignment="center"
+                android:textAppearance="@style/TitleTextAppearance"/>
+
+            <FrameLayout
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/options_container_height"
+                android:layout_gravity="center">
+
+                <EditText
+                    style="@style/CustomThemeNameEditText"
+                    android:id="@+id/custom_theme_name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:minWidth="300dp"/>
+            </FrameLayout>
+
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout-land/fragment_theme_picker.xml b/res/layout-land/fragment_theme_picker.xml
new file mode 100644
index 0000000..d358037
--- /dev/null
+++ b/res/layout-land/fragment_theme_picker.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="?android:colorPrimary">
+    <include layout="@layout/section_header"/>
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <LinearLayout
+            android:id="@+id/content_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="horizontal">
+
+            <FrameLayout
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1">
+                <FrameLayout
+                    android:id="@+id/preview_card_container"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="@dimen/preview_content_padding_top"
+                    android:paddingBottom="@dimen/preview_content_padding_bottom"
+                    android:clipToPadding="false"
+                    android:background="?android:colorSecondary">
+                    <include layout="@layout/theme_preview_card"/>
+                </FrameLayout>
+            </FrameLayout>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/options_container"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:paddingVertical="10dp" />
+        </LinearLayout>
+
+        <androidx.core.widget.ContentLoadingProgressBar
+            android:id="@+id/loading_indicator"
+            style="@android:style/Widget.DeviceDefault.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="200dp"
+            android:layout_gravity="center_horizontal|top"
+            android:indeterminate="true"/>
+
+        <FrameLayout
+            android:id="@+id/error_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <TextView
+                android:id="@+id/error_message"
+                style="@style/TitleTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:text="@string/something_went_wrong"/>
+        </FrameLayout>
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/activity_custom_theme.xml b/res/layout/activity_custom_theme.xml
new file mode 100644
index 0000000..24d58b7
--- /dev/null
+++ b/res/layout/activity_custom_theme.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context="com.android.customization.picker.theme.CustomThemeActivity">
+
+    <FrameLayout
+        android:id="@+id/fragment_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+
+    <FrameLayout
+        android:id="@+id/custom_theme_nav"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/custom_theme_nav_height"
+        android:paddingHorizontal="12dp"
+        android:background="?android:colorPrimary">
+        <Button
+            android:id="@+id/previous_button"
+            style="@style/ActionSecondaryButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start|center_vertical"
+            android:text="@string/custom_theme_previous"/>
+        <Button
+            android:id="@+id/next_button"
+            style="@style/ActionPrimaryButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end|center_vertical"
+            android:text="@string/custom_theme_next"/>
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/custom_theme_option.xml b/res/layout/custom_theme_option.xml
new file mode 100644
index 0000000..aff43a9
--- /dev/null
+++ b/res/layout/custom_theme_option.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/option_padding_horizontal"
+    android:paddingBottom="@dimen/option_bottom_margin"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/option_label"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginBottom="@dimen/theme_option_label_margin"
+        android:ellipsize="end"
+        android:gravity="center_horizontal"
+        android:maxLines="1"
+        android:textAppearance="@style/OptionTitleTextAppearance"/>
+    <FrameLayout
+        android:id="@+id/option_tile"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="@dimen/option_tile_width"
+        android:layout_gravity="center_horizontal"
+        android:paddingHorizontal="@dimen/option_tile_padding_horizontal"
+        android:paddingVertical="@dimen/option_tile_padding_vertical"
+        android:background="@drawable/option_border_custom">
+        <ImageView
+            android:layout_width="@dimen/option_icon_size"
+            android:layout_height="@dimen/option_icon_size"
+            android:layout_gravity="center"
+            android:src="@drawable/ic_add_24px"
+            android:tint="?android:attr/colorAccent" />
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/fragment_custom_theme_component.xml b/res/layout/fragment_custom_theme_component.xml
new file mode 100644
index 0000000..7bae84b
--- /dev/null
+++ b/res/layout/fragment_custom_theme_component.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="?android:colorPrimary">
+    <include layout="@layout/section_header"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <FrameLayout
+            android:id="@+id/component_preview_container"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:background="?android:colorPrimary"
+            app:layout_constrainedHeight="true"
+            app:layout_constraintBottom_toTopOf="@+id/divider"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.0"
+            app:layout_constraintHeight_percent="@dimen/preview_pager_maximum_height_ratio">
+
+            <include
+                android:id="@+id/component_preview_content"
+                layout="@layout/theme_component_preview"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"/>
+        </FrameLayout>
+
+        <View
+            android:id="@+id/divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?android:colorForeground"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/component_preview_container"
+            app:layout_constraintBottom_toTopOf="@+id/component_scroll_view"/>
+
+        <ScrollView
+            android:id="@+id/component_scroll_view"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/divider"
+            app:layout_constraintBottom_toBottomOf="parent">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="vertical">
+        
+                <TextView
+                    android:id="@+id/component_options_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginVertical="18dp"
+                    android:layout_marginHorizontal="16dp"
+                    android:textAlignment="center"
+                    android:textAppearance="@style/TitleTextAppearance"
+                    android:textSize="@dimen/component_options_title_size" />
+
+                <androidx.recyclerview.widget.RecyclerView
+                    android:id="@+id/options_container"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"/>
+            </LinearLayout>
+        </ScrollView>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
diff --git a/res/layout/fragment_custom_theme_name.xml b/res/layout/fragment_custom_theme_name.xml
new file mode 100644
index 0000000..98edd29
--- /dev/null
+++ b/res/layout/fragment_custom_theme_name.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <include layout="@layout/section_header"/>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <FrameLayout
+            android:id="@+id/component_preview_container"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:paddingTop="@dimen/preview_content_padding_top"
+            android:paddingBottom="@dimen/preview_content_padding_bottom"
+            android:clipToPadding="false"
+            app:layout_constrainedHeight="true"
+            app:layout_constraintBottom_toTopOf="@+id/component_scroll_view"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_bias="0.0"
+            app:layout_constraintHeight_percent="@dimen/preview_pager_maximum_height_ratio">
+
+            <include layout="@layout/theme_preview_card"/>
+        </FrameLayout>
+
+        <ScrollView
+                android:id="@+id/component_scroll_view"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:background="?android:colorPrimary"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/component_preview_container"
+                app:layout_constraintBottom_toBottomOf="parent">
+
+            <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical">
+
+                <TextView
+                        android:id="@+id/component_options_title"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:layout_marginVertical="18dp"
+                        android:layout_marginHorizontal="16dp"
+                        android:textAlignment="center"
+                        android:textAppearance="@style/TitleTextAppearance"
+                        android:textSize="@dimen/component_options_title_size"/>
+
+                <EditText
+                        android:id="@+id/custom_theme_name"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginVertical="16dp"
+                        android:layout_marginHorizontal="16dp"
+                        android:layout_gravity="center|top"
+                        android:importantForAutofill="no"
+                        android:minWidth="300dp"
+                        style="@style/CustomThemeNameEditText"/>
+            </LinearLayout>
+        </ScrollView>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</LinearLayout>
diff --git a/res/layout/fragment_theme_full_preview.xml b/res/layout/fragment_theme_full_preview.xml
new file mode 100644
index 0000000..762af07
--- /dev/null
+++ b/res/layout/fragment_theme_full_preview.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <include layout="@layout/section_header"/>
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:paddingTop="@dimen/full_preview_page_default_padding_top"
+        android:paddingBottom="@dimen/full_preview_page_default_padding_bottom"
+        android:clipToPadding="false">
+
+        <include layout="@layout/theme_preview_card"/>
+    </FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/fragment_theme_picker.xml b/res/layout/fragment_theme_picker.xml
new file mode 100644
index 0000000..0ecfdd0
--- /dev/null
+++ b/res/layout/fragment_theme_picker.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+    <include layout="@layout/section_header"/>
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/content_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+            <FrameLayout
+                android:id="@+id/preview_card_container"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:paddingTop="@dimen/preview_content_padding_top"
+                android:paddingBottom="@dimen/preview_content_padding_bottom"
+                android:clipToPadding="false"
+                app:layout_constrainedHeight="true"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toTopOf="@id/options_container"
+                app:layout_constraintHeight_max="@dimen/preview_pager_max_height"
+                app:layout_constraintVertical_bias="0.0"
+                app:layout_constraintHeight_percent="@dimen/preview_pager_maximum_height_ratio">
+                <include layout="@layout/theme_preview_card"/>
+            </FrameLayout>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/options_container"
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_gravity="bottom|center_horizontal"
+                android:layout_marginTop="10dp"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/preview_card_container"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintVertical_bias="1.0"/>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.core.widget.ContentLoadingProgressBar
+            android:id="@+id/loading_indicator"
+            style="@android:style/Widget.DeviceDefault.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="200dp"
+            android:layout_gravity="center_horizontal|top"
+            android:indeterminate="true"/>
+
+        <FrameLayout
+            android:id="@+id/error_section"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone">
+            <TextView
+                android:id="@+id/error_message"
+                style="@style/TitleTextAppearance"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:gravity="center"
+                android:text="@string/something_went_wrong"/>
+        </FrameLayout>
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/preview_card_color_content.xml b/res/layout/preview_card_color_content.xml
new file mode 100644
index 0000000..9ab90c1
--- /dev/null
+++ b/res/layout/preview_card_color_content.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
+    <LinearLayout
+        android:layout_width="@dimen/preview_theme_color_component_size"
+        android:layout_height="wrap_content"
+        android:gravity="center|bottom"
+        android:orientation="horizontal">
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <ImageView
+                android:layout_width="@dimen/preview_theme_icon_size"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_0_bg"/>
+            <ImageView
+                android:layout_width="@dimen/preview_theme_tile_size"
+                android:layout_height="@dimen/preview_theme_tile_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_0_icon"
+                android:tint="@color/tile_enabled_icon_color"/>
+        </FrameLayout>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <ImageView
+                android:layout_width="@dimen/preview_theme_icon_size"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_1_bg"/>
+            <ImageView
+                android:layout_width="@dimen/preview_theme_tile_size"
+                android:layout_height="@dimen/preview_theme_tile_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_1_icon"
+                android:tint="@color/tile_enabled_icon_color"/>
+        </FrameLayout>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <ImageView
+                android:layout_width="@dimen/preview_theme_icon_size"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_2_bg"/>
+            <ImageView
+                android:layout_width="@dimen/preview_theme_tile_size"
+                android:layout_height="@dimen/preview_theme_tile_size"
+                android:layout_gravity="center"
+                android:id="@+id/preview_color_qs_2_icon"
+                android:color="@color/tile_enabled_icon_color"/>
+        </FrameLayout>
+    </LinearLayout>
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+    <LinearLayout
+        android:layout_width="@dimen/preview_theme_color_component_size"
+        android:layout_height="wrap_content"
+        android:layout_weight="0"
+        android:orientation="horizontal"
+        android:gravity="center">
+        <SeekBar
+            android:id="@+id/preview_seekbar"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
+            android:tint="@color/theme_preview_icon_color"
+            android:maxHeight="2dp"
+            android:progress="1"
+            android:clickable="true"
+            android:max="3"/>
+    </LinearLayout>
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+    <LinearLayout
+        android:layout_width="@dimen/preview_theme_color_component_size"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:orientation="horizontal">
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <CheckBox
+                android:id="@+id/preview_check_selected"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:checked="true"
+                android:enabled="false"/>
+        </FrameLayout>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <RadioButton
+                android:id="@+id/preview_radio_selected"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:checked="true"
+                android:enabled="false"/>
+        </FrameLayout>
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <FrameLayout
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1">
+            <Switch
+                android:id="@+id/preview_toggle_selected"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:checked="true"
+                android:enabled="false"/>
+        </FrameLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/preview_card_font_content.xml b/res/layout/preview_card_font_content.xml
new file mode 100644
index 0000000..408778e
--- /dev/null
+++ b/res/layout/preview_card_font_content.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:orientation="vertical"
+    tools:showIn="@layout/theme_preview_card">
+    <TextView
+        style="@style/FontCardTitleStyle"
+        android:id="@+id/font_card_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:gravity="center_horizontal"
+        android:maxLines="1"
+        android:text="@string/font_card_title"/>
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+    <View
+        android:id="@+id/font_card_divider"
+        android:layout_width="16dp"
+        android:layout_height="2dp"
+        android:layout_gravity="center"
+        android:background="?android:colorAccent"/>
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"/>
+    <TextView
+        style="@style/FontCardBodyTextStyle"
+        android:id="@+id/font_card_body"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|center_horizontal"
+        android:gravity="center_horizontal"
+        android:text="@string/font_card_body"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/preview_card_icon_content.xml b/res/layout/preview_card_icon_content.xml
new file mode 100644
index 0000000..29620c8
--- /dev/null
+++ b/res/layout/preview_card_icon_content.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/preview_icon_0"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+        <Space
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <ImageView
+            android:id="@+id/preview_icon_1"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+        <Space
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <ImageView
+            android:id="@+id/preview_icon_2"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+    </LinearLayout>
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="68dp" />
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="bottom|center_horizontal"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/preview_icon_3"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+        <Space
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <ImageView
+            android:id="@+id/preview_icon_4"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+        <Space
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="match_parent"
+            android:layout_weight="0" />
+        <ImageView
+            android:id="@+id/preview_icon_5"
+            android:layout_width="@dimen/preview_theme_icon_size"
+            android:layout_height="@dimen/preview_theme_icon_size"
+            android:layout_weight="1"
+            android:tint="@color/theme_preview_icon_color"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/preview_card_shape_content.xml b/res/layout/preview_card_shape_content.xml
new file mode 100644
index 0000000..0afa6bc
--- /dev/null
+++ b/res/layout/preview_card_shape_content.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:gravity="center_horizontal"
+    android:orientation="vertical">
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="horizontal">
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_0"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_margin="4dp"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+                <Space
+                    android:layout_width="@dimen/preview_theme_shape_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="0" />
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_1"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_margin="4dp"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+                <Space
+                    android:layout_width="@dimen/preview_theme_shape_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="0" />
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_2"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_margin="4dp"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+        </LinearLayout>
+        <Space
+            android:layout_width="match_parent"
+            android:layout_height="60dp" />
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="bottom|center_horizontal"
+            android:orientation="horizontal">
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_3"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_margin="4dp"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+                <Space
+                    android:layout_width="@dimen/preview_theme_shape_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="0" />
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_4"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_gravity="center_horizontal"
+                            android:layout_margin="4dp"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+                <Space
+                    android:layout_width="@dimen/preview_theme_shape_size"
+                    android:layout_height="match_parent"
+                    android:layout_weight="0" />
+                <FrameLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1">
+                        <ImageView
+                            android:id="@+id/shape_preview_icon_5"
+                            android:layout_width="@dimen/preview_theme_shape_size"
+                            android:layout_height="@dimen/preview_theme_shape_size"
+                            android:layout_margin="4dp"
+                            android:layout_gravity="center_horizontal"
+                            android:elevation="4dp"/>
+                </FrameLayout>
+        </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/theme_color_option.xml b/res/layout/theme_color_option.xml
new file mode 100644
index 0000000..8d55626
--- /dev/null
+++ b/res/layout/theme_color_option.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:layout_marginTop="28dp"
+    android:layout_marginHorizontal="@dimen/component_options_margin_horizontal">
+
+    <ImageView
+        android:id="@+id/option_tile"
+        android:layout_width="@dimen/component_color_chip_container_size"
+        android:layout_height="@dimen/component_color_chip_container_size"
+        android:layout_gravity="center"
+        android:scaleType="center"/>
+</FrameLayout>
diff --git a/res/layout/theme_component_preview.xml b/res/layout/theme_component_preview.xml
new file mode 100644
index 0000000..67abe6b
--- /dev/null
+++ b/res/layout/theme_component_preview.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/theme_preview_card_background"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipToPadding="false"
+    android:maxHeight="@dimen/preview_theme_max_height"
+    android:minHeight="@dimen/preview_theme_min_height"
+    android:paddingTop="64dp">
+
+        <TextView
+            android:id="@+id/theme_preview_card_header"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:drawablePadding="@dimen/theme_preview_header_drawable_padding"
+            android:textAppearance="@style/CardTitleTextAppearance"
+            android:importantForAccessibility="no"
+            app:layout_constraintBottom_toTopOf="@id/theme_preview_card_body_container"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintVertical_chainStyle="spread_inside"
+            tools:text="Default"/>
+
+        <FrameLayout
+            android:id="@+id/theme_preview_card_body_container"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/preview_theme_content_margin"
+            android:clipChildren="false"
+            android:importantForAccessibility="noHideDescendants"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintHeight_max="@dimen/preview_theme_content_max_height"
+            app:layout_constraintHeight_min="@dimen/preview_theme_content_min_height"
+            app:layout_constraintHorizontal_bias="0.5"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@+id/theme_preview_card_header"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/theme_info_view.xml b/res/layout/theme_info_view.xml
new file mode 100644
index 0000000..085a35e
--- /dev/null
+++ b/res/layout/theme_info_view.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 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.customization.picker.theme.ThemeInfoView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:padding="@dimen/wallpaper_info_pane_padding"
+    android:theme="@style/WallpaperPicker.BottomPaneStyle">
+
+    <TextView
+        android:id="@+id/style_info_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="@dimen/theme_info_margin"
+        android:gravity="center"
+        android:lineHeight="24dp"
+        android:textAppearance="@style/SubtitleTextAppearance"
+        android:textColor="?android:textColorPrimary"
+        android:textSize="16sp"
+        android:text="@string/style_info_description"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:gravity="center">
+
+        <TextView
+            android:id="@+id/font_preview"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginHorizontal="@dimen/theme_info_margin"
+            android:importantForAccessibility="no"
+            android:textSize="@dimen/theme_info_text_size"
+            android:textColor="?android:attr/colorForeground"
+            android:text="@string/font_component_option_thumbnail"/>
+
+        <ImageView
+            android:id="@+id/qs_preview_icon"
+            android:layout_width="@dimen/theme_info_icon_size"
+            android:layout_height="@dimen/theme_info_icon_size"
+            android:layout_marginHorizontal="@dimen/theme_info_margin"
+            android:tint="?android:textColorPrimary"/>
+
+        <ImageView
+            android:id="@+id/app_preview_icon"
+            android:layout_width="@dimen/theme_info_icon_size"
+            android:layout_height="@dimen/theme_info_icon_size"
+            android:layout_marginHorizontal="@dimen/theme_info_margin"
+            android:layout_marginVertical="@dimen/theme_info_app_preview_icon_margin"
+            android:elevation="@dimen/theme_info_app_preview_icon_elevation"/>
+
+        <ImageView
+            android:id="@+id/shape_preview_icon"
+            android:layout_width="@dimen/theme_info_icon_size"
+            android:layout_height="@dimen/theme_info_icon_size"
+            android:layout_marginHorizontal="@dimen/theme_info_margin"/>
+    </LinearLayout>
+</com.android.customization.picker.theme.ThemeInfoView>
\ No newline at end of file
diff --git a/res/layout/theme_option.xml b/res/layout/theme_option.xml
new file mode 100644
index 0000000..bdf82d0
--- /dev/null
+++ b/res/layout/theme_option.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="@dimen/option_padding_horizontal"
+    android:paddingBottom="@dimen/option_bottom_margin"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/option_label"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginBottom="@dimen/theme_option_label_margin"
+        android:ellipsize="end"
+        android:gravity="center_horizontal"
+        android:maxLines="1"
+        android:textAppearance="@style/OptionTitleTextAppearance"/>
+    <RelativeLayout
+        android:id="@+id/option_tile"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="@dimen/option_tile_width"
+        android:layout_gravity="center_horizontal"
+        android:paddingHorizontal="@dimen/option_tile_padding_horizontal"
+        android:paddingVertical="@dimen/option_tile_padding_vertical"
+        android:background="@drawable/option_border">
+        <ImageView
+            android:id="@+id/theme_option_icon"
+            android:layout_width="@dimen/theme_option_icon_sample_width"
+            android:layout_height="@dimen/theme_option_icon_sample_height"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentStart="true"
+            android:tint="?android:colorForeground"/>
+        <ImageView
+            android:id="@+id/theme_option_shape"
+            android:layout_width="@dimen/theme_option_shape_sample_width"
+            android:layout_height="@dimen/theme_option_shape_sample_height"
+            android:layout_alignBottom="@+id/theme_option_icon"
+            android:layout_toEndOf="@id/theme_option_icon"
+            android:layout_marginStart="@dimen/theme_option_sample_margin"/>
+        <TextView
+            android:id="@+id/theme_option_font"
+            android:layout_width="@dimen/theme_option_font_sample_width"
+            android:layout_height="@dimen/theme_option_font_sample_height"
+            android:layout_gravity="center"
+            android:layout_below="@id/theme_option_icon"
+            android:layout_marginTop="@dimen/option_bottom_margin"
+            android:autoSizeMaxTextSize="@dimen/theme_option_font_text_size"
+            android:autoSizeMinTextSize="@dimen/theme_option_font_min_text_size"
+            android:autoSizeTextType="uniform"
+            android:gravity="center"
+            android:letterSpacing=".2"
+            android:text="@string/theme_font_example"
+            android:textAlignment="center"
+            android:textSize="@dimen/theme_option_font_text_size" />
+    </RelativeLayout>
+</LinearLayout>
diff --git a/res/layout/theme_preview_app_icon_shape.xml b/res/layout/theme_preview_app_icon_shape.xml
new file mode 100644
index 0000000..fe95f90
--- /dev/null
+++ b/res/layout/theme_preview_app_icon_shape.xml
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/app_row_0"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/app_row_1">
+
+        <LinearLayout
+            android:id="@+id/app_item_0"
+            android:layout_width="@dimen/preview_theme_app_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"
+            android:clipChildren="false"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/app_item_1"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <ImageView
+                android:id="@+id/shape_preview_icon_0"
+                android:layout_width="@dimen/preview_theme_app_icon_size"
+                android:layout_height="@dimen/preview_theme_app_icon_size"
+                android:elevation="4dp"/>
+            <TextView
+                android:id="@+id/shape_preview_icon_app_name_0"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/preview_theme_app_icon_shape_text_margin_top"
+                android:textSize="@dimen/preview_theme_app_icon_shape_text_size"
+                android:lineHeight="20dp"
+                android:singleLine="true"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/app_item_1"
+            android:layout_width="@dimen/preview_theme_app_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"
+            android:clipChildren="false"
+            app:layout_constraintStart_toEndOf="@id/app_item_0"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <ImageView
+                android:id="@+id/shape_preview_icon_1"
+                android:layout_width="@dimen/preview_theme_app_icon_size"
+                android:layout_height="@dimen/preview_theme_app_icon_size"
+                android:elevation="4dp"/>
+            <TextView
+                android:id="@+id/shape_preview_icon_app_name_1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/preview_theme_app_icon_shape_text_margin_top"
+                android:textSize="@dimen/preview_theme_app_icon_shape_text_size"
+                android:lineHeight="20dp"
+                android:singleLine="true"/>
+        </LinearLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/app_row_1"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/app_row_0"
+        app:layout_constraintBottom_toBottomOf="parent">
+        <LinearLayout
+            android:id="@+id/app_item_2"
+            android:layout_width="@dimen/preview_theme_app_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"
+            android:clipChildren="false"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/app_item_3"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <ImageView
+                android:id="@+id/shape_preview_icon_2"
+                android:layout_width="@dimen/preview_theme_app_icon_size"
+                android:layout_height="@dimen/preview_theme_app_icon_size"
+                android:elevation="4dp"/>
+            <TextView
+                android:id="@+id/shape_preview_icon_app_name_2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/preview_theme_app_icon_shape_text_margin_top"
+                android:textSize="@dimen/preview_theme_app_icon_shape_text_size"
+                android:lineHeight="20dp"
+                android:singleLine="true"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/app_item_3"
+            android:layout_width="@dimen/preview_theme_app_icon_size"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="vertical"
+            android:clipChildren="false"
+            app:layout_constraintStart_toEndOf="@id/app_item_2"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <ImageView
+                android:id="@+id/shape_preview_icon_3"
+                android:layout_width="@dimen/preview_theme_app_icon_size"
+                android:layout_height="@dimen/preview_theme_app_icon_size"
+                android:elevation="4dp"/>
+            <TextView
+                android:id="@+id/shape_preview_icon_app_name_3"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/preview_theme_app_icon_shape_text_margin_top"
+                android:textSize="@dimen/preview_theme_app_icon_shape_text_size"
+                android:lineHeight="20dp"
+                android:singleLine="true"/>
+        </LinearLayout>
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/theme_preview_card.xml b/res/layout/theme_preview_card.xml
new file mode 100644
index 0000000..4fc8995
--- /dev/null
+++ b/res/layout/theme_preview_card.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 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.
+-->
+<androidx.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/FullContentPreviewCard"
+    android:id="@+id/theme_preview_card"
+    android:contentDescription="@string/theme_preview_card_content_description"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center">
+
+    <ImageView
+        android:id="@+id/wallpaper_preview_image"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?android:colorPrimary" />
+
+    <SurfaceView
+        android:id="@+id/wallpaper_preview_surface"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <FrameLayout
+        android:id="@+id/theme_preview_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:importantForAccessibility="noHideDescendants" />
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/res/layout/theme_preview_color_icons.xml b/res/layout/theme_preview_color_icons.xml
new file mode 100644
index 0000000..e87a7a1
--- /dev/null
+++ b/res/layout/theme_preview_color_icons.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 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.
+-->
+<androidx.cardview.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/color_icons_section"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingHorizontal="@dimen/preview_theme_color_icons_padding_horizontal"
+        android:paddingTop="@dimen/preview_theme_color_icons_padding_top"
+        android:paddingBottom="@dimen/preview_theme_color_icons_padding_bottom"
+        android:orientation="vertical"
+        android:background="?android:colorBackground">
+
+        <!-- Title -->
+        <TextView
+            android:id="@+id/color_icons_section_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/theme_preview_icons_section_title"
+            android:textSize="@dimen/preview_theme_color_icons_title_text_size"
+            android:textColor="?android:textColorSecondary"
+            android:lineHeight="16dp"
+            android:gravity="center"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toTopOf="@id/qs_icons"
+            app:layout_constraintVertical_bias="0.0"
+            app:layout_constraintVertical_chainStyle="spread_inside" />
+
+        <!-- QS icons -->
+        <LinearLayout
+            android:id="@+id/qs_icons"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/color_icons_section_title"
+            app:layout_constraintBottom_toTopOf="@id/button_icons">
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                android:layout_height="@dimen/preview_theme_color_icons_icon_size">
+                <ImageView
+                    android:id="@+id/preview_color_qs_0_bg"
+                    android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_icon_size"/>
+                <ImageView
+                    android:id="@+id/preview_color_qs_0_icon"
+                    android:layout_width="@dimen/preview_theme_color_icons_tile_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_tile_size"
+                    android:tint="?android:textColorPrimary"
+                    android:layout_gravity="center"/>
+            </FrameLayout>
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"/>
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                android:layout_height="@dimen/preview_theme_color_icons_icon_size"
+                android:layout_gravity="center_horizontal">
+                <ImageView
+                    android:id="@+id/preview_color_qs_1_bg"
+                    android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_icon_size"/>
+                <ImageView
+                    android:id="@+id/preview_color_qs_1_icon"
+                    android:layout_width="@dimen/preview_theme_color_icons_tile_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_tile_size"
+                    android:tint="?android:textColorPrimary"
+                    android:layout_gravity="center"/>
+            </FrameLayout>
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"/>
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                android:layout_height="@dimen/preview_theme_color_icons_icon_size">
+                <ImageView
+                    android:id="@+id/preview_color_qs_2_bg"
+                    android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_icon_size"/>
+                <ImageView
+                    android:id="@+id/preview_color_qs_2_icon"
+                    android:layout_width="@dimen/preview_theme_color_icons_tile_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_tile_size"
+                    android:tint="?android:textColorPrimary"
+                    android:layout_gravity="center"/>
+            </FrameLayout>
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"/>
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                android:layout_height="@dimen/preview_theme_color_icons_icon_size">
+                <ImageView
+                    android:id="@+id/preview_color_qs_3_bg"
+                    android:layout_width="@dimen/preview_theme_color_icons_icon_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_icon_size"/>
+                <ImageView
+                    android:id="@+id/preview_color_qs_3_icon"
+                    android:layout_width="@dimen/preview_theme_color_icons_tile_size"
+                    android:layout_height="@dimen/preview_theme_color_icons_tile_size"
+                    android:tint="?android:textColorPrimary"
+                    android:layout_gravity="center"/>
+            </FrameLayout>
+        </LinearLayout>
+
+        <!-- Icons of CheckBox/RadioButton/Switch. -->
+        <RelativeLayout
+            android:id="@+id/button_icons"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/qs_icons"
+            app:layout_constraintBottom_toBottomOf="parent">
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_icon_size"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_alignParentStart="true">
+                <CheckBox
+                    android:id="@+id/preview_check_selected"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:checked="true"
+                    android:enabled="false"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="@dimen/preview_theme_icon_size"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_centerHorizontal="true">
+                <RadioButton
+                    android:id="@+id/preview_radio_selected"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:checked="true"
+                    android:enabled="false"/>
+            </FrameLayout>
+
+            <FrameLayout
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/preview_theme_icon_size"
+                android:layout_alignParentEnd="true">
+                <Switch
+                    android:id="@+id/preview_toggle_selected"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:checked="true"
+                    android:enabled="false"/>
+            </FrameLayout>
+        </RelativeLayout>
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</androidx.cardview.widget.CardView>
diff --git a/res/layout/theme_preview_content.xml b/res/layout/theme_preview_content.xml
new file mode 100644
index 0000000..4b29617
--- /dev/null
+++ b/res/layout/theme_preview_content.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2020 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingTop="@dimen/preview_theme_content_padding_top"
+    android:paddingBottom="@dimen/preview_theme_content_padding_bottom"
+    android:clipToPadding="false"
+    android:clipChildren="false">
+
+    <FrameLayout
+        android:id="@+id/topbar_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/preview_theme_topbar_container_margin_horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/smart_space_date"
+        app:layout_constraintVertical_bias="0.0"
+        app:layout_constraintVertical_chainStyle="spread_inside">
+        <include layout="@layout/theme_preview_topbar" />
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/smart_space_date"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:textSize="@dimen/preview_theme_smart_space_date_size"
+        android:singleLine="true"
+        android:gravity="center|bottom"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/topbar_container"
+        app:layout_constraintBottom_toTopOf="@id/app_icon_shape_container"
+        app:layout_constraintHeight_percent="0.1" />
+
+    <FrameLayout
+        android:id="@+id/app_icon_shape_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/smart_space_date"
+        app:layout_constraintBottom_toTopOf="@id/color_icons_container"
+        app:layout_constraintHeight_percent="0.49">
+        <include layout="@layout/theme_preview_app_icon_shape" />
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/color_icons_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginHorizontal="@dimen/preview_theme_color_icons_container_margin_horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/app_icon_shape_container"
+        app:layout_constraintBottom_toTopOf="@id/theme_qsb_container"
+        app:layout_constraintHeight_percent="0.275">
+        <include layout="@layout/theme_preview_color_icons" />
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/theme_qsb_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginHorizontal="@dimen/preview_theme_qsb_container_margin_horizontal"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/color_icons_container"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintHeight_percent="0.1">
+        <include layout="@layout/theme_cover_qsb" />
+    </FrameLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/theme_preview_topbar.xml b/res/layout/theme_preview_topbar.xml
new file mode 100644
index 0000000..af69de9
--- /dev/null
+++ b/res/layout/theme_preview_topbar.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/theme_preview_top_bar"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    tools:visibility="visible"
+    tools:showIn="@layout/theme_preview_card">
+    <TextView
+        android:id="@+id/theme_preview_clock"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start|center_vertical"
+        android:textColor="?android:textColorSecondary"
+        android:textSize="@dimen/preview_theme_cover_topbar_clock_size"
+        tools:text="8:10"/>
+    <LinearLayout
+        android:id="@+id/theme_preview_top_bar_icons"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end|center_vertical"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/preview_icon_0"
+            android:layout_width="@dimen/preview_theme_cover_topbar_icon_size"
+            android:layout_height="@dimen/preview_theme_cover_topbar_icon_size"
+            android:tint="?android:textColorSecondary"/>
+        <ImageView
+            android:id="@+id/preview_icon_1"
+            android:layout_width="@dimen/preview_theme_cover_topbar_icon_size"
+            android:layout_height="@dimen/preview_theme_cover_topbar_icon_size"
+            android:layout_marginHorizontal="8dp"
+            android:tint="?android:textColorSecondary"/>
+        <ImageView
+            android:id="@+id/preview_icon_2"
+            android:layout_width="@dimen/preview_theme_cover_topbar_icon_size"
+            android:layout_height="@dimen/preview_theme_cover_topbar_icon_size"
+            android:tint="?android:textColorSecondary"/>
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/menu/custom_theme_editor_menu.xml b/res/menu/custom_theme_editor_menu.xml
new file mode 100644
index 0000000..7019181
--- /dev/null
+++ b/res/menu/custom_theme_editor_menu.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/custom_theme_delete"
+        android:title="@string/custom_theme_delete"
+        android:icon="@drawable/ic_delete_24px"
+        android:showAsAction="always"/>
+</menu>
\ No newline at end of file
diff --git a/src/com/android/customization/model/DefaultThemeProvider.java b/src/com/android/customization/model/DefaultThemeProvider.java
new file mode 100644
index 0000000..89067c6
--- /dev/null
+++ b/src/com/android/customization/model/DefaultThemeProvider.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.ResourcesApkProvider;
+import com.android.customization.model.theme.ThemeBundle.Builder;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.module.CustomizationPreferences;
+import com.android.wallpaper.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default implementation of {@link ThemeBundleProvider} that reads Themes' overlays from a stub APK.
+ */
+public class DefaultThemeProvider extends ResourcesApkProvider implements ThemeBundleProvider {
+
+    private static final String TAG = "DefaultThemeProvider";
+
+    private static final String THEMES_ARRAY = "themes";
+    private static final String TITLE_PREFIX = "theme_title_";
+    private static final String FONT_PREFIX = "theme_overlay_font_";
+    private static final String COLOR_PREFIX = "theme_overlay_color_";
+    private static final String SHAPE_PREFIX = "theme_overlay_shape_";
+    private static final String ICON_ANDROID_PREFIX = "theme_overlay_icon_android_";
+    private static final String ICON_LAUNCHER_PREFIX = "theme_overlay_icon_launcher_";
+    private static final String ICON_THEMEPICKER_PREFIX = "theme_overlay_icon_themepicker_";
+    private static final String ICON_SETTINGS_PREFIX = "theme_overlay_icon_settings_";
+    private static final String ICON_SYSUI_PREFIX = "theme_overlay_icon_sysui_";
+
+    private static final String DEFAULT_THEME_NAME= "default";
+    private static final String THEME_TITLE_FIELD = "_theme_title";
+    private static final String THEME_ID_FIELD = "_theme_id";
+
+    private final OverlayThemeExtractor mOverlayProvider;
+    private List<ThemeBundle> mThemes;
+    private final CustomizationPreferences mCustomizationPreferences;
+
+    public DefaultThemeProvider(Context context, CustomizationPreferences customizationPrefs) {
+        super(context, context.getString(R.string.themes_stub_package));
+        mOverlayProvider = new OverlayThemeExtractor(context);
+        mCustomizationPreferences = customizationPrefs;
+    }
+
+    @Override
+    public void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload) {
+        if (mThemes == null || reload) {
+            mThemes = new ArrayList<>();
+            loadAll();
+        }
+
+        if(callback != null) {
+            callback.onOptionsLoaded(mThemes);
+        }
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mOverlayProvider.isAvailable() && super.isAvailable();
+    }
+
+    private void loadAll() {
+        // Add "Custom" option at the beginning.
+        mThemes.add(new CustomTheme.Builder()
+                .setId(CustomTheme.newId())
+                .setTitle(mContext.getString(R.string.custom_theme))
+                .build(mContext));
+
+        addDefaultTheme();
+
+        String[] themeNames = getItemsFromStub(THEMES_ARRAY);
+
+        for (String themeName : themeNames) {
+            // Default theme needs special treatment (see #addDefaultTheme())
+            if (DEFAULT_THEME_NAME.equals(themeName)) {
+                continue;
+            }
+            ThemeBundle.Builder builder = new Builder();
+            try {
+                builder.setTitle(mStubApkResources.getString(
+                        mStubApkResources.getIdentifier(TITLE_PREFIX + themeName,
+                                "string", mStubPackageName)));
+
+                String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, themeName);
+                mOverlayProvider.addShapeOverlay(builder, shapeOverlayPackage);
+
+                String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, themeName);
+                mOverlayProvider.addFontOverlay(builder, fontOverlayPackage);
+
+                String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, themeName);
+                mOverlayProvider.addColorOverlay(builder, colorOverlayPackage);
+
+                String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX,
+                        themeName);
+
+                mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage);
+
+                String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX, themeName);
+
+                mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage);
+
+                String iconLauncherOverlayPackage = getOverlayPackage(ICON_LAUNCHER_PREFIX,
+                        themeName);
+                mOverlayProvider.addNoPreviewIconOverlay(builder, iconLauncherOverlayPackage);
+
+                String iconThemePickerOverlayPackage = getOverlayPackage(ICON_THEMEPICKER_PREFIX,
+                        themeName);
+                mOverlayProvider.addNoPreviewIconOverlay(builder,
+                        iconThemePickerOverlayPackage);
+
+                String iconSettingsOverlayPackage = getOverlayPackage(ICON_SETTINGS_PREFIX,
+                        themeName);
+
+                mOverlayProvider.addNoPreviewIconOverlay(builder, iconSettingsOverlayPackage);
+
+                mThemes.add(builder.build(mContext));
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load part of theme %s, will skip it", themeName),
+                        e);
+            }
+        }
+
+        addCustomThemes();
+    }
+
+    /**
+     * Default theme requires different treatment: if there are overlay packages specified in the
+     * stub apk, we'll use those, otherwise we'll get the System default values. But we cannot skip
+     * the default theme.
+     */
+    private void addDefaultTheme() {
+        ThemeBundle.Builder builder = new Builder().asDefault();
+
+        int titleId = mStubApkResources.getIdentifier(TITLE_PREFIX + DEFAULT_THEME_NAME,
+                "string", mStubPackageName);
+        if (titleId > 0) {
+            builder.setTitle(mStubApkResources.getString(titleId));
+        } else {
+            builder.setTitle(mContext.getString(R.string.default_theme_title));
+        }
+
+        try {
+            String colorOverlayPackage = getOverlayPackage(COLOR_PREFIX, DEFAULT_THEME_NAME);
+            mOverlayProvider.addColorOverlay(builder, colorOverlayPackage);
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.d(TAG, "Didn't find color overlay for default theme, will use system default");
+            mOverlayProvider.addSystemDefaultColor(builder);
+        }
+
+        try {
+            String fontOverlayPackage = getOverlayPackage(FONT_PREFIX, DEFAULT_THEME_NAME);
+            mOverlayProvider.addFontOverlay(builder, fontOverlayPackage);
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.d(TAG, "Didn't find font overlay for default theme, will use system default");
+            mOverlayProvider.addSystemDefaultFont(builder);
+        }
+
+        try {
+            String shapeOverlayPackage = getOverlayPackage(SHAPE_PREFIX, DEFAULT_THEME_NAME);
+            mOverlayProvider.addShapeOverlay(builder ,shapeOverlayPackage, false);
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.d(TAG, "Didn't find shape overlay for default theme, will use system default");
+            mOverlayProvider.addSystemDefaultShape(builder);
+        }
+
+        List<ShapeAppIcon> icons = new ArrayList<>();
+        for (String packageName : mOverlayProvider.getShapePreviewIconPackages()) {
+            Drawable icon = null;
+            CharSequence name = null;
+            try {
+                icon = mContext.getPackageManager().getApplicationIcon(packageName);
+                ApplicationInfo appInfo = mContext.getPackageManager()
+                        .getApplicationInfo(packageName, /* flag= */ 0);
+                name = mContext.getPackageManager().getApplicationLabel(appInfo);
+            } catch (NameNotFoundException e) {
+                Log.d(TAG, "Couldn't find app " + packageName + ", won't use it for icon shape"
+                        + "preview");
+            } finally {
+                if (icon != null && !TextUtils.isEmpty(name)) {
+                    icons.add(new ShapeAppIcon(icon, name));
+                }
+            }
+        }
+        builder.setShapePreviewIcons(icons);
+
+        try {
+            String iconAndroidOverlayPackage = getOverlayPackage(ICON_ANDROID_PREFIX,
+                    DEFAULT_THEME_NAME);
+            mOverlayProvider.addAndroidIconOverlay(builder, iconAndroidOverlayPackage);
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.d(TAG, "Didn't find Android icons overlay for default theme, using system default");
+            mOverlayProvider.addSystemDefaultIcons(builder, ANDROID_PACKAGE, ICONS_FOR_PREVIEW);
+        }
+
+        try {
+            String iconSysUiOverlayPackage = getOverlayPackage(ICON_SYSUI_PREFIX,
+                    DEFAULT_THEME_NAME);
+            mOverlayProvider.addSysUiIconOverlay(builder, iconSysUiOverlayPackage);
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.d(TAG,
+                    "Didn't find SystemUi icons overlay for default theme, using system default");
+            mOverlayProvider.addSystemDefaultIcons(builder, SYSUI_PACKAGE, ICONS_FOR_PREVIEW);
+        }
+
+        mThemes.add(builder.build(mContext));
+    }
+
+    @Override
+    public void storeCustomTheme(CustomTheme theme) {
+        if (mThemes == null) {
+            fetch(options -> {
+                addCustomThemeAndStore(theme);
+            }, false);
+        } else {
+            addCustomThemeAndStore(theme);
+        }
+    }
+
+    private void addCustomThemeAndStore(CustomTheme theme) {
+        if (!mThemes.contains(theme)) {
+            mThemes.add(theme);
+        } else {
+            mThemes.replaceAll(t -> theme.equals(t) ? theme : t);
+        }
+        JSONArray themesArray = new JSONArray();
+        mThemes.stream()
+                .filter(themeBundle -> themeBundle instanceof CustomTheme
+                        && !themeBundle.getPackagesByCategory().isEmpty())
+                .forEachOrdered(themeBundle -> addThemeBundleToArray(themesArray, themeBundle));
+        mCustomizationPreferences.storeCustomThemes(themesArray.toString());
+    }
+
+    private void addThemeBundleToArray(JSONArray themesArray, ThemeBundle themeBundle) {
+        JSONObject jsonPackages = themeBundle.getJsonPackages(false);
+        try {
+            jsonPackages.put(THEME_TITLE_FIELD, themeBundle.getTitle());
+            if (themeBundle instanceof CustomTheme) {
+                jsonPackages.put(THEME_ID_FIELD, ((CustomTheme)themeBundle).getId());
+            }
+        } catch (JSONException e) {
+            Log.w("Exception saving theme's title", e);
+        }
+        themesArray.put(jsonPackages);
+    }
+
+    @Override
+    public void removeCustomTheme(CustomTheme theme) {
+        JSONArray themesArray = new JSONArray();
+        mThemes.stream()
+                .filter(themeBundle -> themeBundle instanceof CustomTheme
+                        && ((CustomTheme) themeBundle).isDefined())
+                .forEachOrdered(customTheme -> {
+                    if (!customTheme.equals(theme)) {
+                        addThemeBundleToArray(themesArray, customTheme);
+                    }
+                });
+        mCustomizationPreferences.storeCustomThemes(themesArray.toString());
+    }
+
+    private void addCustomThemes() {
+        String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes();
+        int customThemesCount = 0;
+        if (!TextUtils.isEmpty(serializedThemes)) {
+            try {
+                JSONArray customThemes = new JSONArray(serializedThemes);
+                for (int i = 0; i < customThemes.length(); i++) {
+                    JSONObject jsonTheme = customThemes.getJSONObject(i);
+                    CustomTheme.Builder builder = new CustomTheme.Builder();
+                    try {
+                        convertJsonToBuilder(jsonTheme, builder);
+                    } catch (NameNotFoundException | NotFoundException e) {
+                        Log.i(TAG, "Couldn't parse serialized custom theme", e);
+                        builder = null;
+                    }
+                    if (builder != null) {
+                        if (TextUtils.isEmpty(builder.getTitle())) {
+                            builder.setTitle(mContext.getString(R.string.custom_theme_title,
+                                    customThemesCount + 1));
+                        }
+                        mThemes.add(builder.build(mContext));
+                    } else {
+                        Log.w(TAG, "Couldn't read stored custom theme, resetting");
+                        mThemes.add(new CustomTheme.Builder()
+                                .setId(CustomTheme.newId())
+                                .setTitle(mContext.getString(
+                                        R.string.custom_theme_title, customThemesCount + 1))
+                                .build(mContext));
+                    }
+                    customThemesCount++;
+                }
+            } catch (JSONException e) {
+                Log.w(TAG, "Couldn't read stored custom theme, resetting", e);
+                mThemes.add(new CustomTheme.Builder()
+                        .setId(CustomTheme.newId())
+                        .setTitle(mContext.getString(
+                                R.string.custom_theme_title, customThemesCount + 1))
+                        .build(mContext));
+            }
+        }
+    }
+
+    @Nullable
+    @Override
+    public ThemeBundle.Builder parseThemeBundle(String serializedTheme) throws JSONException {
+        JSONObject theme = new JSONObject(serializedTheme);
+        try {
+            ThemeBundle.Builder builder = new ThemeBundle.Builder();
+            convertJsonToBuilder(theme, builder);
+            return builder;
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.i(TAG, "Couldn't parse serialized custom theme", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException {
+        JSONObject theme = new JSONObject(serializedTheme);
+        try {
+            CustomTheme.Builder builder = new CustomTheme.Builder();
+            convertJsonToBuilder(theme, builder);
+            return builder;
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.i(TAG, "Couldn't parse serialized custom theme", e);
+            return null;
+        }
+    }
+
+    private void convertJsonToBuilder(JSONObject theme, ThemeBundle.Builder builder)
+            throws JSONException, NameNotFoundException, NotFoundException {
+        Map<String, String> customPackages = new HashMap<>();
+        Iterator<String> keysIterator = theme.keys();
+
+        while (keysIterator.hasNext()) {
+            String category = keysIterator.next();
+            customPackages.put(category, theme.getString(category));
+        }
+        mOverlayProvider.addShapeOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_SHAPE));
+        mOverlayProvider.addFontOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_FONT));
+        mOverlayProvider.addColorOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_COLOR));
+        mOverlayProvider.addAndroidIconOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_ICON_ANDROID));
+        mOverlayProvider.addSysUiIconOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_ICON_SYSUI));
+        mOverlayProvider.addNoPreviewIconOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_ICON_SETTINGS));
+        mOverlayProvider.addNoPreviewIconOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER));
+        mOverlayProvider.addNoPreviewIconOverlay(builder,
+                customPackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER));
+        if (theme.has(THEME_TITLE_FIELD)) {
+            builder.setTitle(theme.getString(THEME_TITLE_FIELD));
+        }
+        if (builder instanceof CustomTheme.Builder && theme.has(THEME_ID_FIELD)) {
+            ((CustomTheme.Builder) builder).setId(theme.getString(THEME_ID_FIELD));
+        }
+    }
+
+    @Override
+    public ThemeBundle findEquivalent(ThemeBundle other) {
+        if (mThemes == null) {
+            return null;
+        }
+        for (ThemeBundle theme : mThemes) {
+            if (theme.isEquivalent(other)) {
+                return theme;
+            }
+        }
+        return null;
+    }
+
+    private String getOverlayPackage(String prefix, String themeName) {
+        return getItemStringFromStub(prefix, themeName);
+    }
+}
diff --git a/src/com/android/customization/model/theme/OverlayThemeExtractor.java b/src/com/android/customization/model/theme/OverlayThemeExtractor.java
new file mode 100644
index 0000000..816176e
--- /dev/null
+++ b/src/com/android/customization/model/theme/OverlayThemeExtractor.java
@@ -0,0 +1,293 @@
+package com.android.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
+import static com.android.customization.model.ResourceConstants.SETTINGS_PACKAGE;
+import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
+
+import android.content.Context;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayManager;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.ThemeBundle.Builder;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+class OverlayThemeExtractor {
+
+    private static final String TAG = "OverlayThemeExtractor";
+
+    private final Context mContext;
+    private final Map<String, OverlayInfo> mOverlayInfos = new HashMap<>();
+    // List of packages
+    private final String[] mShapePreviewIconPackages;
+
+    OverlayThemeExtractor(Context context) {
+        mContext = context;
+        OverlayManager om = context.getSystemService(OverlayManager.class);
+        if (om != null) {
+            Consumer<OverlayInfo> addToMap = overlayInfo -> mOverlayInfos.put(
+                    overlayInfo.getPackageName(), overlayInfo);
+
+            UserHandle user = UserHandle.of(UserHandle.myUserId());
+            om.getOverlayInfosForTarget(ANDROID_PACKAGE, user).forEach(addToMap);
+            om.getOverlayInfosForTarget(SYSUI_PACKAGE, user).forEach(addToMap);
+            om.getOverlayInfosForTarget(SETTINGS_PACKAGE, user).forEach(addToMap);
+            om.getOverlayInfosForTarget(ResourceConstants.getLauncherPackage(context), user)
+                    .forEach(addToMap);
+            om.getOverlayInfosForTarget(context.getPackageName(), user).forEach(addToMap);
+        }
+        mShapePreviewIconPackages = context.getResources().getStringArray(
+                R.array.icon_shape_preview_packages);
+    }
+
+    boolean isAvailable() {
+        return !mOverlayInfos.isEmpty();
+    }
+
+    void addColorOverlay(Builder builder, String colorOverlayPackage)
+            throws NameNotFoundException {
+        if (!TextUtils.isEmpty(colorOverlayPackage)) {
+            builder.addOverlayPackage(getOverlayCategory(colorOverlayPackage),
+                    colorOverlayPackage)
+                    .setColorAccentLight(loadColor(ResourceConstants.ACCENT_COLOR_LIGHT_NAME,
+                            colorOverlayPackage))
+                    .setColorAccentDark(loadColor(ResourceConstants.ACCENT_COLOR_DARK_NAME,
+                            colorOverlayPackage));
+        } else {
+            addSystemDefaultColor(builder);
+        }
+    }
+
+    void addShapeOverlay(Builder builder, String shapeOverlayPackage)
+            throws NameNotFoundException {
+        addShapeOverlay(builder, shapeOverlayPackage, true);
+    }
+
+    void addShapeOverlay(Builder builder, String shapeOverlayPackage, boolean addPreview)
+            throws NameNotFoundException {
+        if (!TextUtils.isEmpty(shapeOverlayPackage)) {
+            builder.addOverlayPackage(getOverlayCategory(shapeOverlayPackage),
+                    shapeOverlayPackage)
+                    .setShapePath(
+                            loadString(ResourceConstants.CONFIG_ICON_MASK, shapeOverlayPackage))
+                    .setBottomSheetCornerRadius(
+                            loadDimen(ResourceConstants.CONFIG_CORNERRADIUS, shapeOverlayPackage));
+        } else {
+            addSystemDefaultShape(builder);
+        }
+        if (addPreview) {
+            addShapePreviewIcons(builder);
+        }
+    }
+
+    private void addShapePreviewIcons(Builder builder) {
+        List<ShapeAppIcon> icons = new ArrayList<>();
+        for (String packageName : mShapePreviewIconPackages) {
+            Drawable icon = null;
+            CharSequence name = null;
+            try {
+                icon = mContext.getPackageManager().getApplicationIcon(packageName);
+                // Add the shape icon app name.
+                ApplicationInfo appInfo = mContext.getPackageManager()
+                        .getApplicationInfo(packageName, /* flag= */ 0);
+                name = mContext.getPackageManager().getApplicationLabel(appInfo);
+            } catch (NameNotFoundException e) {
+                Log.d(TAG, "Couldn't find app " + packageName
+                        + ", won't use it for icon shape preview");
+            } finally {
+                if (icon != null && !TextUtils.isEmpty(name)) {
+                    icons.add(new ShapeAppIcon(icon, name));
+                }
+            }
+        }
+        builder.setShapePreviewIcons(icons);
+    }
+
+    void addNoPreviewIconOverlay(Builder builder, String overlayPackage) {
+        if (!TextUtils.isEmpty(overlayPackage)) {
+            builder.addOverlayPackage(getOverlayCategory(overlayPackage),
+                    overlayPackage);
+        }
+    }
+
+    void addSysUiIconOverlay(Builder builder, String iconSysUiOverlayPackage)
+            throws NameNotFoundException {
+        if (!TextUtils.isEmpty(iconSysUiOverlayPackage)) {
+            addIconOverlay(builder, iconSysUiOverlayPackage);
+        }
+    }
+
+    void addAndroidIconOverlay(Builder builder, String iconAndroidOverlayPackage)
+            throws NameNotFoundException {
+        if (!TextUtils.isEmpty(iconAndroidOverlayPackage)) {
+            addIconOverlay(builder, iconAndroidOverlayPackage, ICONS_FOR_PREVIEW);
+        } else {
+            addSystemDefaultIcons(builder, ANDROID_PACKAGE, ICONS_FOR_PREVIEW);
+        }
+    }
+
+    void addIconOverlay(Builder builder, String packageName, String... previewIcons)
+            throws NameNotFoundException {
+        builder.addOverlayPackage(getOverlayCategory(packageName), packageName);
+        for (String iconName : previewIcons) {
+            builder.addIcon(loadIconPreviewDrawable(iconName, packageName, false));
+        }
+    }
+
+    void addFontOverlay(Builder builder, String fontOverlayPackage)
+            throws NameNotFoundException {
+        if (!TextUtils.isEmpty(fontOverlayPackage)) {
+            builder.addOverlayPackage(getOverlayCategory(fontOverlayPackage),
+                    fontOverlayPackage)
+                    .setBodyFontFamily(loadTypeface(
+                            ResourceConstants.CONFIG_BODY_FONT_FAMILY,
+                            fontOverlayPackage))
+                    .setHeadlineFontFamily(loadTypeface(
+                            ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY,
+                            fontOverlayPackage));
+        } else {
+            addSystemDefaultFont(builder);
+        }
+    }
+
+    void addSystemDefaultIcons(Builder builder, String packageName,
+            String... previewIcons) {
+        try {
+            for (String iconName : previewIcons) {
+                builder.addIcon(loadIconPreviewDrawable(iconName, packageName, true));
+            }
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.w(TAG, "Didn't find android package icons, will skip preview", e);
+        }
+    }
+
+    void addSystemDefaultShape(Builder builder) {
+        Resources system = Resources.getSystem();
+        String iconMaskPath = system.getString(
+                system.getIdentifier(ResourceConstants.CONFIG_ICON_MASK,
+                        "string", ResourceConstants.ANDROID_PACKAGE));
+        builder.setShapePath(iconMaskPath)
+                .setBottomSheetCornerRadius(
+                        system.getDimensionPixelOffset(
+                                system.getIdentifier(ResourceConstants.CONFIG_CORNERRADIUS,
+                                        "dimen", ResourceConstants.ANDROID_PACKAGE)));
+    }
+
+    void addSystemDefaultColor(Builder builder) {
+        Resources system = Resources.getSystem();
+        int colorAccentLight = system.getColor(
+                system.getIdentifier(ResourceConstants.ACCENT_COLOR_LIGHT_NAME, "color",
+                        ResourceConstants.ANDROID_PACKAGE), null);
+        builder.setColorAccentLight(colorAccentLight);
+
+        int colorAccentDark = system.getColor(
+                system.getIdentifier(ResourceConstants.ACCENT_COLOR_DARK_NAME, "color",
+                        ResourceConstants.ANDROID_PACKAGE), null);
+        builder.setColorAccentDark(colorAccentDark);
+    }
+
+    void addSystemDefaultFont(Builder builder) {
+        Resources system = Resources.getSystem();
+        String headlineFontFamily = system.getString(system.getIdentifier(
+                ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY, "string",
+                ResourceConstants.ANDROID_PACKAGE));
+        String bodyFontFamily = system.getString(system.getIdentifier(
+                ResourceConstants.CONFIG_BODY_FONT_FAMILY,
+                "string", ResourceConstants.ANDROID_PACKAGE));
+        builder.setHeadlineFontFamily(Typeface.create(headlineFontFamily, Typeface.NORMAL))
+                .setBodyFontFamily(Typeface.create(bodyFontFamily, Typeface.NORMAL));
+    }
+
+    Typeface loadTypeface(String configName, String fontOverlayPackage)
+            throws NameNotFoundException, NotFoundException {
+
+        // TODO(santie): check for font being present in system
+
+        Resources overlayRes = mContext.getPackageManager()
+                .getResourcesForApplication(fontOverlayPackage);
+
+        String fontFamily = overlayRes.getString(overlayRes.getIdentifier(configName,
+                "string", fontOverlayPackage));
+        return Typeface.create(fontFamily, Typeface.NORMAL);
+    }
+
+    int loadColor(String colorName, String colorPackage)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes = mContext.getPackageManager()
+                .getResourcesForApplication(colorPackage);
+        return overlayRes.getColor(overlayRes.getIdentifier(colorName, "color", colorPackage),
+                null);
+    }
+
+    String loadString(String stringName, String packageName)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes =
+                mContext.getPackageManager().getResourcesForApplication(
+                        packageName);
+        return overlayRes.getString(overlayRes.getIdentifier(stringName, "string", packageName));
+    }
+
+    @Dimension
+    int loadDimen(String dimenName, String packageName)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes =
+                mContext.getPackageManager().getResourcesForApplication(
+                        packageName);
+        return overlayRes.getDimensionPixelOffset(overlayRes.getIdentifier(
+                dimenName, "dimen", packageName));
+    }
+
+    boolean loadBoolean(String booleanName, String packageName)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes =
+                mContext.getPackageManager().getResourcesForApplication(
+                        packageName);
+        return overlayRes.getBoolean(overlayRes.getIdentifier(
+                booleanName, "boolean", packageName));
+    }
+
+    Drawable loadIconPreviewDrawable(String drawableName, String packageName,
+            boolean fromSystem) throws NameNotFoundException, NotFoundException {
+
+        Resources packageRes =
+                mContext.getPackageManager().getResourcesForApplication(
+                        packageName);
+        Resources res = fromSystem ? Resources.getSystem() : packageRes;
+        return res.getDrawable(
+                packageRes.getIdentifier(drawableName, "drawable", packageName), null);
+    }
+
+    @Nullable
+    String getOverlayCategory(String packageName) {
+        OverlayInfo info = mOverlayInfos.get(packageName);
+        return info != null ? info.getCategory() : null;
+    }
+
+    String[] getShapePreviewIconPackages() {
+        return mShapePreviewIconPackages;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/customization/model/theme/ThemeBundle.java b/src/com/android/customization/model/theme/ThemeBundle.java
new file mode 100644
index 0000000..3a32f25
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundle.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.PATH_SIZE;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Path;
+import android.graphics.Typeface;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.PathParser;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.customization.widget.DynamicAdaptiveIconDrawable;
+import com.android.wallpaper.R;
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.asset.BitmapCachingAsset;
+import com.android.wallpaper.model.WallpaperInfo;
+import com.android.wallpaper.util.ResourceUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Represents a Theme component available in the system as a "persona" bundle.
+ * Note that in this context a Theme is not related to Android's Styles, but it's rather an
+ * abstraction representing a series of overlays to be applied to the system.
+ */
+public class ThemeBundle implements CustomizationOption<ThemeBundle> {
+
+    private static final String TAG = "ThemeBundle";
+    private final static String EMPTY_JSON = "{}";
+    private final static String TIMESTAMP_FIELD = "_applied_timestamp";
+
+    private final String mTitle;
+    private final PreviewInfo mPreviewInfo;
+    private final boolean mIsDefault;
+    protected final Map<String, String> mPackagesByCategory;
+    private WallpaperInfo mOverrideWallpaper;
+    private Asset mOverrideWallpaperAsset;
+    private CharSequence mContentDescription;
+
+    protected ThemeBundle(String title, Map<String, String> overlayPackages,
+            boolean isDefault, PreviewInfo previewInfo) {
+        mTitle = title;
+        mIsDefault = isDefault;
+        mPreviewInfo = previewInfo;
+        mPackagesByCategory = Collections.unmodifiableMap(removeNullValues(overlayPackages));
+    }
+
+    @Override
+    public String getTitle() {
+        return mTitle;
+    }
+
+    @Override
+    public void bindThumbnailTile(View view) {
+        Resources res = view.getContext().getResources();
+
+        ((TextView) view.findViewById(R.id.theme_option_font)).setTypeface(
+                mPreviewInfo.headlineFontFamily);
+        if (mPreviewInfo.shapeDrawable != null) {
+            ((ShapeDrawable) mPreviewInfo.shapeDrawable).getPaint().setColor(
+                    mPreviewInfo.resolveAccentColor(res));
+            ((ImageView) view.findViewById(R.id.theme_option_shape)).setImageDrawable(
+                    mPreviewInfo.shapeDrawable);
+        }
+        if (!mPreviewInfo.icons.isEmpty()) {
+            Drawable icon = mPreviewInfo.icons.get(0).getConstantState().newDrawable().mutate();
+            icon.setTint(ResourceUtils.getColorAttr(
+                    view.getContext(), android.R.attr.textColorSecondary));
+            ((ImageView) view.findViewById(R.id.theme_option_icon)).setImageDrawable(
+                    icon);
+        }
+        view.setContentDescription(getContentDescription(view.getContext()));
+    }
+
+    @Override
+    public boolean isActive(CustomizationManager<ThemeBundle> manager) {
+        ThemeManager themeManager = (ThemeManager) manager;
+
+        if (mIsDefault) {
+            String serializedOverlays = themeManager.getStoredOverlays();
+            return TextUtils.isEmpty(serializedOverlays) || EMPTY_JSON.equals(serializedOverlays);
+        } else {
+            Map<String, String> currentOverlays = themeManager.getCurrentOverlays();
+            return mPackagesByCategory.equals(currentOverlays);
+        }
+    }
+
+    @Override
+    public int getLayoutResId() {
+        return R.layout.theme_option;
+    }
+
+    /**
+     * This is similar to #equals() but it only compares this theme's packages with the other, that
+     * is, it will return true if applying this theme has the same effect of applying the given one.
+     */
+    public boolean isEquivalent(ThemeBundle other) {
+        if (other == null) {
+            return false;
+        }
+        if (mIsDefault) {
+            return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages())
+                    || EMPTY_JSON.equals(other.getSerializedPackages());
+        }
+        // Map#equals ensures keys and values are compared.
+        return mPackagesByCategory.equals(other.mPackagesByCategory);
+    }
+
+    public PreviewInfo getPreviewInfo() {
+        return mPreviewInfo;
+    }
+
+    public void setOverrideThemeWallpaper(WallpaperInfo homeWallpaper) {
+        mOverrideWallpaper = homeWallpaper;
+        mOverrideWallpaperAsset = null;
+    }
+
+    private Asset getOverrideWallpaperAsset(Context context) {
+        if (mOverrideWallpaperAsset == null) {
+            mOverrideWallpaperAsset = new BitmapCachingAsset(context,
+                    mOverrideWallpaper.getThumbAsset(context));
+        }
+        return mOverrideWallpaperAsset;
+    }
+
+    boolean isDefault() {
+        return mIsDefault;
+    }
+
+    public Map<String, String> getPackagesByCategory() {
+        return mPackagesByCategory;
+    }
+
+    public String getSerializedPackages() {
+        return getJsonPackages(false).toString();
+    }
+
+    public String getSerializedPackagesWithTimestamp() {
+        return getJsonPackages(true).toString();
+    }
+
+    JSONObject getJsonPackages(boolean insertTimestamp) {
+        if (isDefault()) {
+            return new JSONObject();
+        }
+        JSONObject json = new JSONObject(mPackagesByCategory);
+        // Remove items with null values to avoid deserialization issues.
+        removeNullValues(json);
+        if (insertTimestamp) {
+            try {
+                json.put(TIMESTAMP_FIELD, System.currentTimeMillis());
+            } catch (JSONException e) {
+                Log.e(TAG, "Couldn't add timestamp to serialized themebundle");
+            }
+        }
+        return json;
+    }
+
+    private void removeNullValues(JSONObject json) {
+        Iterator<String> keys = json.keys();
+        Set<String> keysToRemove = new HashSet<>();
+        while(keys.hasNext()) {
+            String key = keys.next();
+            if (json.isNull(key)) {
+                keysToRemove.add(key);
+            }
+        }
+        for (String key : keysToRemove) {
+            json.remove(key);
+        }
+    }
+
+    private Map<String, String> removeNullValues(Map<String, String> map) {
+        return map.entrySet()
+                .stream()
+                .filter(entry -> entry.getValue() != null)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    protected CharSequence getContentDescription(Context context) {
+        if (mContentDescription == null) {
+            CharSequence defaultName = context.getString(R.string.default_theme_title);
+            if (isDefault()) {
+                mContentDescription = defaultName;
+            } else {
+                PackageManager pm = context.getPackageManager();
+                CharSequence fontName = getOverlayName(pm, OVERLAY_CATEGORY_FONT);
+                CharSequence iconName = getOverlayName(pm, OVERLAY_CATEGORY_ICON_ANDROID);
+                CharSequence shapeName = getOverlayName(pm, OVERLAY_CATEGORY_SHAPE);
+                CharSequence colorName = getOverlayName(pm, OVERLAY_CATEGORY_COLOR);
+                mContentDescription = context.getString(R.string.theme_description,
+                        TextUtils.isEmpty(fontName) ? defaultName : fontName,
+                        TextUtils.isEmpty(iconName) ? defaultName : iconName,
+                        TextUtils.isEmpty(shapeName) ? defaultName : shapeName,
+                        TextUtils.isEmpty(colorName) ? defaultName : colorName);
+            }
+        }
+        return mContentDescription;
+    }
+
+    private CharSequence getOverlayName(PackageManager pm, String overlayCategoryFont) {
+        try {
+            return pm.getApplicationInfo(
+                    mPackagesByCategory.get(overlayCategoryFont), 0).loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException e) {
+            return "";
+        }
+    }
+
+    public static class PreviewInfo {
+        public final Typeface bodyFontFamily;
+        public final Typeface headlineFontFamily;
+        @ColorInt public final int colorAccentLight;
+        @ColorInt public final int colorAccentDark;
+        public final List<Drawable> icons;
+        public final Drawable shapeDrawable;
+        public final List<ShapeAppIcon> shapeAppIcons;
+        @Dimension public final int bottomSheeetCornerRadius;
+
+        /** A class to represent an App icon and its name. */
+        public static class ShapeAppIcon {
+            private Drawable mIconDrawable;
+            private CharSequence mAppName;
+
+            public ShapeAppIcon(Drawable icon, CharSequence appName) {
+                mIconDrawable = icon;
+                mAppName = appName;
+            }
+
+            /** Returns a copy of app icon drawable. */
+            public Drawable getDrawableCopy() {
+                return mIconDrawable.getConstantState().newDrawable().mutate();
+            }
+
+            /** Returns the app name. */
+            public CharSequence getAppName() {
+                return mAppName;
+            }
+        }
+
+        private PreviewInfo(Context context, Typeface bodyFontFamily, Typeface headlineFontFamily,
+                int colorAccentLight, int colorAccentDark, List<Drawable> icons,
+                Drawable shapeDrawable, @Dimension int cornerRadius,
+                List<ShapeAppIcon> shapeAppIcons) {
+            this.bodyFontFamily = bodyFontFamily;
+            this.headlineFontFamily = headlineFontFamily;
+            this.colorAccentLight = colorAccentLight;
+            this.colorAccentDark = colorAccentDark;
+            this.icons = icons;
+            this.shapeDrawable = shapeDrawable;
+            this.bottomSheeetCornerRadius = cornerRadius;
+            this.shapeAppIcons = shapeAppIcons;
+        }
+
+        /**
+         * Returns the accent color to be applied corresponding with the current configuration's
+         * UI mode.
+         * @return one of {@link #colorAccentDark} or {@link #colorAccentLight}
+         */
+        @ColorInt
+        public int resolveAccentColor(Resources res) {
+            return (res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
+                    == Configuration.UI_MODE_NIGHT_YES ? colorAccentDark : colorAccentLight;
+        }
+    }
+
+    public static class Builder {
+        protected String mTitle;
+        private Typeface mBodyFontFamily;
+        private Typeface mHeadlineFontFamily;
+        @ColorInt private int mColorAccentLight = -1;
+        @ColorInt private int mColorAccentDark = -1;
+        private List<Drawable> mIcons = new ArrayList<>();
+        private String mPathString;
+        private Path mShapePath;
+        private boolean mIsDefault;
+        @Dimension private int mCornerRadius;
+        protected Map<String, String> mPackages = new HashMap<>();
+        private List<ShapeAppIcon> mAppIcons = new ArrayList<>();
+
+        public ThemeBundle build(Context context) {
+            return new ThemeBundle(mTitle, mPackages, mIsDefault, createPreviewInfo(context));
+        }
+
+        public PreviewInfo createPreviewInfo(Context context) {
+            ShapeDrawable shapeDrawable = null;
+            List<ShapeAppIcon> shapeIcons = new ArrayList<>();
+            Path path = mShapePath;
+            if (!TextUtils.isEmpty(mPathString)) {
+                path = PathParser.createPathFromPathData(mPathString);
+            }
+            if (path != null) {
+                PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE);
+                shapeDrawable = new ShapeDrawable(shape);
+                shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
+                shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
+                for (ShapeAppIcon icon : mAppIcons) {
+                    Drawable drawable = icon.mIconDrawable;
+                    if (drawable instanceof AdaptiveIconDrawable) {
+                        AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) drawable;
+                        shapeIcons.add(new ShapeAppIcon(
+                                new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(),
+                                        adaptiveIcon.getForeground(), path),
+                                icon.getAppName()));
+                    } else if (drawable instanceof DynamicAdaptiveIconDrawable) {
+                        shapeIcons.add(icon);
+                    }
+                    // TODO: add iconloader library's legacy treatment helper methods for
+                    //  non-adaptive icons
+                }
+            }
+            return new PreviewInfo(context, mBodyFontFamily, mHeadlineFontFamily, mColorAccentLight,
+                    mColorAccentDark, mIcons, shapeDrawable, mCornerRadius, shapeIcons);
+        }
+
+        public Map<String, String> getPackages() {
+            return Collections.unmodifiableMap(mPackages);
+        }
+
+        public String getTitle() {
+            return mTitle;
+        }
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setBodyFontFamily(@Nullable Typeface bodyFontFamily) {
+            mBodyFontFamily = bodyFontFamily;
+            return this;
+        }
+
+        public Builder setHeadlineFontFamily(@Nullable Typeface headlineFontFamily) {
+            mHeadlineFontFamily = headlineFontFamily;
+            return this;
+        }
+
+        public Builder setColorAccentLight(@ColorInt int colorAccentLight) {
+            mColorAccentLight = colorAccentLight;
+            return this;
+        }
+
+        public Builder setColorAccentDark(@ColorInt int colorAccentDark) {
+            mColorAccentDark = colorAccentDark;
+            return this;
+        }
+
+        public Builder addIcon(Drawable icon) {
+            mIcons.add(icon);
+            return this;
+        }
+
+        public Builder addOverlayPackage(String category, String packageName) {
+            mPackages.put(category, packageName);
+            return this;
+        }
+
+        public Builder setShapePath(String path) {
+            mPathString = path;
+            return this;
+        }
+
+        public Builder setShapePath(Path path) {
+            mShapePath = path;
+            return this;
+        }
+
+        public Builder asDefault() {
+            mIsDefault = true;
+            return this;
+        }
+
+        public Builder setShapePreviewIcons(List<ShapeAppIcon> appIcons) {
+            mAppIcons.clear();
+            mAppIcons.addAll(appIcons);
+            return this;
+        }
+
+        public Builder setBottomSheetCornerRadius(@Dimension int radius) {
+            mCornerRadius = radius;
+            return this;
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/ThemeBundleProvider.java b/src/com/android/customization/model/theme/ThemeBundleProvider.java
new file mode 100644
index 0000000..34342b3
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundleProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.theme.custom.CustomTheme;
+
+import org.json.JSONException;
+
+/**
+ * Interface for a class that can retrieve Themes from the system.
+ */
+public interface ThemeBundleProvider {
+
+    /**
+     * Returns whether themes are available in the current setup.
+     */
+    boolean isAvailable();
+
+    /**
+     * Retrieve the available themes.
+     * @param callback called when the themes have been retrieved (or immediately if cached)
+     * @param reload whether to reload themes if they're cached.
+     */
+    void fetch(OptionsFetchedListener<ThemeBundle> callback, boolean reload);
+
+    void storeCustomTheme(CustomTheme theme);
+
+    void removeCustomTheme(CustomTheme theme);
+
+    @Nullable ThemeBundle.Builder parseThemeBundle(String serializedTheme) throws JSONException;
+
+    @Nullable CustomTheme.Builder parseCustomTheme(String serializedTheme) throws JSONException;
+
+    ThemeBundle findEquivalent(ThemeBundle other);
+}
diff --git a/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java b/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java
new file mode 100644
index 0000000..4f0cd6c
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeBundledWallpaperInfo.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.StringRes;
+
+import com.android.wallpaper.asset.Asset;
+import com.android.wallpaper.asset.ResourceAsset;
+import com.android.wallpaper.model.InlinePreviewIntentFactory;
+import com.android.wallpaper.model.WallpaperInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a wallpaper coming from the resources of the theme bundle container APK.
+ */
+public class ThemeBundledWallpaperInfo extends WallpaperInfo {
+    public static final Creator<ThemeBundledWallpaperInfo> CREATOR =
+            new Creator<ThemeBundledWallpaperInfo>() {
+                @Override
+                public ThemeBundledWallpaperInfo createFromParcel(Parcel in) {
+                    return new ThemeBundledWallpaperInfo(in);
+                }
+
+                @Override
+                public ThemeBundledWallpaperInfo[] newArray(int size) {
+                    return new ThemeBundledWallpaperInfo[size];
+                }
+            };
+
+    private static final String TAG = "ThemeBundledWallpaperInfo";
+
+    private final String mPackageName;
+    private final String mResName;
+    private final String mCollectionId;
+    @DrawableRes private final int mDrawableResId;
+    @StringRes private final int mTitleResId;
+    @StringRes private final int mAttributionResId;
+    @StringRes private final int mActionUrlResId;
+    private List<String> mAttributions;
+    private String mActionUrl;
+    private Resources mResources;
+    private Asset mAsset;
+
+    /**
+     * Constructs a new theme-bundled static wallpaper model object.
+     *
+     * @param drawableResId  Resource ID of the raw wallpaper image.
+     * @param resName        The unique name of the wallpaper resource, e.g. "z_wp001".
+     * @param themeName   Unique name of the collection this wallpaper belongs in; used for logging.
+     * @param titleResId     Resource ID of the string for the title attribution.
+     * @param attributionResId Resource ID of the string for the first subtitle attribution.
+     */
+    public ThemeBundledWallpaperInfo(String packageName, String resName, String themeName,
+            int drawableResId, int titleResId, int attributionResId, int actionUrlResId) {
+        mPackageName = packageName;
+        mResName = resName;
+        mCollectionId = themeName;
+        mDrawableResId = drawableResId;
+        mTitleResId = titleResId;
+        mAttributionResId = attributionResId;
+        mActionUrlResId = actionUrlResId;
+    }
+
+    private ThemeBundledWallpaperInfo(Parcel in) {
+        super(in);
+        mPackageName = in.readString();
+        mResName = in.readString();
+        mCollectionId = in.readString();
+        mDrawableResId = in.readInt();
+        mTitleResId = in.readInt();
+        mAttributionResId = in.readInt();
+        mActionUrlResId = in.readInt();
+    }
+
+    @Override
+    public List<String> getAttributions(Context context) {
+        if (mAttributions == null) {
+            Resources res = getPackageResources(context);
+            mAttributions = new ArrayList<>();
+            if (mTitleResId != 0) {
+                mAttributions.add(res.getString(mTitleResId));
+            }
+            if (mAttributionResId != 0) {
+                mAttributions.add(res.getString(mAttributionResId));
+            }
+        }
+
+        return mAttributions;
+    }
+
+    @Override
+    public String getActionUrl(Context context) {
+        if (mActionUrl == null && mActionUrlResId != 0) {
+            mActionUrl = getPackageResources(context).getString(mActionUrlResId);
+        }
+        return mActionUrl;
+    }
+
+    @Override
+    public Asset getAsset(Context context) {
+        if (mAsset == null) {
+            Resources res = getPackageResources(context);
+            mAsset = new ResourceAsset(res, mDrawableResId);
+        }
+
+        return mAsset;
+    }
+
+    @Override
+    public Asset getThumbAsset(Context context) {
+        return getAsset(context);
+    }
+
+    @Override
+    public void showPreview(Activity srcActivity, InlinePreviewIntentFactory factory,
+            int requestCode, boolean isAssetIdPresent) {
+        try {
+            srcActivity.startActivityForResult(factory.newIntent(srcActivity, this,
+                    isAssetIdPresent), requestCode);
+        } catch (ActivityNotFoundException |SecurityException e) {
+            Log.e(TAG, "App isn't installed or ThemePicker doesn't have permission to launch", e);
+        }
+    }
+
+    @Override
+    public String getCollectionId(Context unused) {
+        return mCollectionId;
+    }
+
+    @Override
+    public String getWallpaperId() {
+        return mResName;
+    }
+
+    public String getResName() {
+        return mResName;
+    }
+
+    /**
+     * Returns the {@link Resources} instance for the theme bundles stub APK.
+     */
+    private Resources getPackageResources(Context context) {
+        if (mResources != null) {
+            return mResources;
+        }
+
+        try {
+            mResources = context.getPackageManager().getResourcesForApplication(mPackageName);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Could not get app resources for " + mPackageName);
+        }
+        return mResources;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(mPackageName);
+        dest.writeString(mResName);
+        dest.writeString(mCollectionId);
+        dest.writeInt(mDrawableResId);
+        dest.writeInt(mTitleResId);
+        dest.writeInt(mAttributionResId);
+        dest.writeInt(mActionUrlResId);
+    }
+}
diff --git a/src/com/android/customization/model/theme/ThemeManager.java b/src/com/android/customization/model/theme/ThemeManager.java
new file mode 100644
index 0000000..85241c1
--- /dev/null
+++ b/src/com/android/customization/model/theme/ThemeManager.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.module.ThemesUserEventLogger;
+
+import org.json.JSONObject;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class ThemeManager implements CustomizationManager<ThemeBundle> {
+
+    public static final Set<String> THEME_CATEGORIES = new HashSet<>();
+    static {
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_COLOR);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_FONT);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_SHAPE);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_ICON_ANDROID);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_ICON_SETTINGS);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_ICON_SYSUI);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_ICON_LAUNCHER);
+        THEME_CATEGORIES.add(OVERLAY_CATEGORY_ICON_THEMEPICKER);
+    }
+
+    private final ThemeBundleProvider mProvider;
+    private final OverlayManagerCompat mOverlayManagerCompat;
+
+    protected final FragmentActivity mActivity;
+    private final ThemesUserEventLogger mEventLogger;
+
+    private Map<String, String> mCurrentOverlays;
+
+    public ThemeManager(ThemeBundleProvider provider, FragmentActivity activity,
+            OverlayManagerCompat overlayManagerCompat,
+            ThemesUserEventLogger logger) {
+        mProvider = provider;
+        mActivity = activity;
+        mOverlayManagerCompat = overlayManagerCompat;
+        mEventLogger = logger;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return mOverlayManagerCompat.isAvailable() && mProvider.isAvailable();
+    }
+
+    @Override
+    public void apply(ThemeBundle theme, Callback callback) {
+        applyOverlays(theme, callback);
+    }
+
+    private void applyOverlays(ThemeBundle theme, Callback callback) {
+        boolean allApplied = Settings.Secure.putString(mActivity.getContentResolver(),
+                ResourceConstants.THEME_SETTING, theme.getSerializedPackagesWithTimestamp());
+        if (theme instanceof CustomTheme) {
+            storeCustomTheme((CustomTheme) theme);
+        }
+        mCurrentOverlays = null;
+        if (allApplied) {
+            mEventLogger.logThemeApplied(theme, theme instanceof CustomTheme);
+            callback.onSuccess();
+        } else {
+            callback.onError(null);
+        }
+    }
+
+    private void storeCustomTheme(CustomTheme theme) {
+        mProvider.storeCustomTheme(theme);
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<ThemeBundle> callback, boolean reload) {
+        mProvider.fetch(callback, reload);
+    }
+
+    public Map<String, String> getCurrentOverlays() {
+        if (mCurrentOverlays == null) {
+            mCurrentOverlays = mOverlayManagerCompat.getEnabledOverlaysForTargets(
+                    ResourceConstants.getPackagesToOverlay(mActivity));
+            mCurrentOverlays.entrySet().removeIf(
+                    categoryAndPackage -> !THEME_CATEGORIES.contains(categoryAndPackage.getKey()));
+        }
+        return mCurrentOverlays;
+    }
+
+    public String getStoredOverlays() {
+        return Settings.Secure.getString(mActivity.getContentResolver(),
+                ResourceConstants.THEME_SETTING);
+    }
+
+    public void removeCustomTheme(CustomTheme theme) {
+        mProvider.removeCustomTheme(theme);
+    }
+
+    /**
+     * @return an existing ThemeBundle that matches the same packages as the given one, if one
+     * exists, or {@code null} otherwise.
+     */
+    @Nullable
+    public ThemeBundle findThemeByPackages(ThemeBundle other) {
+        return mProvider.findEquivalent(other);
+    }
+
+    /**
+     * Store empty theme if no theme has been set yet. This will prevent Settings from showing the
+     * suggestion to select a theme
+     */
+    public void storeEmptyTheme() {
+        String themeSetting = Settings.Secure.getString(mActivity.getContentResolver(),
+                ResourceConstants.THEME_SETTING);
+        if (TextUtils.isEmpty(themeSetting)) {
+            Settings.Secure.putString(mActivity.getContentResolver(),
+                    ResourceConstants.THEME_SETTING, new JSONObject().toString());
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/ColorOptionsProvider.java b/src/com/android/customization/model/theme/custom/ColorOptionsProvider.java
new file mode 100644
index 0000000..f3b950b
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ColorOptionsProvider.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.ACCENT_COLOR_DARK_NAME;
+import static com.android.customization.model.ResourceConstants.ACCENT_COLOR_LIGHT_NAME;
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ANDROID_THEME;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.PATH_SIZE;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.graphics.PathParser;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.custom.ThemeComponentOption.ColorOption;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link ThemeComponentOptionProvider} that reads {@link ColorOption}s from
+ * icon overlays.
+ */
+public class ColorOptionsProvider extends ThemeComponentOptionProvider<ColorOption> {
+
+    private static final String TAG = "ColorOptionsProvider";
+    private final CustomThemeManager mCustomThemeManager;
+    private final String mDefaultThemePackage;
+
+    public ColorOptionsProvider(Context context, OverlayManagerCompat manager,
+            CustomThemeManager customThemeManager) {
+        super(context, manager, OVERLAY_CATEGORY_COLOR);
+        mCustomThemeManager = customThemeManager;
+        // System color is set with a static overlay for android.theme category, so let's try to
+        // find that first, and if that's not present, we'll default to System resources.
+        // (see #addDefault())
+        List<String> themePackages = manager.getOverlayPackagesForCategory(
+                OVERLAY_CATEGORY_ANDROID_THEME, UserHandle.myUserId(), ANDROID_PACKAGE);
+        mDefaultThemePackage = themePackages.isEmpty() ? null : themePackages.get(0);
+    }
+
+    @Override
+    protected void loadOptions() {
+        List<Drawable> previewIcons = new ArrayList<>();
+        String iconPackage =
+                mCustomThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_ICON_ANDROID);
+        if (TextUtils.isEmpty(iconPackage)) {
+            iconPackage = ANDROID_PACKAGE;
+        }
+        for (String iconName : ICONS_FOR_PREVIEW) {
+            try {
+                previewIcons.add(loadIconPreviewDrawable(iconName, iconPackage));
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load icon in %s for color preview, will skip it",
+                        iconPackage), e);
+            }
+        }
+        String shapePackage = mCustomThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE);
+        if (TextUtils.isEmpty(shapePackage)) {
+            shapePackage = ANDROID_PACKAGE;
+        }
+        Drawable shape = loadShape(shapePackage);
+        addDefault(previewIcons, shape);
+        for (String overlayPackage : mOverlayPackages) {
+            try {
+                Resources overlayRes = getOverlayResources(overlayPackage);
+                int lightColor = overlayRes.getColor(
+                        overlayRes.getIdentifier(ACCENT_COLOR_LIGHT_NAME, "color", overlayPackage),
+                        null);
+                int darkColor = overlayRes.getColor(
+                        overlayRes.getIdentifier(ACCENT_COLOR_DARK_NAME, "color", overlayPackage),
+                        null);
+                PackageManager pm = mContext.getPackageManager();
+                String label = pm.getApplicationInfo(overlayPackage, 0).loadLabel(pm).toString();
+                ColorOption option = new ColorOption(overlayPackage, label, lightColor, darkColor);
+                option.setPreviewIcons(previewIcons);
+                option.setShapeDrawable(shape);
+                mOptions.add(option);
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load color overlay %s, will skip it",
+                        overlayPackage), e);
+            }
+        }
+    }
+
+    private void addDefault(List<Drawable> previewIcons, Drawable shape) {
+        int lightColor, darkColor;
+        Resources system = Resources.getSystem();
+        try {
+            Resources r = getOverlayResources(mDefaultThemePackage);
+            lightColor = r.getColor(
+                    r.getIdentifier(ACCENT_COLOR_LIGHT_NAME, "color", mDefaultThemePackage),
+                    null);
+            darkColor = r.getColor(
+                    r.getIdentifier(ACCENT_COLOR_DARK_NAME, "color", mDefaultThemePackage),
+                    null);
+        } catch (NotFoundException | NameNotFoundException e) {
+            Log.d(TAG, "Didn't find default color, will use system option", e);
+
+            lightColor = system.getColor(
+                    system.getIdentifier(ACCENT_COLOR_LIGHT_NAME, "color", ANDROID_PACKAGE), null);
+
+            darkColor = system.getColor(
+                    system.getIdentifier(ACCENT_COLOR_DARK_NAME, "color", ANDROID_PACKAGE), null);
+        }
+        ColorOption option = new ColorOption(null,
+                mContext.getString(R.string.default_theme_title), lightColor, darkColor);
+        option.setPreviewIcons(previewIcons);
+        option.setShapeDrawable(shape);
+        mOptions.add(option);
+    }
+
+    private Drawable loadIconPreviewDrawable(String drawableName, String packageName)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes = getOverlayResources(packageName);
+        return overlayRes.getDrawable(
+                overlayRes.getIdentifier(drawableName, "drawable", packageName), null);
+    }
+
+    private Drawable loadShape(String packageName) {
+        String path = null;
+        try {
+            Resources r = getOverlayResources(packageName);
+
+            path = ResourceConstants.getIconMask(r, packageName);
+        } catch (NameNotFoundException e) {
+            Log.d(TAG, String.format("Couldn't load shape icon for %s, skipping.", packageName), e);
+        }
+        ShapeDrawable shapeDrawable = null;
+        if (!TextUtils.isEmpty(path)) {
+            PathShape shape = new PathShape(PathParser.createPathFromPathData(path),
+                    PATH_SIZE, PATH_SIZE);
+            shapeDrawable = new ShapeDrawable(shape);
+            shapeDrawable.setIntrinsicHeight((int) PATH_SIZE);
+            shapeDrawable.setIntrinsicWidth((int) PATH_SIZE);
+        }
+        return shapeDrawable;
+    }
+
+}
diff --git a/src/com/android/customization/model/theme/custom/CustomTheme.java b/src/com/android/customization/model/theme/custom/CustomTheme.java
new file mode 100644
index 0000000..a1ee106
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/CustomTheme.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.wallpaper.R;
+
+import java.util.Map;
+import java.util.UUID;
+
+public class CustomTheme extends ThemeBundle {
+
+    public static String newId() {
+        return UUID.randomUUID().toString();
+    }
+
+    /**
+     * Used to uniquely identify a custom theme since names can change.
+     */
+    private final String mId;
+
+    private CustomTheme(@NonNull String id, String title, Map<String, String> overlayPackages,
+            @Nullable PreviewInfo previewInfo) {
+        super(title, overlayPackages, false, previewInfo);
+        mId = id;
+    }
+
+    public String getId() {
+        return mId;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof CustomTheme)) {
+            return false;
+        }
+        CustomTheme other = (CustomTheme) obj;
+        return mId.equals(other.mId);
+    }
+
+    @Override
+    public int hashCode() {
+        return mId.hashCode();
+    }
+
+    @Override
+    public void bindThumbnailTile(View view) {
+        if (isDefined()) {
+            super.bindThumbnailTile(view);
+        }
+    }
+
+    @Override
+    public int getLayoutResId() {
+        return isDefined() ? R.layout.theme_option : R.layout.custom_theme_option;
+    }
+
+    @Override
+    public boolean isActive(CustomizationManager<ThemeBundle> manager) {
+        return isDefined() && super.isActive(manager);
+    }
+
+    @Override
+    public boolean isEquivalent(ThemeBundle other) {
+        return isDefined() && super.isEquivalent(other);
+    }
+
+    public boolean isDefined() {
+        return getPreviewInfo() != null;
+    }
+
+    public static class Builder extends ThemeBundle.Builder {
+        private String mId;
+
+        @Override
+        public CustomTheme build(Context context) {
+            return new CustomTheme(mId, mTitle, mPackages,
+                    mPackages.isEmpty() ? null : createPreviewInfo(context));
+        }
+
+        public Builder setId(String id) {
+            mId = id;
+            return this;
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/CustomThemeManager.java b/src/com/android/customization/model/theme/custom/CustomThemeManager.java
new file mode 100644
index 0000000..42d73e6
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/CustomThemeManager.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo;
+import com.android.customization.model.theme.ThemeBundleProvider;
+import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.model.theme.custom.CustomTheme.Builder;
+
+import org.json.JSONException;
+
+import java.util.Map;
+
+public class CustomThemeManager implements CustomizationManager<ThemeComponentOption> {
+
+    private static final String TAG = "CustomThemeManager";
+    private static final String KEY_STATE_CURRENT_SELECTION = "CustomThemeManager.currentSelection";
+
+    private final CustomTheme mOriginalTheme;
+    private CustomTheme.Builder mBuilder;
+
+    private CustomThemeManager(Map<String, String> overlayPackages,
+            @Nullable CustomTheme originalTheme) {
+        mBuilder = new Builder();
+        overlayPackages.forEach(mBuilder::addOverlayPackage);
+        mOriginalTheme = originalTheme;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return true;
+    }
+
+    @Override
+    public void apply(ThemeComponentOption option, @Nullable Callback callback) {
+        option.buildStep(mBuilder);
+        if (callback != null) {
+            callback.onSuccess();
+        }
+    }
+
+    public Map<String, String> getOverlayPackages() {
+        return mBuilder.getPackages();
+    }
+
+    public CustomTheme buildPartialCustomTheme(Context context, String id, String title) {
+        return ((CustomTheme.Builder)mBuilder.setId(id).setTitle(title)).build(context);
+    }
+
+    @Override
+    public void fetchOptions(OptionsFetchedListener<ThemeComponentOption> callback, boolean reload) {
+        //Unused
+    }
+
+    public CustomTheme getOriginalTheme() {
+        return mOriginalTheme;
+    }
+
+    public PreviewInfo buildCustomThemePreviewInfo(Context context) {
+        return mBuilder.createPreviewInfo(context);
+    }
+
+    /** Saves the custom theme selections while system config changes. */
+    public void saveCustomTheme(Context context, Bundle savedInstanceState) {
+        CustomTheme customTheme =
+                buildPartialCustomTheme(context, /* id= */ null, /* title= */ null);
+        savedInstanceState.putString(KEY_STATE_CURRENT_SELECTION,
+                customTheme.getSerializedPackages());
+    }
+
+    /** Reads the saved custom theme after system config changed. */
+    public void readCustomTheme(ThemeBundleProvider themeBundleProvider,
+                                Bundle savedInstanceState) {
+        String packages = savedInstanceState.getString(KEY_STATE_CURRENT_SELECTION);
+        if (!TextUtils.isEmpty(packages)) {
+            try {
+                mBuilder = themeBundleProvider.parseCustomTheme(packages);
+            } catch (JSONException e) {
+                Log.w(TAG, "Couldn't parse provided custom theme.");
+            }
+        } else {
+            Log.w(TAG, "No custom theme being restored.");
+        }
+    }
+
+    public static CustomThemeManager create(
+            @Nullable CustomTheme customTheme, ThemeManager themeManager) {
+        if (customTheme != null && customTheme.isDefined()) {
+            return new CustomThemeManager(customTheme.getPackagesByCategory(), customTheme);
+        }
+        // Seed the first custom theme with the currently applied theme.
+        return new CustomThemeManager(themeManager.getCurrentOverlays(), customTheme);
+    }
+
+}
diff --git a/src/com/android/customization/model/theme/custom/FontOptionsProvider.java b/src/com/android/customization/model/theme/custom/FontOptionsProvider.java
new file mode 100644
index 0000000..53568c9
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/FontOptionsProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.CONFIG_BODY_FONT_FAMILY;
+import static com.android.customization.model.ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Typeface;
+import android.util.Log;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.custom.ThemeComponentOption.FontOption;
+import com.android.wallpaper.R;
+
+/**
+ * Implementation of {@link ThemeComponentOptionProvider} that reads {@link FontOption}s from
+ * font overlays.
+ */
+public class FontOptionsProvider extends ThemeComponentOptionProvider<FontOption> {
+
+    private static final String TAG = "FontOptionsProvider";
+
+    public FontOptionsProvider(Context context, OverlayManagerCompat manager) {
+        super(context, manager, OVERLAY_CATEGORY_FONT);
+    }
+
+    @Override
+    protected void loadOptions() {
+        addDefault();
+        for (String overlayPackage : mOverlayPackages) {
+            try {
+                Resources overlayRes = getOverlayResources(overlayPackage);
+                Typeface headlineFont = Typeface.create(
+                        getFontFamily(overlayPackage, overlayRes, CONFIG_HEADLINE_FONT_FAMILY),
+                        Typeface.NORMAL);
+                Typeface bodyFont = Typeface.create(
+                        getFontFamily(overlayPackage, overlayRes, CONFIG_BODY_FONT_FAMILY),
+                        Typeface.NORMAL);
+                PackageManager pm = mContext.getPackageManager();
+                String label = pm.getApplicationInfo(overlayPackage, 0).loadLabel(pm).toString();
+                mOptions.add(new FontOption(overlayPackage, label, headlineFont, bodyFont));
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load font overlay %s, will skip it",
+                        overlayPackage), e);
+            }
+        }
+    }
+
+    private void addDefault() {
+        Resources system = Resources.getSystem();
+        Typeface headlineFont = Typeface.create(system.getString(system.getIdentifier(
+                ResourceConstants.CONFIG_HEADLINE_FONT_FAMILY,"string", ANDROID_PACKAGE)),
+                Typeface.NORMAL);
+        Typeface bodyFont = Typeface.create(system.getString(system.getIdentifier(
+                ResourceConstants.CONFIG_BODY_FONT_FAMILY,
+                "string", ANDROID_PACKAGE)),
+                Typeface.NORMAL);
+        mOptions.add(new FontOption(null, mContext.getString(R.string.default_theme_title),
+                headlineFont, bodyFont));
+    }
+
+    private String getFontFamily(String overlayPackage, Resources overlayRes, String configName) {
+        return overlayRes.getString(overlayRes.getIdentifier(configName, "string", overlayPackage));
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/IconOptionsProvider.java b/src/com/android/customization/model/theme/custom/IconOptionsProvider.java
new file mode 100644
index 0000000..f7b669b
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/IconOptionsProvider.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.ICONS_FOR_PREVIEW;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.custom.ThemeComponentOption.IconOption;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of {@link ThemeComponentOptionProvider} that reads {@link IconOption}s from
+ * icon overlays.
+ */
+public class IconOptionsProvider extends ThemeComponentOptionProvider<IconOption> {
+
+    private static final String TAG = "IconOptionsProvider";
+
+    private final List<String> mSysUiIconsOverlayPackages = new ArrayList<>();
+    private final List<String> mSettingsIconsOverlayPackages = new ArrayList<>();
+    private final List<String> mLauncherIconsOverlayPackages = new ArrayList<>();
+    private final List<String> mThemePickerIconsOverlayPackages = new ArrayList<>();
+
+    public IconOptionsProvider(Context context, OverlayManagerCompat manager) {
+        super(context, manager, OVERLAY_CATEGORY_ICON_ANDROID);
+        String[] targetPackages = ResourceConstants.getPackagesToOverlay(context);
+        mSysUiIconsOverlayPackages.addAll(manager.getOverlayPackagesForCategory(
+                OVERLAY_CATEGORY_ICON_SYSUI, UserHandle.myUserId(), targetPackages));
+        mSettingsIconsOverlayPackages.addAll(manager.getOverlayPackagesForCategory(
+                OVERLAY_CATEGORY_ICON_SETTINGS, UserHandle.myUserId(), targetPackages));
+        mLauncherIconsOverlayPackages.addAll(manager.getOverlayPackagesForCategory(
+                OVERLAY_CATEGORY_ICON_LAUNCHER, UserHandle.myUserId(), targetPackages));
+        mThemePickerIconsOverlayPackages.addAll(manager.getOverlayPackagesForCategory(
+                OVERLAY_CATEGORY_ICON_THEMEPICKER, UserHandle.myUserId(), targetPackages));
+    }
+
+    @Override
+    protected void loadOptions() {
+        addDefault();
+
+        Map<String, IconOption> optionsByPrefix = new HashMap<>();
+        for (String overlayPackage : mOverlayPackages) {
+            IconOption option = addOrUpdateOption(optionsByPrefix, overlayPackage,
+                    OVERLAY_CATEGORY_ICON_ANDROID);
+            try{
+                for (String iconName : ICONS_FOR_PREVIEW) {
+                    option.addIcon(loadIconPreviewDrawable(iconName, overlayPackage));
+                }
+            } catch (NotFoundException | NameNotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load icon overlay details for %s, will skip it",
+                        overlayPackage), e);
+            }
+        }
+
+        for (String overlayPackage : mSysUiIconsOverlayPackages) {
+            addOrUpdateOption(optionsByPrefix, overlayPackage, OVERLAY_CATEGORY_ICON_SYSUI);
+        }
+
+        for (String overlayPackage : mSettingsIconsOverlayPackages) {
+            addOrUpdateOption(optionsByPrefix, overlayPackage, OVERLAY_CATEGORY_ICON_SETTINGS);
+        }
+
+        for (String overlayPackage : mLauncherIconsOverlayPackages) {
+            addOrUpdateOption(optionsByPrefix, overlayPackage, OVERLAY_CATEGORY_ICON_LAUNCHER);
+        }
+
+        for (String overlayPackage : mThemePickerIconsOverlayPackages) {
+            addOrUpdateOption(optionsByPrefix, overlayPackage, OVERLAY_CATEGORY_ICON_THEMEPICKER);
+        }
+
+        for (IconOption option : optionsByPrefix.values()) {
+            if (option.isValid(mContext)) {
+                mOptions.add(option);
+                option.setLabel(mContext.getString(R.string.icon_component_label, mOptions.size()));
+            }
+        }
+    }
+
+    private IconOption addOrUpdateOption(Map<String, IconOption> optionsByPrefix,
+            String overlayPackage, String category) {
+        String prefix = overlayPackage.substring(0, overlayPackage.lastIndexOf("."));
+        IconOption option;
+        if (!optionsByPrefix.containsKey(prefix)) {
+            option = new IconOption();
+            optionsByPrefix.put(prefix, option);
+        } else {
+            option = optionsByPrefix.get(prefix);
+        }
+        option.addOverlayPackage(category, overlayPackage);
+        return option;
+    }
+
+    private Drawable loadIconPreviewDrawable(String drawableName, String packageName)
+            throws NameNotFoundException, NotFoundException {
+        final Resources resources = ANDROID_PACKAGE.equals(packageName)
+                ? Resources.getSystem()
+                : mContext.getPackageManager().getResourcesForApplication(packageName);
+        return resources.getDrawable(
+                resources.getIdentifier(drawableName, "drawable", packageName), null);
+    }
+
+    private void addDefault() {
+        IconOption option = new IconOption();
+        option.setLabel(mContext.getString(R.string.default_theme_title));
+        try {
+            for (String iconName : ICONS_FOR_PREVIEW) {
+                option.addIcon(loadIconPreviewDrawable(iconName, ANDROID_PACKAGE));
+            }
+        } catch (NameNotFoundException | NotFoundException e) {
+            Log.w(TAG, "Didn't find SystemUi package icons, will skip option", e);
+        }
+        option.addOverlayPackage(OVERLAY_CATEGORY_ICON_ANDROID, null);
+        option.addOverlayPackage(OVERLAY_CATEGORY_ICON_SYSUI, null);
+        option.addOverlayPackage(OVERLAY_CATEGORY_ICON_SETTINGS, null);
+        option.addOverlayPackage(OVERLAY_CATEGORY_ICON_LAUNCHER, null);
+        option.addOverlayPackage(OVERLAY_CATEGORY_ICON_THEMEPICKER, null);
+        mOptions.add(option);
+    }
+
+}
diff --git a/src/com/android/customization/model/theme/custom/ShapeOptionsProvider.java b/src/com/android/customization/model/theme/custom/ShapeOptionsProvider.java
new file mode 100644
index 0000000..f93b892
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ShapeOptionsProvider.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.CONFIG_CORNERRADIUS;
+import static com.android.customization.model.ResourceConstants.CONFIG_ICON_MASK;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.PATH_SIZE;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Dimension;
+import androidx.core.graphics.PathParser;
+
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.customization.model.theme.custom.ThemeComponentOption.ShapeOption;
+import com.android.customization.widget.DynamicAdaptiveIconDrawable;
+import com.android.wallpaper.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implementation of {@link ThemeComponentOptionProvider} that reads {@link ShapeOption}s from
+ * icon overlays.
+ */
+public class ShapeOptionsProvider extends ThemeComponentOptionProvider<ShapeOption> {
+
+    private static final String TAG = "ShapeOptionsProvider";
+    private final String[] mShapePreviewIconPackages;
+    private int mThumbSize;
+
+    public ShapeOptionsProvider(Context context, OverlayManagerCompat manager) {
+        super(context, manager, OVERLAY_CATEGORY_SHAPE);
+        mShapePreviewIconPackages = context.getResources().getStringArray(
+                R.array.icon_shape_preview_packages);
+        mThumbSize = mContext.getResources().getDimensionPixelSize(
+                R.dimen.component_shape_thumb_size);
+    }
+
+    @Override
+    protected void loadOptions() {
+        addDefault();
+        for (String overlayPackage : mOverlayPackages) {
+            try {
+                Path path = loadPath(mContext.getPackageManager()
+                        .getResourcesForApplication(overlayPackage), overlayPackage);
+                PackageManager pm = mContext.getPackageManager();
+                String label = pm.getApplicationInfo(overlayPackage, 0).loadLabel(pm).toString();
+                mOptions.add(new ShapeOption(overlayPackage, label, path,
+                        loadCornerRadius(overlayPackage), createShapeDrawable(path),
+                        getShapedAppIcons(path)));
+            } catch (NameNotFoundException | NotFoundException e) {
+                Log.w(TAG, String.format("Couldn't load shape overlay %s, will skip it",
+                        overlayPackage), e);
+            }
+        }
+    }
+
+    private void addDefault() {
+        Resources system = Resources.getSystem();
+        Path path = loadPath(system, ANDROID_PACKAGE);
+        mOptions.add(new ShapeOption(null, mContext.getString(R.string.default_theme_title), path,
+                system.getDimensionPixelOffset(
+                        system.getIdentifier(ResourceConstants.CONFIG_CORNERRADIUS,
+                                "dimen", ResourceConstants.ANDROID_PACKAGE)),
+                createShapeDrawable(path), getShapedAppIcons(path)));
+    }
+
+    private ShapeDrawable createShapeDrawable(Path path) {
+        PathShape shape = new PathShape(path, PATH_SIZE, PATH_SIZE);
+        ShapeDrawable shapeDrawable = new ShapeDrawable(shape);
+        shapeDrawable.setIntrinsicHeight(mThumbSize);
+        shapeDrawable.setIntrinsicWidth(mThumbSize);
+        return shapeDrawable;
+    }
+
+    private List<ShapeAppIcon> getShapedAppIcons(Path path) {
+        List<ShapeAppIcon> shapedAppIcons = new ArrayList<>();
+        for (String packageName : mShapePreviewIconPackages) {
+            Drawable icon = null;
+            CharSequence name = null;
+            try {
+                Drawable appIcon = mContext.getPackageManager().getApplicationIcon(packageName);
+                if (appIcon instanceof AdaptiveIconDrawable) {
+                    AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) appIcon;
+                    icon = new DynamicAdaptiveIconDrawable(adaptiveIcon.getBackground(),
+                            adaptiveIcon.getForeground(), path);
+
+                    ApplicationInfo appInfo = mContext.getPackageManager()
+                            .getApplicationInfo(packageName, /* flag= */ 0);
+                    name = mContext.getPackageManager().getApplicationLabel(appInfo);
+                }
+            } catch (NameNotFoundException e) {
+                Log.d(TAG, "Couldn't find app " + packageName
+                        + ", won't use it for icon shape preview");
+            } finally {
+                if (icon != null && !TextUtils.isEmpty(name)) {
+                    shapedAppIcons.add(new ShapeAppIcon(icon, name));
+                }
+            }
+        }
+        return shapedAppIcons;
+    }
+
+    private Path loadPath(Resources overlayRes, String packageName) {
+        String shape = overlayRes.getString(overlayRes.getIdentifier(CONFIG_ICON_MASK, "string",
+                packageName));
+
+        if (!TextUtils.isEmpty(shape)) {
+            return PathParser.createPathFromPathData(shape);
+        }
+        return null;
+    }
+
+    @Dimension
+    private int loadCornerRadius(String packageName)
+            throws NameNotFoundException, NotFoundException {
+
+        Resources overlayRes =
+                mContext.getPackageManager().getResourcesForApplication(
+                        packageName);
+        return overlayRes.getDimensionPixelOffset(overlayRes.getIdentifier(
+                CONFIG_CORNERRADIUS, "dimen", packageName));
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/ThemeComponentOption.java b/src/com/android/customization/model/theme/custom/ThemeComponentOption.java
new file mode 100644
index 0000000..78be0fc
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ThemeComponentOption.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_LAUNCHER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_THEMEPICKER;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.getLauncherPackage;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Path;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.Dimension;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.graphics.ColorUtils;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.customization.model.theme.custom.CustomTheme.Builder;
+import com.android.wallpaper.R;
+import com.android.wallpaper.util.ResourceUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Represents an option of a component of a custom Theme (for example, a possible color, or font,
+ * shape, etc).
+ * Extending classes correspond to each component's options and provide the structure to bind
+ * preview and thumbnails.
+ * // TODO (santie): refactor the logic to bind preview cards to reuse between ThemeFragment and
+ * // here
+ */
+public abstract class ThemeComponentOption implements CustomizationOption<ThemeComponentOption> {
+
+    protected final Map<String, String> mOverlayPackageNames = new HashMap<>();
+
+    protected void addOverlayPackage(String category, String packageName) {
+        mOverlayPackageNames.put(category, packageName);
+    }
+
+    public Map<String, String> getOverlayPackages() {
+        return mOverlayPackageNames;
+    }
+
+    @Override
+    public String getTitle() {
+        return null;
+    }
+
+    public abstract void bindPreview(ViewGroup container);
+
+    public Builder buildStep(Builder builder) {
+        getOverlayPackages().forEach(builder::addOverlayPackage);
+        return builder;
+    }
+
+    public static class FontOption extends ThemeComponentOption {
+
+        private final String mLabel;
+        private final Typeface mHeadlineFont;
+        private final Typeface mBodyFont;
+
+        public FontOption(String packageName, String label, Typeface headlineFont,
+                Typeface bodyFont) {
+            addOverlayPackage(OVERLAY_CATEGORY_FONT, packageName);
+            mLabel = label;
+            mHeadlineFont = headlineFont;
+            mBodyFont = bodyFont;
+        }
+
+        @Override
+        public String getTitle() {
+            return null;
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+            ((TextView) view.findViewById(R.id.thumbnail_text)).setTypeface(
+                    mHeadlineFont);
+            view.setContentDescription(mLabel);
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            CustomThemeManager customThemeManager = (CustomThemeManager) manager;
+            return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_FONT),
+                    customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_FONT));
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return R.layout.theme_font_option;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+            container.setContentDescription(
+                    container.getContext().getString(R.string.font_preview_content_description));
+
+            bindPreviewHeader(container, R.string.preview_name_font, R.drawable.ic_font, null);
+
+            ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
+            if (cardBody.getChildCount() == 0) {
+                LayoutInflater.from(container.getContext()).inflate(
+                        R.layout.preview_card_font_content,
+                        cardBody, true);
+            }
+            TextView title = container.findViewById(R.id.font_card_title);
+            title.setTypeface(mHeadlineFont);
+            TextView bodyText = container.findViewById(R.id.font_card_body);
+            bodyText.setTypeface(mBodyFont);
+            container.findViewById(R.id.font_card_divider).setBackgroundColor(
+                    title.getCurrentTextColor());
+        }
+
+        @Override
+        public Builder buildStep(Builder builder) {
+            builder.setHeadlineFontFamily(mHeadlineFont).setBodyFontFamily(mBodyFont);
+            return super.buildStep(builder);
+        }
+    }
+
+    void bindPreviewHeader(ViewGroup container, @StringRes int headerTextResId,
+            @DrawableRes int headerIcon, String drawableName) {
+        TextView header = container.findViewById(R.id.theme_preview_card_header);
+        header.setText(headerTextResId);
+
+        Context context = container.getContext();
+        Drawable icon;
+        if (!TextUtils.isEmpty(drawableName)) {
+            try {
+                Resources resources = context.getPackageManager()
+                        .getResourcesForApplication(getLauncherPackage(context));
+                icon = resources.getDrawable(resources.getIdentifier(
+                        drawableName, "drawable", getLauncherPackage(context)), null);
+            } catch (NameNotFoundException | NotFoundException e) {
+                icon = context.getResources().getDrawable(headerIcon, context.getTheme());
+            }
+        } else {
+            icon = context.getResources().getDrawable(headerIcon, context.getTheme());
+        }
+        int size = context.getResources().getDimensionPixelSize(R.dimen.card_header_icon_size);
+        icon.setBounds(0, 0, size, size);
+
+        header.setCompoundDrawables(null, icon, null, null);
+        header.setCompoundDrawableTintList(ColorStateList.valueOf(
+                header.getCurrentTextColor()));
+    }
+
+    public static class IconOption extends ThemeComponentOption {
+
+        public static final int THUMBNAIL_ICON_POSITION = 0;
+        private static int[] mIconIds = {
+                R.id.preview_icon_0, R.id.preview_icon_1, R.id.preview_icon_2, R.id.preview_icon_3,
+                R.id.preview_icon_4, R.id.preview_icon_5
+        };
+
+        private List<Drawable> mIcons = new ArrayList<>();
+        private String mLabel;
+
+        @Override
+        public void bindThumbnailTile(View view) {
+            Resources res = view.getContext().getResources();
+            Drawable icon = mIcons.get(THUMBNAIL_ICON_POSITION)
+                    .getConstantState().newDrawable().mutate();
+            icon.setTint(ResourceUtils.getColorAttr(
+                    view.getContext(), android.R.attr.textColorSecondary));
+            ((ImageView) view.findViewById(R.id.option_icon)).setImageDrawable(
+                    icon);
+            view.setContentDescription(mLabel);
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            CustomThemeManager customThemeManager = (CustomThemeManager) manager;
+            Map<String, String> themePackages = customThemeManager.getOverlayPackages();
+            if (getOverlayPackages().isEmpty()) {
+                return themePackages.get(OVERLAY_CATEGORY_ICON_SYSUI) == null &&
+                        themePackages.get(OVERLAY_CATEGORY_ICON_SETTINGS) == null &&
+                        themePackages.get(OVERLAY_CATEGORY_ICON_ANDROID) == null &&
+                        themePackages.get(OVERLAY_CATEGORY_ICON_LAUNCHER) == null &&
+                        themePackages.get(OVERLAY_CATEGORY_ICON_THEMEPICKER) == null;
+            }
+            for (Map.Entry<String, String> overlayEntry : getOverlayPackages().entrySet()) {
+                if(!Objects.equals(overlayEntry.getValue(),
+                        themePackages.get(overlayEntry.getKey()))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return R.layout.theme_icon_option;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+            container.setContentDescription(
+                    container.getContext().getString(R.string.icon_preview_content_description));
+
+            bindPreviewHeader(container, R.string.preview_name_icon, R.drawable.ic_widget,
+                    "ic_widget");
+
+            ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
+            if (cardBody.getChildCount() == 0) {
+                LayoutInflater.from(container.getContext()).inflate(
+                        R.layout.preview_card_icon_content, cardBody, true);
+            }
+            for (int i = 0; i < mIconIds.length && i < mIcons.size(); i++) {
+                ((ImageView) container.findViewById(mIconIds[i])).setImageDrawable(
+                        mIcons.get(i));
+            }
+        }
+
+        public void addIcon(Drawable previewIcon) {
+            mIcons.add(previewIcon);
+        }
+
+        /**
+         * @return whether this icon option has overlays and previews for all the required packages
+         */
+        public boolean isValid(Context context) {
+            return getOverlayPackages().keySet().size() ==
+                    ResourceConstants.getPackagesToOverlay(context).length;
+        }
+
+        public void setLabel(String label) {
+            mLabel = label;
+        }
+
+        @Override
+        public Builder buildStep(Builder builder) {
+            for (Drawable icon : mIcons) {
+                builder.addIcon(icon);
+            }
+            return super.buildStep(builder);
+        }
+    }
+
+    public static class ColorOption extends ThemeComponentOption {
+
+        /**
+         * Ids of views used to represent quick setting tiles in the color preview screen
+         */
+        private static int[] COLOR_TILE_IDS = {
+                R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg, R.id.preview_color_qs_2_bg
+        };
+
+        /**
+         * Ids of the views for the foreground of the icon, mapping to the corresponding index of
+         * the actual icon drawable.
+         */
+        static int[][] COLOR_TILES_ICON_IDS = {
+                new int[]{ R.id.preview_color_qs_0_icon, 0},
+                new int[]{ R.id.preview_color_qs_1_icon, 1},
+                new int[] { R.id.preview_color_qs_2_icon, 3}
+        };
+
+        /**
+         * Ids of views used to represent control buttons in the color preview screen
+         */
+        private static int[] COLOR_BUTTON_IDS = {
+                R.id.preview_check_selected, R.id.preview_radio_selected,
+                R.id.preview_toggle_selected
+        };
+
+        @ColorInt private int mColorAccentLight;
+        @ColorInt private int mColorAccentDark;
+        /**
+         * Icons shown as example of QuickSettings tiles in the color preview screen.
+         */
+        private List<Drawable> mIcons = new ArrayList<>();
+
+        /**
+         * Drawable with the currently selected shape to be used as background of the sample
+         * QuickSetting icons in the color preview screen.
+         */
+        private Drawable mShapeDrawable;
+
+        private String mLabel;
+
+        ColorOption(String packageName, String label, @ColorInt int lightColor,
+                @ColorInt int darkColor) {
+            addOverlayPackage(OVERLAY_CATEGORY_COLOR, packageName);
+            mLabel = label;
+            mColorAccentLight = lightColor;
+            mColorAccentDark = darkColor;
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+            @ColorInt int color = resolveColor(view.getResources());
+            LayerDrawable selectedOption = (LayerDrawable) view.getResources().getDrawable(
+                    R.drawable.color_chip_hollow, view.getContext().getTheme());
+            Drawable unselectedOption = view.getResources().getDrawable(
+                    R.drawable.color_chip_filled, view.getContext().getTheme());
+
+            selectedOption.findDrawableByLayerId(R.id.center_fill).setTintList(
+                    ColorStateList.valueOf(color));
+            unselectedOption.setTintList(ColorStateList.valueOf(color));
+
+            StateListDrawable stateListDrawable = new StateListDrawable();
+            stateListDrawable.addState(new int[] {android.R.attr.state_activated}, selectedOption);
+            stateListDrawable.addState(
+                    new int[] {-android.R.attr.state_activated}, unselectedOption);
+
+            ((ImageView) view.findViewById(R.id.option_tile)).setImageDrawable(stateListDrawable);
+            view.setContentDescription(mLabel);
+        }
+
+        @ColorInt
+        private int resolveColor(Resources res) {
+            Configuration configuration = res.getConfiguration();
+            return (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+                    == Configuration.UI_MODE_NIGHT_YES ? mColorAccentDark : mColorAccentLight;
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            CustomThemeManager customThemeManager = (CustomThemeManager) manager;
+            return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_COLOR),
+                    customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_COLOR));
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return R.layout.theme_color_option;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+            container.setContentDescription(
+                    container.getContext().getString(R.string.color_preview_content_description));
+
+            bindPreviewHeader(container, R.string.preview_name_color, R.drawable.ic_colorize_24px,
+                    null);
+
+            ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
+            if (cardBody.getChildCount() == 0) {
+                LayoutInflater.from(container.getContext()).inflate(
+                        R.layout.preview_card_color_content, cardBody, true);
+            }
+            Resources res = container.getResources();
+            @ColorInt int accentColor = resolveColor(res);
+            @ColorInt int controlGreyColor = ResourceUtils.getColorAttr(
+                    container.getContext(),
+                    android.R.attr.textColorTertiary);
+            ColorStateList tintList = new ColorStateList(
+                    new int[][]{
+                            new int[]{android.R.attr.state_selected},
+                            new int[]{android.R.attr.state_checked},
+                            new int[]{-android.R.attr.state_enabled}
+                    },
+                    new int[] {
+                            accentColor,
+                            accentColor,
+                            controlGreyColor
+                    }
+            );
+
+            for (int i = 0; i < COLOR_BUTTON_IDS.length; i++) {
+                CompoundButton button = container.findViewById(COLOR_BUTTON_IDS[i]);
+                button.setButtonTintList(tintList);
+            }
+
+            Switch enabledSwitch = container.findViewById(R.id.preview_toggle_selected);
+            enabledSwitch.setThumbTintList(tintList);
+            enabledSwitch.setTrackTintList(tintList);
+
+            ColorStateList seekbarTintList = ColorStateList.valueOf(accentColor);
+            SeekBar seekbar = container.findViewById(R.id.preview_seekbar);
+            seekbar.setThumbTintList(seekbarTintList);
+            seekbar.setProgressTintList(seekbarTintList);
+            seekbar.setProgressBackgroundTintList(seekbarTintList);
+            // Disable seekbar
+            seekbar.setOnTouchListener((view, motionEvent) -> true);
+
+            int iconFgColor = ResourceUtils.getColorAttr(container.getContext(),
+                    android.R.attr.colorBackground);
+            if (!mIcons.isEmpty() && mShapeDrawable != null) {
+                for (int i = 0; i < COLOR_TILE_IDS.length; i++) {
+                    Drawable icon = mIcons.get(COLOR_TILES_ICON_IDS[i][1]).getConstantState()
+                            .newDrawable();
+                    icon.setTint(iconFgColor);
+                    //TODO: load and set the shape.
+                    Drawable bgShape = mShapeDrawable.getConstantState().newDrawable();
+                    bgShape.setTint(accentColor);
+
+                    ImageView bg = container.findViewById(COLOR_TILE_IDS[i]);
+                    bg.setImageDrawable(bgShape);
+                    ImageView fg = container.findViewById(COLOR_TILES_ICON_IDS[i][0]);
+                    fg.setImageDrawable(icon);
+                }
+            }
+        }
+
+        public void setPreviewIcons(List<Drawable> icons) {
+            mIcons.addAll(icons);
+        }
+
+        public void setShapeDrawable(@Nullable Drawable shapeDrawable) {
+            mShapeDrawable = shapeDrawable;
+        }
+
+        @Override
+        public Builder buildStep(Builder builder) {
+            builder.setColorAccentDark(mColorAccentDark).setColorAccentLight(mColorAccentLight);
+            return super.buildStep(builder);
+        }
+    }
+
+    public static class ShapeOption extends ThemeComponentOption {
+
+        private final LayerDrawable mShape;
+        private final List<ShapeAppIcon> mAppIcons;
+        private final String mLabel;
+        private final Path mPath;
+        private final int mCornerRadius;
+        private int[] mShapeIconIds = {
+                R.id.shape_preview_icon_0, R.id.shape_preview_icon_1, R.id.shape_preview_icon_2,
+                R.id.shape_preview_icon_3, R.id.shape_preview_icon_4, R.id.shape_preview_icon_5
+        };
+
+        ShapeOption(String packageName, String label, Path path,
+                @Dimension int cornerRadius, Drawable shapeDrawable,
+                List<ShapeAppIcon> appIcons) {
+            addOverlayPackage(OVERLAY_CATEGORY_SHAPE, packageName);
+            mLabel = label;
+            mAppIcons = appIcons;
+            mPath = path;
+            mCornerRadius = cornerRadius;
+            Drawable background = shapeDrawable.getConstantState().newDrawable();
+            Drawable foreground = shapeDrawable.getConstantState().newDrawable();
+            mShape = new LayerDrawable(new Drawable[]{background, foreground});
+            mShape.setLayerGravity(0, Gravity.CENTER);
+            mShape.setLayerGravity(1, Gravity.CENTER);
+        }
+
+        @Override
+        public void bindThumbnailTile(View view) {
+            ImageView thumb = view.findViewById(R.id.shape_thumbnail);
+            Resources res = view.getResources();
+            Theme theme = view.getContext().getTheme();
+            int borderWidth = 2 * res.getDimensionPixelSize(R.dimen.option_border_width);
+
+            Drawable background = mShape.getDrawable(0);
+            background.setTintList(res.getColorStateList(R.color.option_border_color, theme));
+
+            ShapeDrawable foreground = (ShapeDrawable) mShape.getDrawable(1);
+
+            foreground.setIntrinsicHeight(background.getIntrinsicHeight() - borderWidth);
+            foreground.setIntrinsicWidth(background.getIntrinsicWidth() - borderWidth);
+            TypedArray ta = view.getContext().obtainStyledAttributes(
+                    new int[]{android.R.attr.colorPrimary});
+            int primaryColor = ta.getColor(0, 0);
+            ta.recycle();
+            int foregroundColor =
+                    ResourceUtils.getColorAttr(view.getContext(), android.R.attr.textColorPrimary);
+
+            foreground.setTint(ColorUtils.blendARGB(primaryColor, foregroundColor, .05f));
+
+            thumb.setImageDrawable(mShape);
+            view.setContentDescription(mLabel);
+        }
+
+        @Override
+        public boolean isActive(CustomizationManager<ThemeComponentOption> manager) {
+            CustomThemeManager customThemeManager = (CustomThemeManager) manager;
+            return Objects.equals(getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE),
+                    customThemeManager.getOverlayPackages().get(OVERLAY_CATEGORY_SHAPE));
+        }
+
+        @Override
+        public int getLayoutResId() {
+            return R.layout.theme_shape_option;
+        }
+
+        @Override
+        public void bindPreview(ViewGroup container) {
+            container.setContentDescription(
+                    container.getContext().getString(R.string.shape_preview_content_description));
+
+            bindPreviewHeader(container, R.string.preview_name_shape, R.drawable.ic_shapes_24px,
+                    null);
+
+            ViewGroup cardBody = container.findViewById(R.id.theme_preview_card_body_container);
+            if (cardBody.getChildCount() == 0) {
+                LayoutInflater.from(container.getContext()).inflate(
+                        R.layout.preview_card_shape_content, cardBody, true);
+            }
+            for (int i = 0; i < mShapeIconIds.length && i < mAppIcons.size(); i++) {
+                ImageView iconView = cardBody.findViewById(mShapeIconIds[i]);
+                iconView.setBackground(mAppIcons.get(i).getDrawableCopy());
+            }
+        }
+
+        @Override
+        public Builder buildStep(Builder builder) {
+            builder.setShapePath(mPath)
+                    .setBottomSheetCornerRadius(mCornerRadius)
+                    .setShapePreviewIcons(mAppIcons);
+            return super.buildStep(builder);
+        }
+    }
+}
diff --git a/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java b/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java
new file mode 100644
index 0000000..992c47c
--- /dev/null
+++ b/src/com/android/customization/model/theme/custom/ThemeComponentOptionProvider.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme.custom;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.os.UserHandle;
+
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.ResourceConstants;
+import com.android.customization.model.theme.OverlayManagerCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class used to retrieve Custom Theme Component options (eg, different fonts)
+ * from the system.
+ */
+public abstract class ThemeComponentOptionProvider<T extends ThemeComponentOption> {
+
+    protected final Context mContext;
+    protected final List<String> mOverlayPackages;
+    protected List<T> mOptions;
+
+    public ThemeComponentOptionProvider(Context context, OverlayManagerCompat manager,
+            String... categories) {
+        mContext = context;
+        mOverlayPackages = new ArrayList<>();
+        for (String category : categories) {
+            mOverlayPackages.addAll(manager.getOverlayPackagesForCategory(category,
+                    UserHandle.myUserId(), ResourceConstants.getPackagesToOverlay(mContext)));
+        }
+    }
+
+    /**
+     * Returns whether there are options for this component available in the current setup.
+     */
+    public boolean isAvailable() {
+        return !mOverlayPackages.isEmpty();
+    }
+
+    /**
+     * Retrieve the available options for this component.
+     * @param callback called when the themes have been retrieved (or immediately if cached)
+     * @param reload whether to reload themes if they're cached.
+     */
+    public void fetch(OptionsFetchedListener<T> callback, boolean reload) {
+        if (mOptions == null || reload) {
+            mOptions = new ArrayList<>();
+            loadOptions();
+        }
+
+        if(callback != null) {
+            callback.onOptionsLoaded(mOptions);
+        }
+    }
+
+    protected abstract void loadOptions();
+
+    protected Resources getOverlayResources(String overlayPackage) throws NameNotFoundException {
+        return mContext.getPackageManager().getResourcesForApplication(overlayPackage);
+    }
+}
diff --git a/src/com/android/customization/picker/theme/CustomThemeActivity.java b/src/com/android/customization/picker/theme/CustomThemeActivity.java
new file mode 100644
index 0000000..62a2f26
--- /dev/null
+++ b/src/com/android/customization/picker/theme/CustomThemeActivity.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2019 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.customization.picker.theme;
+
+import android.app.AlertDialog.Builder;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.theme.DefaultThemeProvider;
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.customization.model.theme.ThemeBundleProvider;
+import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.model.theme.custom.ColorOptionsProvider;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.model.theme.custom.CustomThemeManager;
+import com.android.customization.model.theme.custom.FontOptionsProvider;
+import com.android.customization.model.theme.custom.IconOptionsProvider;
+import com.android.customization.model.theme.custom.ShapeOptionsProvider;
+import com.android.customization.model.theme.custom.ThemeComponentOption;
+import com.android.customization.model.theme.custom.ThemeComponentOption.ColorOption;
+import com.android.customization.model.theme.custom.ThemeComponentOption.FontOption;
+import com.android.customization.model.theme.custom.ThemeComponentOption.IconOption;
+import com.android.customization.model.theme.custom.ThemeComponentOption.ShapeOption;
+import com.android.customization.model.theme.custom.ThemeComponentOptionProvider;
+import com.android.customization.module.CustomizationInjector;
+import com.android.customization.module.ThemesUserEventLogger;
+import com.android.customization.picker.theme.CustomThemeStepFragment.CustomThemeComponentStepHost;
+import com.android.wallpaper.R;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.picker.AppbarFragment.AppbarFragmentHost;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomThemeActivity extends FragmentActivity implements
+        AppbarFragmentHost, CustomThemeComponentStepHost {
+    public static final String EXTRA_THEME_ID = "CustomThemeActivity.ThemeId";
+    public static final String EXTRA_THEME_TITLE = "CustomThemeActivity.ThemeTitle";
+    public static final String EXTRA_THEME_PACKAGES = "CustomThemeActivity.ThemePackages";
+    public static final int REQUEST_CODE_CUSTOM_THEME = 1;
+    public static final int RESULT_THEME_DELETED = 10;
+    public static final int RESULT_THEME_APPLIED = 20;
+
+    private static final String TAG = "CustomThemeActivity";
+    private static final String KEY_STATE_CURRENT_STEP = "CustomThemeActivity.currentStep";
+
+    private ThemesUserEventLogger mUserEventLogger;
+    private List<ComponentStep<?>> mSteps;
+    private int mCurrentStep;
+    private CustomThemeManager mCustomThemeManager;
+    private ThemeManager mThemeManager;
+    private TextView mNextButton;
+    private TextView mPreviousButton;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector();
+        mUserEventLogger = (ThemesUserEventLogger) injector.getUserEventLogger(this);
+        ThemeBundleProvider themeProvider =
+                new DefaultThemeProvider(this, injector.getCustomizationPreferences(this));
+        Intent intent = getIntent();
+        CustomTheme customTheme = null;
+        if (intent != null && intent.hasExtra(EXTRA_THEME_PACKAGES)
+                && intent.hasExtra(EXTRA_THEME_TITLE) && intent.hasExtra(EXTRA_THEME_ID)) {
+            try {
+                CustomTheme.Builder themeBuilder = themeProvider.parseCustomTheme(
+                        intent.getStringExtra(EXTRA_THEME_PACKAGES));
+                if (themeBuilder != null) {
+                    themeBuilder.setId(intent.getStringExtra(EXTRA_THEME_ID));
+                    themeBuilder.setTitle(intent.getStringExtra(EXTRA_THEME_TITLE));
+                    customTheme = themeBuilder.build(this);
+                }
+            } catch (JSONException e) {
+                Log.w(TAG, "Couldn't parse provided custom theme, will override it");
+            }
+        }
+
+        mThemeManager = injector.getThemeManager(
+                new DefaultThemeProvider(this, injector.getCustomizationPreferences(this)),
+                this,
+                new OverlayManagerCompat(this),
+                mUserEventLogger);
+        mThemeManager.fetchOptions(null, false);
+        mCustomThemeManager = CustomThemeManager.create(customTheme, mThemeManager);
+        if (savedInstanceState != null) {
+            mCustomThemeManager.readCustomTheme(themeProvider, savedInstanceState);
+        }
+
+        int currentStep = 0;
+        if (savedInstanceState != null) {
+            currentStep = savedInstanceState.getInt(KEY_STATE_CURRENT_STEP);
+        }
+        initSteps(currentStep);
+
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_custom_theme);
+        mNextButton = findViewById(R.id.next_button);
+        mNextButton.setOnClickListener(view -> onNextOrApply());
+        mPreviousButton = findViewById(R.id.previous_button);
+        mPreviousButton.setOnClickListener(view -> onBackPressed());
+
+        FragmentManager fm = getSupportFragmentManager();
+        Fragment fragment = fm.findFragmentById(R.id.fragment_container);
+        if (fragment == null) {
+            // Navigate to the first step
+            navigateToStep(0);
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(KEY_STATE_CURRENT_STEP, mCurrentStep);
+        if (mCustomThemeManager != null) {
+            mCustomThemeManager.saveCustomTheme(this, outState);
+        }
+    }
+
+    private void navigateToStep(int i) {
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        ComponentStep step = mSteps.get(i);
+        Fragment fragment = step.getFragment(mCustomThemeManager.getOriginalTheme().getTitle());
+
+        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+        fragmentTransaction.replace(R.id.fragment_container, fragment);
+        // Don't add step 0 to the back stack so that going back from it just finishes the Activity
+        if (i > 0) {
+            fragmentTransaction.addToBackStack("Step " + i);
+        }
+        fragmentTransaction.commit();
+        fragmentManager.executePendingTransactions();
+        updateNavigationButtonLabels();
+    }
+
+    private void initSteps(int currentStep) {
+        mSteps = new ArrayList<>();
+        OverlayManagerCompat manager = new OverlayManagerCompat(this);
+        mSteps.add(new FontStep(new FontOptionsProvider(this, manager), 0));
+        mSteps.add(new IconStep(new IconOptionsProvider(this, manager), 1));
+        mSteps.add(new ColorStep(new ColorOptionsProvider(this, manager, mCustomThemeManager), 2));
+        mSteps.add(new ShapeStep(new ShapeOptionsProvider(this, manager), 3));
+        mSteps.add(new NameStep(4));
+        mCurrentStep = currentStep;
+    }
+
+    private void onNextOrApply() {
+        CustomThemeStepFragment stepFragment = getCurrentStepFragment();
+        if (stepFragment instanceof CustomThemeComponentFragment) {
+            CustomThemeComponentFragment fragment = (CustomThemeComponentFragment) stepFragment;
+            mCustomThemeManager.apply(fragment.getSelectedOption(), new Callback() {
+                @Override
+                public void onSuccess() {
+                    navigateToStep(mCurrentStep + 1);
+                }
+
+                @Override
+                public void onError(@Nullable Throwable throwable) {
+                    Log.w(TAG, "Error applying custom theme component", throwable);
+                    Toast.makeText(CustomThemeActivity.this, R.string.apply_theme_error_msg,
+                            Toast.LENGTH_LONG).show();
+                }
+            });
+        } else if (stepFragment instanceof CustomThemeNameFragment) {
+            CustomThemeNameFragment fragment = (CustomThemeNameFragment) stepFragment;
+            CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme();
+
+            // We're on the last step, apply theme and leave
+            CustomTheme themeToApply = mCustomThemeManager.buildPartialCustomTheme(this,
+                    originalTheme.getId(), fragment.getThemeName());
+
+            // If the current theme is equal to the original theme being edited, then
+            // don't search for an equivalent, let the user apply the same one by keeping
+            // it null.
+            ThemeBundle equivalent = (originalTheme.isEquivalent(themeToApply))
+                    ? null : mThemeManager.findThemeByPackages(themeToApply);
+
+            if (equivalent != null) {
+                Builder builder =
+                        new Builder(CustomThemeActivity.this);
+                builder.setTitle(getString(R.string.use_style_instead_title,
+                        equivalent.getTitle()))
+                        .setMessage(getString(R.string.use_style_instead_body,
+                                equivalent.getTitle()))
+                        .setPositiveButton(getString(R.string.use_style_button,
+                                equivalent.getTitle()),
+                                (dialogInterface, i) -> applyTheme(equivalent))
+                        .setNegativeButton(R.string.no_thanks, null)
+                        .create()
+                        .show();
+            } else {
+                applyTheme(themeToApply);
+            }
+        } else {
+            throw new IllegalStateException("Unknown CustomThemeStepFragment");
+        }
+    }
+
+    private void applyTheme(ThemeBundle themeToApply) {
+        mThemeManager.apply(themeToApply, new Callback() {
+            @Override
+            public void onSuccess() {
+                overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
+                Toast.makeText(getApplicationContext(), R.string.applied_theme_msg,
+                        Toast.LENGTH_LONG).show();
+                setResult(RESULT_THEME_APPLIED);
+                finish();
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                Log.w(TAG, "Error applying custom theme", throwable);
+                Toast.makeText(CustomThemeActivity.this,
+                        R.string.apply_theme_error_msg,
+                        Toast.LENGTH_LONG).show();
+            }
+        });
+    }
+
+    private CustomThemeStepFragment getCurrentStepFragment() {
+        return (CustomThemeStepFragment)
+                getSupportFragmentManager().findFragmentById(R.id.fragment_container);
+    }
+
+    @Override
+    public void setCurrentStep(int i) {
+        mCurrentStep = i;
+        updateNavigationButtonLabels();
+    }
+
+    private void updateNavigationButtonLabels() {
+        mPreviousButton.setVisibility(mCurrentStep == 0 ? View.INVISIBLE : View.VISIBLE);
+        mNextButton.setText((mCurrentStep < mSteps.size() -1) ? R.string.custom_theme_next
+                : R.string.apply_btn);
+    }
+
+    @Override
+    public void delete() {
+        mThemeManager.removeCustomTheme(mCustomThemeManager.getOriginalTheme());
+        setResult(RESULT_THEME_DELETED);
+        finish();
+    }
+
+    @Override
+    public void cancel() {
+        finish();
+    }
+
+    @Override
+    public ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider(
+            int position) {
+        return mSteps.get(position).provider;
+    }
+
+    @Override
+    public CustomThemeManager getCustomThemeManager() {
+        return mCustomThemeManager;
+    }
+
+    @Override
+    public void onUpArrowPressed() {
+        // Skip it because CustomThemeStepFragment will implement cancel button
+        // (instead of up arrow) on action bar.
+    }
+
+    @Override
+    public boolean isUpArrowSupported() {
+        // Skip it because CustomThemeStepFragment will implement cancel button
+        // (instead of up arrow) on action bar.
+        return false;
+    }
+
+    /**
+     * Represents a step in selecting a custom theme, picking a particular component (eg font,
+     * color, shape, etc).
+     * Each step has a Fragment instance associated that instances of this class will provide.
+     */
+    private static abstract class ComponentStep<T extends ThemeComponentOption> {
+        @StringRes final int titleResId;
+        @StringRes final int accessibilityResId;
+        final ThemeComponentOptionProvider<T> provider;
+        final int position;
+        private CustomThemeStepFragment mFragment;
+
+        protected ComponentStep(@StringRes int titleResId, @StringRes int accessibilityResId,
+                ThemeComponentOptionProvider<T> provider, int position) {
+            this.titleResId = titleResId;
+            this.accessibilityResId = accessibilityResId;
+            this.provider = provider;
+            this.position = position;
+        }
+
+        CustomThemeStepFragment getFragment(String title) {
+            if (mFragment == null) {
+                mFragment = createFragment(title);
+            }
+            return mFragment;
+        }
+
+        /**
+         * @return a newly created fragment that will handle this step's UI.
+         */
+        abstract CustomThemeStepFragment createFragment(String title);
+    }
+
+    private class FontStep extends ComponentStep<FontOption> {
+
+        protected FontStep(ThemeComponentOptionProvider<FontOption> provider,
+                int position) {
+            super(R.string.font_component_title, R.string.accessibility_custom_font_title, provider,
+                    position);
+        }
+
+        @Override
+        CustomThemeComponentFragment createFragment(String title) {
+            return CustomThemeComponentFragment.newInstance(
+                    title,
+                    position,
+                    titleResId,
+                    accessibilityResId);
+        }
+    }
+
+    private class IconStep extends ComponentStep<IconOption> {
+
+        protected IconStep(ThemeComponentOptionProvider<IconOption> provider,
+                int position) {
+            super(R.string.icon_component_title, R.string.accessibility_custom_icon_title, provider,
+                    position);
+        }
+
+        @Override
+        CustomThemeComponentFragment createFragment(String title) {
+            return CustomThemeComponentFragment.newInstance(
+                    title,
+                    position,
+                    titleResId,
+                    accessibilityResId);
+        }
+    }
+
+    private class ColorStep extends ComponentStep<ColorOption> {
+
+        protected ColorStep(ThemeComponentOptionProvider<ColorOption> provider,
+                int position) {
+            super(R.string.color_component_title, R.string.accessibility_custom_color_title,
+                    provider, position);
+        }
+
+        @Override
+        CustomThemeComponentFragment createFragment(String title) {
+            return CustomThemeComponentFragment.newInstance(
+                    title,
+                    position,
+                    titleResId,
+                    accessibilityResId);
+        }
+    }
+
+    private class ShapeStep extends ComponentStep<ShapeOption> {
+
+        protected ShapeStep(ThemeComponentOptionProvider<ShapeOption> provider,
+                int position) {
+            super(R.string.shape_component_title, R.string.accessibility_custom_shape_title,
+                    provider, position);
+        }
+
+        @Override
+        CustomThemeComponentFragment createFragment(String title) {
+            return CustomThemeComponentFragment.newInstance(
+                    title,
+                    position,
+                    titleResId,
+                    accessibilityResId);
+        }
+    }
+
+    private class NameStep extends ComponentStep {
+
+        protected NameStep(int position) {
+            super(R.string.name_component_title, R.string.accessibility_custom_name_title, null,
+                    position);
+        }
+
+        @Override
+        CustomThemeNameFragment createFragment(String title) {
+            return CustomThemeNameFragment.newInstance(
+                    title,
+                    position,
+                    titleResId,
+                    accessibilityResId);
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java b/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java
new file mode 100644
index 0000000..a1e9967
--- /dev/null
+++ b/src/com/android/customization/picker/theme/CustomThemeComponentFragment.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 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.customization.picker.theme;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.customization.model.theme.custom.ThemeComponentOption;
+import com.android.customization.model.theme.custom.ThemeComponentOptionProvider;
+import com.android.customization.widget.OptionSelectorController;
+import com.android.customization.widget.OptionSelectorController.CheckmarkStyle;
+import com.android.wallpaper.R;
+import com.android.wallpaper.picker.AppbarFragment;
+
+public class CustomThemeComponentFragment extends CustomThemeStepFragment {
+    private static final String ARG_USE_GRID_LAYOUT = "CustomThemeComponentFragment.use_grid";;
+
+    public static CustomThemeComponentFragment newInstance(CharSequence toolbarTitle, int position,
+            int titleResId, int accessibilityResId) {
+        return newInstance(toolbarTitle, position, titleResId, accessibilityResId, false);
+    }
+
+    public static CustomThemeComponentFragment newInstance(CharSequence toolbarTitle, int position,
+            int titleResId, int accessibilityResId, boolean allowGridLayout) {
+        CustomThemeComponentFragment fragment = new CustomThemeComponentFragment();
+        Bundle arguments = AppbarFragment.createArguments(toolbarTitle);
+        arguments.putInt(ARG_KEY_POSITION, position);
+        arguments.putInt(ARG_KEY_TITLE_RES_ID, titleResId);
+        arguments.putInt(ARG_KEY_ACCESSIBILITY_RES_ID, accessibilityResId);
+        arguments.putBoolean(ARG_USE_GRID_LAYOUT, allowGridLayout);
+        fragment.setArguments(arguments);
+        return fragment;
+    }
+
+    private ThemeComponentOptionProvider<? extends ThemeComponentOption> mProvider;
+    private boolean mUseGridLayout;
+
+    private RecyclerView mOptionsContainer;
+    private OptionSelectorController<ThemeComponentOption> mOptionsController;
+    private ThemeComponentOption mSelectedOption;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mUseGridLayout = getArguments().getBoolean(ARG_USE_GRID_LAYOUT);
+        mProvider = mHost.getComponentOptionProvider(mPosition);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = super.onCreateView(inflater, container, savedInstanceState);
+        mOptionsContainer = view.findViewById(R.id.options_container);
+        mPreviewContainer = view.findViewById(R.id.component_preview_content);
+        mTitle = view.findViewById(R.id.component_options_title);
+        mTitle.setText(mTitleResId);
+        setUpOptions();
+
+        return view;
+    }
+
+    @Override
+    protected int getFragmentLayoutResId() {
+        return R.layout.fragment_custom_theme_component;
+    }
+
+    public ThemeComponentOption getSelectedOption() {
+        return mSelectedOption;
+    }
+
+    private void bindPreview() {
+        mSelectedOption.bindPreview(mPreviewContainer);
+    }
+
+    private void setUpOptions() {
+        mProvider.fetch(options -> {
+            mOptionsController = new OptionSelectorController(
+                    mOptionsContainer, options, mUseGridLayout, CheckmarkStyle.NONE);
+
+            mOptionsController.addListener(selected -> {
+                mSelectedOption = (ThemeComponentOption) selected;
+                bindPreview();
+                // Preview and apply. The selection will be kept whatever user goes to previous page
+                // or encounter system config changes, the current selection can be recovered.
+                mCustomThemeManager.apply(mSelectedOption, /* callback= */ null);
+            });
+            mOptionsController.initOptions(mCustomThemeManager);
+
+            for (ThemeComponentOption option : options) {
+                if (option.isActive(mCustomThemeManager)) {
+                    mSelectedOption = option;
+                    break;
+                }
+            }
+            if (mSelectedOption == null) {
+                mSelectedOption = options.get(0);
+            }
+            mOptionsController.setSelectedOption(mSelectedOption);
+        }, false);
+    }
+}
diff --git a/src/com/android/customization/picker/theme/CustomThemeNameFragment.java b/src/com/android/customization/picker/theme/CustomThemeNameFragment.java
new file mode 100644
index 0000000..ea9099f
--- /dev/null
+++ b/src/com/android/customization/picker/theme/CustomThemeNameFragment.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2019 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.customization.picker.theme;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.module.CustomizationInjector;
+import com.android.customization.module.CustomizationPreferences;
+import com.android.customization.picker.WallpaperPreviewer;
+import com.android.wallpaper.R;
+import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.picker.AppbarFragment;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/** Fragment of naming a custom theme. */
+public class CustomThemeNameFragment extends CustomThemeStepFragment {
+
+    private static final String TAG = "CustomThemeNameFragment";
+
+    public static CustomThemeNameFragment newInstance(CharSequence toolbarTitle, int position,
+            int titleResId, int accessibilityResId) {
+        CustomThemeNameFragment fragment = new CustomThemeNameFragment();
+        Bundle arguments = AppbarFragment.createArguments(toolbarTitle);
+        arguments.putInt(ARG_KEY_POSITION, position);
+        arguments.putInt(ARG_KEY_TITLE_RES_ID, titleResId);
+        arguments.putInt(ARG_KEY_ACCESSIBILITY_RES_ID, accessibilityResId);
+        fragment.setArguments(arguments);
+        return fragment;
+    }
+
+    private EditText mNameEditor;
+    private ImageView mWallpaperImage;
+    private ThemeOptionPreviewer mThemeOptionPreviewer;
+    private CustomizationPreferences mCustomizationPreferences;
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = super.onCreateView(inflater, container, savedInstanceState);
+        mTitle = view.findViewById(R.id.component_options_title);
+        mTitle.setText(mTitleResId);
+        CurrentWallpaperInfoFactory currentWallpaperFactory = InjectorProvider.getInjector()
+                .getCurrentWallpaperInfoFactory(getActivity().getApplicationContext());
+        CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector();
+        mCustomizationPreferences = injector.getCustomizationPreferences(getContext());
+
+        // Set theme option.
+        ViewGroup previewContainer = view.findViewById(R.id.theme_preview_container);
+        mThemeOptionPreviewer = new ThemeOptionPreviewer(getLifecycle(), getContext(),
+                previewContainer);
+        PreviewInfo previewInfo = mCustomThemeManager.buildCustomThemePreviewInfo(getContext());
+        mThemeOptionPreviewer.setPreviewInfo(previewInfo);
+
+        // Set wallpaper background.
+        mWallpaperImage = view.findViewById(R.id.wallpaper_preview_image);
+        final WallpaperPreviewer wallpaperPreviewer = new WallpaperPreviewer(
+                getLifecycle(),
+                getActivity(),
+                mWallpaperImage,
+                view.findViewById(R.id.wallpaper_preview_surface));
+        currentWallpaperFactory.createCurrentWallpaperInfos(
+                (homeWallpaper, lockWallpaper, presentationMode) -> {
+                    wallpaperPreviewer.setWallpaper(homeWallpaper,
+                            mThemeOptionPreviewer::updateColorForLauncherWidgets);
+                }, false);
+
+        // Set theme default name.
+        mNameEditor = view.findViewById(R.id.custom_theme_name);
+        mNameEditor.setText(getOriginalThemeName());
+        return view;
+    }
+
+    private String getOriginalThemeName() {
+        CustomTheme originalTheme = mCustomThemeManager.getOriginalTheme();
+        if (originalTheme == null || !originalTheme.isDefined()) {
+            // For new custom theme. use custom themes amount plus 1 as default naming.
+            String serializedThemes = mCustomizationPreferences.getSerializedCustomThemes();
+            int customThemesCount = 0;
+            if (!TextUtils.isEmpty(serializedThemes)) {
+                try {
+                    JSONArray customThemes = new JSONArray(serializedThemes);
+                    customThemesCount = customThemes.length();
+                } catch (JSONException e) {
+                    Log.w(TAG, "Couldn't read stored custom theme");
+                }
+            }
+            return getContext().getString(
+                    R.string.custom_theme_title, customThemesCount + 1);
+        } else {
+            // For existing custom theme, keep its name as default naming.
+            return originalTheme.getTitle();
+        }
+    }
+
+    @Override
+    protected int getFragmentLayoutResId() {
+        return R.layout.fragment_custom_theme_name;
+    }
+
+    public String getThemeName() {
+        return mNameEditor.getText().toString();
+    }
+}
diff --git a/src/com/android/customization/picker/theme/CustomThemeStepFragment.java b/src/com/android/customization/picker/theme/CustomThemeStepFragment.java
new file mode 100644
index 0000000..3f07431
--- /dev/null
+++ b/src/com/android/customization/picker/theme/CustomThemeStepFragment.java
@@ -0,0 +1,116 @@
+package com.android.customization.picker.theme;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.customization.model.theme.custom.CustomThemeManager;
+import com.android.customization.model.theme.custom.ThemeComponentOption;
+import com.android.customization.model.theme.custom.ThemeComponentOptionProvider;
+import com.android.wallpaper.R;
+import com.android.wallpaper.picker.AppbarFragment;
+
+abstract class CustomThemeStepFragment extends AppbarFragment {
+    protected static final String ARG_KEY_POSITION = "CustomThemeStepFragment.position";
+    protected static final String ARG_KEY_TITLE_RES_ID = "CustomThemeStepFragment.title_res";
+    protected static final String ARG_KEY_ACCESSIBILITY_RES_ID =
+            "CustomThemeStepFragment.accessibility_res";
+    protected CustomThemeComponentStepHost mHost;
+    protected CustomThemeManager mCustomThemeManager;
+    protected int mPosition;
+    protected ViewGroup mPreviewContainer;
+    protected TextView mTitle;
+    @StringRes
+    protected int mTitleResId;
+    @StringRes
+    protected int mAccessibilityResId;
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mHost = (CustomThemeComponentStepHost) context;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mHost.setCurrentStep(mPosition);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mPosition = getArguments().getInt(ARG_KEY_POSITION);
+        mTitleResId = getArguments().getInt(ARG_KEY_TITLE_RES_ID);
+        mAccessibilityResId = getArguments().getInt(ARG_KEY_ACCESSIBILITY_RES_ID);
+        mCustomThemeManager = mHost.getCustomThemeManager();
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(
+                getFragmentLayoutResId(), container, /* attachToRoot */ false);
+        // No original theme means it's a new one, so no toolbar icon for deleting it is needed
+        if (mCustomThemeManager.getOriginalTheme() == null
+                || !mCustomThemeManager.getOriginalTheme().isDefined()) {
+            setUpToolbar(view);
+        } else {
+            setUpToolbar(view, R.menu.custom_theme_editor_menu);
+            mToolbar.getMenu().getItem(0).setIconTintList(
+                    getContext().getColorStateList(R.color.toolbar_icon_tint));
+        }
+        Drawable closeIcon = getResources().getDrawable(R.drawable.ic_close_24px, null).mutate();
+        closeIcon.setTintList(getResources().getColorStateList(R.color.toolbar_icon_tint, null));
+        mToolbar.setNavigationIcon(closeIcon);
+
+        mToolbar.setNavigationContentDescription(R.string.cancel);
+        mToolbar.setNavigationOnClickListener(v -> mHost.cancel());
+
+        mPreviewContainer = view.findViewById(R.id.component_preview_content);
+        return view;
+    }
+
+    @Override
+    protected String getAccessibilityTitle() {
+        return getString(mAccessibilityResId);
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (item.getItemId() == R.id.custom_theme_delete) {
+            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+            builder.setMessage(R.string.delete_custom_theme_confirmation)
+                    .setPositiveButton(R.string.delete_custom_theme_button,
+                            (dialogInterface, i) -> mHost.delete())
+                    .setNegativeButton(R.string.cancel, null)
+                    .create()
+                    .show();
+            return true;
+        }
+        return super.onMenuItemClick(item);
+    }
+
+    protected abstract int getFragmentLayoutResId();
+
+    public interface CustomThemeComponentStepHost {
+        void delete();
+        void cancel();
+        ThemeComponentOptionProvider<? extends ThemeComponentOption> getComponentOptionProvider(
+                int position);
+
+        CustomThemeManager getCustomThemeManager();
+
+        void setCurrentStep(int step);
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeFragment.java b/src/com/android/customization/picker/theme/ThemeFragment.java
new file mode 100644
index 0000000..3a9a56f
--- /dev/null
+++ b/src/com/android/customization/picker/theme/ThemeFragment.java
@@ -0,0 +1,402 @@
+/*
+ * 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.customization.picker.theme;
+
+import static android.app.Activity.RESULT_OK;
+
+import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY;
+import static com.android.wallpaper.widget.BottomActionBar.BottomAction.CUSTOMIZE;
+import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.widget.ContentLoadingProgressBar;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.model.CustomizationManager.OptionsFetchedListener;
+import com.android.customization.model.CustomizationOption;
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.customization.model.theme.ThemeManager;
+import com.android.customization.model.theme.custom.CustomTheme;
+import com.android.customization.module.ThemesUserEventLogger;
+import com.android.customization.picker.WallpaperPreviewer;
+import com.android.customization.widget.OptionSelectorController;
+import com.android.wallpaper.R;
+import com.android.wallpaper.model.WallpaperInfo;
+import com.android.wallpaper.module.CurrentWallpaperInfoFactory;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.picker.AppbarFragment;
+import com.android.wallpaper.widget.BottomActionBar;
+import com.android.wallpaper.widget.BottomActionBar.AccessibilityCallback;
+import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent;
+
+import java.util.List;
+
+/**
+ * Fragment that contains the main UI for selecting and applying a ThemeBundle.
+ */
+public class ThemeFragment extends AppbarFragment {
+
+    private static final String TAG = "ThemeFragment";
+    private static final String KEY_SELECTED_THEME = "ThemeFragment.SelectedThemeBundle";
+    private static final String KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE =
+            "ThemeFragment.bottomActionBarVisible";
+    private static final int FULL_PREVIEW_REQUEST_CODE = 1000;
+
+    /**
+     * Interface to be implemented by an Activity hosting a {@link ThemeFragment}
+     */
+    public interface ThemeFragmentHost {
+        ThemeManager getThemeManager();
+    }
+    public static ThemeFragment newInstance(CharSequence title) {
+        ThemeFragment fragment = new ThemeFragment();
+        fragment.setArguments(AppbarFragment.createArguments(title));
+        return fragment;
+    }
+
+    private RecyclerView mOptionsContainer;
+    private OptionSelectorController<ThemeBundle> mOptionsController;
+    private ThemeManager mThemeManager;
+    private ThemesUserEventLogger mEventLogger;
+    private ThemeBundle mSelectedTheme;
+    private ContentLoadingProgressBar mLoading;
+    private View mContent;
+    private View mError;
+    private WallpaperInfo mCurrentHomeWallpaper;
+    private CurrentWallpaperInfoFactory mCurrentWallpaperFactory;
+    private BottomActionBar mBottomActionBar;
+    private WallpaperPreviewer mWallpaperPreviewer;
+    private ImageView mWallpaperImage;
+    private ThemeOptionPreviewer mThemeOptionPreviewer;
+    private ThemeInfoView mThemeInfoView;
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mThemeManager = ((ThemeFragmentHost) context).getThemeManager();
+        mEventLogger = (ThemesUserEventLogger)
+                InjectorProvider.getInjector().getUserEventLogger(context);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(
+                R.layout.fragment_theme_picker, container, /* attachToRoot */ false);
+        setUpToolbar(view);
+
+        mContent = view.findViewById(R.id.content_section);
+        mLoading = view.findViewById(R.id.loading_indicator);
+        mError = view.findViewById(R.id.error_section);
+        mCurrentWallpaperFactory = InjectorProvider.getInjector()
+                .getCurrentWallpaperInfoFactory(getActivity().getApplicationContext());
+        mOptionsContainer = view.findViewById(R.id.options_container);
+
+        mThemeOptionPreviewer = new ThemeOptionPreviewer(
+                getLifecycle(),
+                getContext(),
+                view.findViewById(R.id.theme_preview_container));
+
+        // Set Wallpaper background.
+        mWallpaperImage = view.findViewById(R.id.wallpaper_preview_image);
+        mWallpaperPreviewer = new WallpaperPreviewer(
+                getLifecycle(),
+                getActivity(),
+                mWallpaperImage,
+                view.findViewById(R.id.wallpaper_preview_surface));
+        mCurrentWallpaperFactory.createCurrentWallpaperInfos(
+                (homeWallpaper, lockWallpaper, presentationMode) -> {
+                    mCurrentHomeWallpaper = homeWallpaper;
+                    mWallpaperPreviewer.setWallpaper(mCurrentHomeWallpaper,
+                            mThemeOptionPreviewer::updateColorForLauncherWidgets);
+                }, false);
+        return view;
+    }
+
+    @Override
+    protected void onBottomActionBarReady(BottomActionBar bottomActionBar) {
+        super.onBottomActionBarReady(bottomActionBar);
+        mBottomActionBar = bottomActionBar;
+        mBottomActionBar.showActionsOnly(INFORMATION, APPLY);
+        mBottomActionBar.setActionClickListener(APPLY, v -> {
+            mBottomActionBar.disableActions();
+            applyTheme();
+        });
+
+        mBottomActionBar.bindBottomSheetContentWithAction(
+                new ThemeInfoContent(getContext()), INFORMATION);
+        mBottomActionBar.setActionClickListener(CUSTOMIZE, this::onCustomizeClicked);
+
+        // Update target view's accessibility param since it will be blocked by the bottom sheet
+        // when expanded.
+        mBottomActionBar.setAccessibilityCallback(new AccessibilityCallback() {
+            @Override
+            public void onBottomSheetCollapsed() {
+                mOptionsContainer.setImportantForAccessibility(
+                        View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            }
+
+            @Override
+            public void onBottomSheetExpanded() {
+                mOptionsContainer.setImportantForAccessibility(
+                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+            }
+        });
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        // Setup options here when all views are ready(including BottomActionBar), since we need to
+        // update views after options are loaded.
+        setUpOptions(savedInstanceState);
+    }
+
+    private void applyTheme() {
+        mThemeManager.apply(mSelectedTheme, new Callback() {
+            @Override
+            public void onSuccess() {
+                Toast.makeText(getContext(), R.string.applied_theme_msg, Toast.LENGTH_LONG).show();
+                getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
+                getActivity().finish();
+            }
+
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                Log.w(TAG, "Error applying theme", throwable);
+                // Since we disabled it when clicked apply button.
+                mBottomActionBar.enableActions();
+                mBottomActionBar.hide();
+                Toast.makeText(getContext(), R.string.apply_theme_error_msg,
+                        Toast.LENGTH_LONG).show();
+            }
+        });
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mSelectedTheme != null && !mSelectedTheme.isActive(mThemeManager)) {
+            outState.putString(KEY_SELECTED_THEME, mSelectedTheme.getSerializedPackages());
+        }
+        if (mBottomActionBar != null) {
+            outState.putBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE, mBottomActionBar.isVisible());
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME) {
+            if (resultCode == CustomThemeActivity.RESULT_THEME_DELETED) {
+                mSelectedTheme = null;
+                reloadOptions();
+            } else if (resultCode == CustomThemeActivity.RESULT_THEME_APPLIED) {
+                getActivity().overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
+                getActivity().finish();
+            } else {
+                if (mSelectedTheme != null) {
+                    mOptionsController.setSelectedOption(mSelectedTheme);
+                    // Set selected option above will show BottomActionBar,
+                    // hide BottomActionBar for the mis-trigger.
+                    mBottomActionBar.hide();
+                } else {
+                    reloadOptions();
+                }
+            }
+        } else if (requestCode == FULL_PREVIEW_REQUEST_CODE && resultCode == RESULT_OK) {
+            applyTheme();
+        }
+        super.onActivityResult(requestCode, resultCode, data);
+    }
+
+    private void onCustomizeClicked(View view) {
+        if (mSelectedTheme instanceof CustomTheme) {
+            navigateToCustomTheme((CustomTheme) mSelectedTheme);
+        }
+    }
+
+    private void hideError() {
+        mContent.setVisibility(View.VISIBLE);
+        mError.setVisibility(View.GONE);
+    }
+
+    private void showError() {
+        mLoading.hide();
+        mContent.setVisibility(View.GONE);
+        mError.setVisibility(View.VISIBLE);
+    }
+
+    private void setUpOptions(@Nullable Bundle savedInstanceState) {
+        hideError();
+        mLoading.show();
+        mThemeManager.fetchOptions(new OptionsFetchedListener<ThemeBundle>() {
+            @Override
+            public void onOptionsLoaded(List<ThemeBundle> options) {
+                mOptionsController = new OptionSelectorController<>(mOptionsContainer, options);
+                mOptionsController.initOptions(mThemeManager);
+
+                // Find out the selected theme option.
+                // 1. Find previously selected theme.
+                String previouslySelected = savedInstanceState != null
+                        ? savedInstanceState.getString(KEY_SELECTED_THEME) : null;
+                ThemeBundle previouslySelectedTheme = null;
+                ThemeBundle activeTheme = null;
+                for (ThemeBundle theme : options) {
+                    if (previouslySelected != null
+                            && previouslySelected.equals(theme.getSerializedPackages())) {
+                        previouslySelectedTheme = theme;
+                    }
+                    if (theme.isActive(mThemeManager)) {
+                        activeTheme = theme;
+                    }
+                }
+                // 2. Use active theme if no previously selected theme.
+                mSelectedTheme = previouslySelectedTheme != null
+                        ? previouslySelectedTheme
+                        : activeTheme;
+                // 3. Select the first system theme(default theme currently)
+                //    if there is no matching custom enabled theme.
+                if (mSelectedTheme == null) {
+                    mSelectedTheme = findFirstSystemThemeBundle(options);
+                }
+
+                mOptionsController.setSelectedOption(mSelectedTheme);
+                onOptionSelected(mSelectedTheme);
+                restoreBottomActionBarVisibility(savedInstanceState);
+
+                mOptionsController.addListener(selectedOption -> {
+                    onOptionSelected(selectedOption);
+                    if (!isAddCustomThemeOption(selectedOption)) {
+                        mBottomActionBar.show();
+                    }
+                });
+                mLoading.hide();
+            }
+            @Override
+            public void onError(@Nullable Throwable throwable) {
+                if (throwable != null) {
+                    Log.e(TAG, "Error loading theme bundles", throwable);
+                }
+                showError();
+            }
+        }, false);
+    }
+
+    private void reloadOptions() {
+        mThemeManager.fetchOptions(options -> {
+            mOptionsController.resetOptions(options);
+            for (ThemeBundle theme : options) {
+                if (theme.isActive(mThemeManager)) {
+                    mSelectedTheme = theme;
+                    break;
+                }
+            }
+            if (mSelectedTheme == null) {
+                mSelectedTheme = findFirstSystemThemeBundle(options);
+            }
+            mOptionsController.setSelectedOption(mSelectedTheme);
+            // Set selected option above will show BottomActionBar,
+            // hide BottomActionBar for the mis-trigger.
+            mBottomActionBar.hide();
+        }, true);
+    }
+
+    private ThemeBundle findFirstSystemThemeBundle(List<ThemeBundle> options) {
+        for (ThemeBundle bundle : options) {
+            if (!(bundle instanceof CustomTheme)) {
+                return bundle;
+            }
+        }
+        return null;
+    }
+
+    private void onOptionSelected(CustomizationOption selectedOption) {
+        if (isAddCustomThemeOption(selectedOption)) {
+            navigateToCustomTheme((CustomTheme) selectedOption);
+        } else {
+            mSelectedTheme = (ThemeBundle) selectedOption;
+            mSelectedTheme.setOverrideThemeWallpaper(mCurrentHomeWallpaper);
+            mEventLogger.logThemeSelected(mSelectedTheme,
+                    selectedOption instanceof CustomTheme);
+            mThemeOptionPreviewer.setPreviewInfo(mSelectedTheme.getPreviewInfo());
+            if (mThemeInfoView != null && mSelectedTheme != null) {
+                mThemeInfoView.populateThemeInfo(mSelectedTheme);
+            }
+
+            if (selectedOption instanceof CustomTheme) {
+                mBottomActionBar.showActionsOnly(INFORMATION, CUSTOMIZE, APPLY);
+            } else {
+                mBottomActionBar.showActionsOnly(INFORMATION, APPLY);
+            }
+        }
+    }
+
+    private void restoreBottomActionBarVisibility(@Nullable Bundle savedInstanceState) {
+        boolean isBottomActionBarVisible = savedInstanceState != null
+                && savedInstanceState.getBoolean(KEY_STATE_BOTTOM_ACTION_BAR_VISIBLE);
+        if (isBottomActionBarVisible) {
+            mBottomActionBar.show();
+        } else {
+            mBottomActionBar.hide();
+        }
+    }
+
+    private boolean isAddCustomThemeOption(CustomizationOption option) {
+        return option instanceof CustomTheme && !((CustomTheme) option).isDefined();
+    }
+
+    private void navigateToCustomTheme(CustomTheme themeToEdit) {
+        Intent intent = new Intent(getActivity(), CustomThemeActivity.class);
+        intent.putExtra(CustomThemeActivity.EXTRA_THEME_TITLE, themeToEdit.getTitle());
+        intent.putExtra(CustomThemeActivity.EXTRA_THEME_ID, themeToEdit.getId());
+        intent.putExtra(CustomThemeActivity.EXTRA_THEME_PACKAGES,
+                themeToEdit.getSerializedPackages());
+        startActivityForResult(intent, CustomThemeActivity.REQUEST_CODE_CUSTOM_THEME);
+    }
+
+    private final class ThemeInfoContent extends BottomSheetContent<ThemeInfoView> {
+
+        private ThemeInfoContent(Context context) {
+            super(context);
+        }
+
+        @Override
+        public int getViewId() {
+            return R.layout.theme_info_view;
+        }
+
+        @Override
+        public void onViewCreated(ThemeInfoView view) {
+            mThemeInfoView = view;
+            if (mSelectedTheme != null) {
+                mThemeInfoView.populateThemeInfo(mSelectedTheme);
+            }
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java b/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java
new file mode 100644
index 0000000..3ba64ec
--- /dev/null
+++ b/src/com/android/customization/picker/theme/ThemeFullPreviewFragment.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 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.customization.picker.theme;
+
+import static android.app.Activity.RESULT_OK;
+
+import static com.android.wallpaper.widget.BottomActionBar.BottomAction.APPLY;
+import static com.android.wallpaper.widget.BottomActionBar.BottomAction.INFORMATION;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.theme.DefaultThemeProvider;
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.customization.model.theme.ThemeBundleProvider;
+import com.android.customization.module.CustomizationInjector;
+import com.android.customization.picker.WallpaperPreviewer;
+import com.android.wallpaper.R;
+import com.android.wallpaper.model.WallpaperInfo;
+import com.android.wallpaper.module.InjectorProvider;
+import com.android.wallpaper.picker.AppbarFragment;
+import com.android.wallpaper.widget.BottomActionBar;
+import com.android.wallpaper.widget.BottomActionBar.BottomSheetContent;
+
+import com.bumptech.glide.Glide;
+
+import org.json.JSONException;
+
+/** A Fragment for theme full preview page. */
+public class ThemeFullPreviewFragment extends AppbarFragment {
+    private static final String TAG = "ThemeFullPreviewFragment";
+
+    public static final String EXTRA_THEME_OPTION_TITLE = "theme_option_title";
+    protected static final String EXTRA_THEME_OPTION = "theme_option";
+    protected static final String EXTRA_WALLPAPER_INFO = "wallpaper_info";
+    protected static final String EXTRA_CAN_APPLY_FROM_FULL_PREVIEW = "can_apply";
+
+    private WallpaperInfo mWallpaper;
+    private ThemeBundle mThemeBundle;
+    private boolean mCanApplyFromFullPreview;
+
+    /**
+     * Returns a new {@link ThemeFullPreviewFragment} with the provided title and bundle arguments
+     * set.
+     */
+    public static ThemeFullPreviewFragment newInstance(CharSequence title, Bundle intentBundle) {
+        ThemeFullPreviewFragment fragment = new ThemeFullPreviewFragment();
+        Bundle bundle = new Bundle();
+        bundle.putAll(AppbarFragment.createArguments(title));
+        bundle.putAll(intentBundle);
+        fragment.setArguments(bundle);
+        return fragment;
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mWallpaper = getArguments().getParcelable(EXTRA_WALLPAPER_INFO);
+        mCanApplyFromFullPreview = getArguments().getBoolean(EXTRA_CAN_APPLY_FROM_FULL_PREVIEW);
+        CustomizationInjector injector = (CustomizationInjector) InjectorProvider.getInjector();
+        ThemeBundleProvider themeProvider = new DefaultThemeProvider(
+                getContext(), injector.getCustomizationPreferences(getContext()));
+        try {
+            ThemeBundle.Builder builder = themeProvider.parseThemeBundle(
+                    getArguments().getString(EXTRA_THEME_OPTION));
+            if (builder != null) {
+                builder.setTitle(getArguments().getString(EXTRA_THEME_OPTION_TITLE));
+                mThemeBundle = builder.build(getContext());
+            }
+        } catch (JSONException e) {
+            Log.w(TAG, "Couldn't parse provided custom theme, will override it");
+            // TODO(chihhangchuang): Handle the error case.
+        }
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(
+                R.layout.fragment_theme_full_preview, container, /* attachToRoot */ false);
+        setUpToolbar(view);
+        Glide.get(getContext()).clearMemory();
+
+        // Set theme option.
+        final ThemeOptionPreviewer themeOptionPreviewer = new ThemeOptionPreviewer(
+                getLifecycle(),
+                getContext(),
+                view.findViewById(R.id.theme_preview_container));
+        themeOptionPreviewer.setPreviewInfo(mThemeBundle.getPreviewInfo());
+
+        // Set wallpaper background.
+        ImageView wallpaperImageView = view.findViewById(R.id.wallpaper_preview_image);
+        final WallpaperPreviewer wallpaperPreviewer = new WallpaperPreviewer(
+                getLifecycle(),
+                getActivity(),
+                wallpaperImageView,
+                view.findViewById(R.id.wallpaper_preview_surface));
+        wallpaperPreviewer.setWallpaper(mWallpaper,
+                themeOptionPreviewer::updateColorForLauncherWidgets);
+        return view;
+    }
+
+    @Override
+    protected void onBottomActionBarReady(BottomActionBar bottomActionBar) {
+        super.onBottomActionBarReady(bottomActionBar);
+        if (mCanApplyFromFullPreview) {
+            bottomActionBar.showActionsOnly(INFORMATION, APPLY);
+            bottomActionBar.setActionClickListener(APPLY, v -> finishActivityWithResultOk());
+        } else {
+            bottomActionBar.showActionsOnly(INFORMATION);
+        }
+        bottomActionBar.bindBottomSheetContentWithAction(
+                new ThemeInfoContent(getContext()), INFORMATION);
+        bottomActionBar.show();
+    }
+
+    private void finishActivityWithResultOk() {
+        Activity activity = requireActivity();
+        activity.overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
+        Intent intent = new Intent();
+        activity.setResult(RESULT_OK, intent);
+        activity.finish();
+    }
+
+    private final class ThemeInfoContent extends BottomSheetContent<ThemeInfoView> {
+
+        private ThemeInfoContent(Context context) {
+            super(context);
+        }
+
+        @Override
+        public int getViewId() {
+            return R.layout.theme_info_view;
+        }
+
+        @Override
+        public void onViewCreated(ThemeInfoView view) {
+            view.populateThemeInfo(mThemeBundle);
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeInfoView.java b/src/com/android/customization/picker/theme/ThemeInfoView.java
new file mode 100644
index 0000000..e929c4d
--- /dev/null
+++ b/src/com/android/customization/picker/theme/ThemeInfoView.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2020 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.customization.picker.theme;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.wallpaper.R;
+
+/** A view for displaying style info. */
+public class ThemeInfoView extends LinearLayout {
+    private static final int WIFI_ICON_PREVIEW_INDEX = 0;
+    private static final int SHAPE_PREVIEW_INDEX = 0;
+
+    private TextView mTitle;
+    private TextView mFontPreviewTextView;
+    private ImageView mIconPreviewImageView;
+    private ImageView mAppPreviewImageView;
+    private ImageView mShapePreviewImageView;
+
+    public ThemeInfoView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTitle = findViewById(R.id.style_info_title);
+        mFontPreviewTextView = findViewById(R.id.font_preview);
+        mIconPreviewImageView = findViewById(R.id.qs_preview_icon);
+        mAppPreviewImageView = findViewById(R.id.app_preview_icon);
+        mShapePreviewImageView = findViewById(R.id.shape_preview_icon);
+    }
+
+    /** Populates theme info. */
+    public void populateThemeInfo(@NonNull ThemeBundle selectedTheme) {
+        ThemeBundle.PreviewInfo previewInfo = selectedTheme.getPreviewInfo();
+
+        if (previewInfo != null) {
+            mTitle.setText(getContext().getString(R.string.style_info_description));
+            if (previewInfo.headlineFontFamily != null) {
+                mTitle.setTypeface(previewInfo.headlineFontFamily);
+                mFontPreviewTextView.setTypeface(previewInfo.headlineFontFamily);
+            }
+
+            if (previewInfo.icons.get(WIFI_ICON_PREVIEW_INDEX) != null) {
+                mIconPreviewImageView.setImageDrawable(
+                        previewInfo.icons.get(WIFI_ICON_PREVIEW_INDEX));
+            }
+
+            if (previewInfo.shapeAppIcons.get(SHAPE_PREVIEW_INDEX) != null) {
+                mAppPreviewImageView.setBackground(
+                        previewInfo.shapeAppIcons.get(SHAPE_PREVIEW_INDEX).getDrawableCopy());
+            }
+
+            if (previewInfo.shapeDrawable != null) {
+                mShapePreviewImageView.setImageDrawable(previewInfo.shapeDrawable);
+                mShapePreviewImageView.setImageTintList(
+                        ColorStateList.valueOf(previewInfo.resolveAccentColor(getResources())));
+            }
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java b/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java
new file mode 100644
index 0000000..14b53ec
--- /dev/null
+++ b/src/com/android/customization/picker/theme/ThemeOptionPreviewer.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2020 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.customization.picker.theme;
+
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import android.app.WallpaperColors;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.text.format.DateFormat;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+
+import com.android.customization.model.theme.ThemeBundle;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo;
+import com.android.customization.model.theme.ThemeBundle.PreviewInfo.ShapeAppIcon;
+import com.android.wallpaper.R;
+import com.android.wallpaper.util.ResourceUtils;
+import com.android.wallpaper.util.ScreenSizeCalculator;
+import com.android.wallpaper.util.TimeUtils;
+import com.android.wallpaper.util.TimeUtils.TimeTicker;
+
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** A class to load the {@link ThemeBundle} preview to the view. */
+class ThemeOptionPreviewer implements LifecycleObserver {
+    private static final String DATE_FORMAT = "EEEE, MMM d";
+
+    // Maps which icon from ResourceConstants#ICONS_FOR_PREVIEW.
+    private static final int ICON_WIFI = 0;
+    private static final int ICON_BLUETOOTH = 1;
+    private static final int ICON_FLASHLIGHT = 3;
+    private static final int ICON_AUTO_ROTATE = 4;
+    private static final int ICON_CELLULAR_SIGNAL = 6;
+    private static final int ICON_BATTERY = 7;
+
+    // Icons in the top bar (fake "status bar") with the particular order.
+    private static final int [] sTopBarIconToPreviewIcon = new int [] {
+            ICON_WIFI, ICON_CELLULAR_SIGNAL, ICON_BATTERY };
+
+    // Ids of app icon shape preview.
+    private int[] mShapeAppIconIds = {
+            R.id.shape_preview_icon_0, R.id.shape_preview_icon_1,
+            R.id.shape_preview_icon_2, R.id.shape_preview_icon_3
+    };
+    private int[] mShapeIconAppNameIds = {
+            R.id.shape_preview_icon_app_name_0, R.id.shape_preview_icon_app_name_1,
+            R.id.shape_preview_icon_app_name_2, R.id.shape_preview_icon_app_name_3
+    };
+
+    // Ids of color/icons section.
+    private int[][] mColorTileIconIds = {
+            new int[] { R.id.preview_color_qs_0_icon, ICON_WIFI},
+            new int[] { R.id.preview_color_qs_1_icon, ICON_BLUETOOTH},
+            new int[] { R.id.preview_color_qs_2_icon, ICON_FLASHLIGHT},
+            new int[] { R.id.preview_color_qs_3_icon, ICON_AUTO_ROTATE},
+    };
+    private int[] mColorTileIds = {
+            R.id.preview_color_qs_0_bg, R.id.preview_color_qs_1_bg,
+            R.id.preview_color_qs_2_bg, R.id.preview_color_qs_3_bg
+    };
+    private int[] mColorButtonIds = {
+            R.id.preview_check_selected, R.id.preview_radio_selected, R.id.preview_toggle_selected
+    };
+
+    private final Context mContext;
+
+    private View mContentView;
+    private TextView mStatusBarClock;
+    private TextView mSmartSpaceDate;
+    private TimeTicker mTicker;
+
+    private boolean mHasPreviewInfoSet;
+    private boolean mHasWallpaperColorSet;
+
+    ThemeOptionPreviewer(Lifecycle lifecycle, Context context, ViewGroup previewContainer) {
+        lifecycle.addObserver(this);
+
+        mContext = context;
+        mContentView = LayoutInflater.from(context).inflate(
+                R.layout.theme_preview_content, /* root= */ null);
+        mContentView.setVisibility(View.INVISIBLE);
+        mStatusBarClock = mContentView.findViewById(R.id.theme_preview_clock);
+        mSmartSpaceDate = mContentView.findViewById(R.id.smart_space_date);
+        updateTime();
+        final float screenAspectRatio =
+                ScreenSizeCalculator.getInstance().getScreenAspectRatio(mContext);
+        Configuration config = mContext.getResources().getConfiguration();
+        final boolean directionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
+        previewContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+            @Override
+            public void onLayoutChange(View view, int left, int top, int right, int bottom,
+                                       int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                // Calculate the full preview card height and width.
+                final int fullPreviewCardHeight = getFullPreviewCardHeight();
+                final int fullPreviewCardWidth = (int) (fullPreviewCardHeight / screenAspectRatio);
+
+                // Relayout the content view to match full preview card size.
+                mContentView.measure(
+                        makeMeasureSpec(fullPreviewCardWidth, EXACTLY),
+                        makeMeasureSpec(fullPreviewCardHeight, EXACTLY));
+                mContentView.layout(0, 0, fullPreviewCardWidth, fullPreviewCardHeight);
+
+                // Scale the content view from full preview size to the container size. For full
+                // preview, the scale value is 1.
+                float scale = (float) previewContainer.getMeasuredHeight() / fullPreviewCardHeight;
+                mContentView.setScaleX(scale);
+                mContentView.setScaleY(scale);
+                // The pivot point is centered by default, set to (0, 0).
+                mContentView.setPivotX(directionLTR ? 0f : mContentView.getMeasuredWidth());
+                mContentView.setPivotY(0f);
+
+                // Ensure there will be only one content view in the container.
+                previewContainer.removeAllViews();
+                // Finally, add the content view to the container.
+                previewContainer.addView(
+                        mContentView,
+                        mContentView.getMeasuredWidth(),
+                        mContentView.getMeasuredHeight());
+
+                previewContainer.removeOnLayoutChangeListener(this);
+            }
+        });
+    }
+
+    /** Loads the Theme option preview into the container view. */
+    public void setPreviewInfo(PreviewInfo previewInfo) {
+        setHeadlineFont(previewInfo.headlineFontFamily);
+        setBodyFont(previewInfo.bodyFontFamily);
+        setTopBarIcons(previewInfo.icons);
+        setAppIconShape(previewInfo.shapeAppIcons);
+        setColorAndIconsSection(previewInfo.icons, previewInfo.shapeDrawable,
+                previewInfo.resolveAccentColor(mContext.getResources()));
+        setColorAndIconsBoxRadius(previewInfo.bottomSheeetCornerRadius);
+        setQsbRadius(previewInfo.bottomSheeetCornerRadius);
+        mHasPreviewInfoSet = true;
+        showPreviewIfHasAllConfigSet();
+    }
+
+    /**
+     * Updates the color of widgets in launcher (like top status bar, smart space, and app name
+     * text) which will change its content color according to different wallpapers.
+     *
+     * @param colors the {@link WallpaperColors} of the wallpaper, or {@code null} to use light
+     *               color as default
+     */
+    public void updateColorForLauncherWidgets(@Nullable WallpaperColors colors) {
+        boolean useLightTextColor = colors == null
+                || (colors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0;
+        int textColor = mContext.getColor(useLightTextColor
+                ? android.R.color.white
+                : android.R.color.black);
+        int textShadowColor = mContext.getColor(useLightTextColor
+                ? android.R.color.tertiary_text_dark
+                : android.R.color.transparent);
+        // Update the top status bar clock text color.
+        mStatusBarClock.setTextColor(textColor);
+        // Update the top status bar icon color.
+        ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons);
+        for (int i = 0; i < iconsContainer.getChildCount(); i++) {
+            ((ImageView) iconsContainer.getChildAt(i))
+                    .setImageTintList(ColorStateList.valueOf(textColor));
+        }
+        // Update smart space date color.
+        mSmartSpaceDate.setTextColor(textColor);
+        mSmartSpaceDate.setShadowLayer(
+                mContext.getResources().getDimension(
+                        R.dimen.smartspace_preview_key_ambient_shadow_blur),
+                /* dx = */ 0,
+                /* dy = */ 0,
+                textShadowColor);
+
+        // Update shape app icon name text color.
+        for (int id : mShapeIconAppNameIds) {
+            TextView appName = mContentView.findViewById(id);
+            appName.setTextColor(textColor);
+            appName.setShadowLayer(
+                    mContext.getResources().getDimension(
+                            R.dimen.preview_theme_app_name_key_ambient_shadow_blur),
+                    /* dx = */ 0,
+                    /* dy = */ 0,
+                    textShadowColor);
+        }
+
+        mHasWallpaperColorSet = true;
+        showPreviewIfHasAllConfigSet();
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+    @MainThread
+    public void onResume() {
+        mTicker = TimeTicker.registerNewReceiver(mContext, this::updateTime);
+        updateTime();
+    }
+
+    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+    @MainThread
+    public void onPause() {
+        if (mContext != null) {
+            mContext.unregisterReceiver(mTicker);
+        }
+    }
+
+    private void showPreviewIfHasAllConfigSet() {
+        if (mHasPreviewInfoSet && mHasWallpaperColorSet
+                && mContentView.getVisibility() != View.VISIBLE) {
+            mContentView.setAlpha(0f);
+            mContentView.setVisibility(View.VISIBLE);
+            mContentView.animate().alpha(1f)
+                    .setStartDelay(50)
+                    .setDuration(200)
+                    .setInterpolator(AnimationUtils.loadInterpolator(mContext,
+                            android.R.interpolator.fast_out_linear_in))
+                    .start();
+        }
+    }
+
+    private void setHeadlineFont(Typeface headlineFont) {
+        mStatusBarClock.setTypeface(headlineFont);
+        mSmartSpaceDate.setTypeface(headlineFont);
+
+        // Update font of color/icons section title.
+        TextView colorIconsSectionTitle = mContentView.findViewById(R.id.color_icons_section_title);
+        colorIconsSectionTitle.setTypeface(headlineFont);
+    }
+
+    private void setBodyFont(Typeface bodyFont) {
+        // Update font of app names.
+        for (int id : mShapeIconAppNameIds) {
+            TextView appName = mContentView.findViewById(id);
+            appName.setTypeface(bodyFont);
+        }
+    }
+
+    private void setTopBarIcons(List<Drawable> icons) {
+        ViewGroup iconsContainer = mContentView.findViewById(R.id.theme_preview_top_bar_icons);
+        for (int i = 0; i < iconsContainer.getChildCount(); i++) {
+            int iconIndex = sTopBarIconToPreviewIcon[i];
+            if (iconIndex < icons.size()) {
+                ((ImageView) iconsContainer.getChildAt(i))
+                        .setImageDrawable(icons.get(iconIndex).getConstantState()
+                                .newDrawable().mutate());
+            } else {
+                iconsContainer.getChildAt(i).setVisibility(View.GONE);
+            }
+        }
+    }
+
+    private void setAppIconShape(List<ShapeAppIcon> appIcons) {
+        for (int i = 0; i < mShapeAppIconIds.length && i < mShapeIconAppNameIds.length
+                && i < appIcons.size(); i++) {
+            ShapeAppIcon icon = appIcons.get(i);
+            // Set app icon.
+            ImageView iconView = mContentView.findViewById(mShapeAppIconIds[i]);
+            iconView.setBackground(icon.getDrawableCopy());
+            // Set app name.
+            TextView appName = mContentView.findViewById(mShapeIconAppNameIds[i]);
+            appName.setText(icon.getAppName());
+        }
+    }
+
+    private void setColorAndIconsSection(List<Drawable> icons, Drawable shapeDrawable,
+                                         int accentColor) {
+        // Set QS icons and background.
+        for (int i = 0; i < mColorTileIconIds.length && i < icons.size(); i++) {
+            Drawable icon = icons.get(mColorTileIconIds[i][1]).getConstantState()
+                    .newDrawable().mutate();
+            Drawable bgShape = shapeDrawable.getConstantState().newDrawable();
+            bgShape.setTint(accentColor);
+
+            ImageView bg = mContentView.findViewById(mColorTileIds[i]);
+            bg.setImageDrawable(bgShape);
+            ImageView fg = mContentView.findViewById(mColorTileIconIds[i][0]);
+            fg.setImageDrawable(icon);
+        }
+
+        // Set color for Buttons (CheckBox, RadioButton, and Switch).
+        ColorStateList tintList = getColorStateList(accentColor);
+        for (int mColorButtonId : mColorButtonIds) {
+            CompoundButton button = mContentView.findViewById(mColorButtonId);
+            button.setButtonTintList(tintList);
+            if (button instanceof Switch) {
+                ((Switch) button).setThumbTintList(tintList);
+                ((Switch) button).setTrackTintList(tintList);
+            }
+        }
+    }
+
+    private void setColorAndIconsBoxRadius(int cornerRadius) {
+        ((CardView) mContentView.findViewById(R.id.color_icons_section)).setRadius(cornerRadius);
+    }
+
+    private void setQsbRadius(int cornerRadius) {
+        View qsb = mContentView.findViewById(R.id.theme_qsb);
+        if (qsb != null && qsb.getVisibility() == View.VISIBLE) {
+            if (qsb.getBackground() instanceof GradientDrawable) {
+                GradientDrawable bg = (GradientDrawable) qsb.getBackground();
+                float radius = useRoundedQSB(cornerRadius)
+                        ? (float) qsb.getLayoutParams().height / 2 : cornerRadius;
+                bg.setCornerRadii(new float[]{
+                        radius, radius, radius, radius,
+                        radius, radius, radius, radius});
+            }
+        }
+    }
+
+    private void updateTime() {
+        Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
+        if (mStatusBarClock != null) {
+            mStatusBarClock.setText(TimeUtils.getFormattedTime(mContext, calendar));
+        }
+        if (mSmartSpaceDate != null) {
+            String datePattern =
+                    DateFormat.getBestDateTimePattern(Locale.getDefault(), DATE_FORMAT);
+            mSmartSpaceDate.setText(DateFormat.format(datePattern, calendar));
+        }
+    }
+
+    private boolean useRoundedQSB(int cornerRadius) {
+        return cornerRadius >= mContext.getResources().getDimensionPixelSize(
+                R.dimen.roundCornerThreshold);
+    }
+
+    private ColorStateList getColorStateList(int accentColor) {
+        int controlGreyColor =
+                ResourceUtils.getColorAttr(mContext, android.R.attr.textColorTertiary);
+        return new ColorStateList(
+                new int[][]{
+                        new int[]{android.R.attr.state_selected},
+                        new int[]{android.R.attr.state_checked},
+                        new int[]{-android.R.attr.state_enabled},
+                },
+                new int[] {
+                        accentColor,
+                        accentColor,
+                        controlGreyColor
+                }
+        );
+    }
+
+    /**
+     * Gets the screen height which does not include the system status bar and bottom navigation
+     * bar.
+     */
+    private int getDisplayHeight() {
+        final DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
+        return dm.heightPixels;
+    }
+
+    // The height of top tool bar (R.layout.section_header).
+    private int getTopToolBarHeight() {
+        final TypedValue typedValue = new TypedValue();
+        return mContext.getTheme().resolveAttribute(
+                android.R.attr.actionBarSize, typedValue, true)
+                ? TypedValue.complexToDimensionPixelSize(
+                        typedValue.data, mContext.getResources().getDisplayMetrics())
+                : 0;
+    }
+
+    private int getFullPreviewCardHeight() {
+        final Resources res = mContext.getResources();
+        return getDisplayHeight()
+                - getTopToolBarHeight()
+                - res.getDimensionPixelSize(R.dimen.bottom_actions_height)
+                - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_top)
+                - res.getDimensionPixelSize(R.dimen.full_preview_page_default_padding_bottom);
+    }
+}
diff --git a/src/com/android/customization/widget/OptionSelectorController.java b/src/com/android/customization/widget/OptionSelectorController.java
new file mode 100644
index 0000000..6eb052c
--- /dev/null
+++ b/src/com/android/customization/widget/OptionSelectorController.java
@@ -0,0 +1,470 @@
+/*
+ * 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.customization.widget;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.TextView;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
+
+import com.android.customization.model.CustomizationManager;
+import com.android.customization.model.CustomizationOption;
+import com.android.wallpaper.R;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Simple controller for a RecyclerView-based widget to hold the options for each customization
+ * section (eg, thumbnails for themes, clocks, grid sizes).
+ * To use, just pass the RV that will contain the tiles and the list of {@link CustomizationOption}
+ * representing each option, and call {@link #initOptions(CustomizationManager)} to populate the
+ * widget.
+ */
+public class OptionSelectorController<T extends CustomizationOption<T>> {
+
+    /**
+     * Interface to be notified when an option is selected by the user.
+     */
+    public interface OptionSelectedListener {
+
+        /**
+         * Called when an option has been selected (and marked as such in the UI)
+         */
+        void onOptionSelected(CustomizationOption selected);
+    }
+
+    @IntDef({CheckmarkStyle.NONE, CheckmarkStyle.CORNER, CheckmarkStyle.CENTER,
+            CheckmarkStyle.CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED})
+    public @interface CheckmarkStyle {
+        int NONE = 0;
+        int CORNER = 1;
+        int CENTER = 2;
+        int CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED = 3;
+    }
+
+    private final float mLinearLayoutHorizontalDisplayOptionsMax;
+
+    private final RecyclerView mContainer;
+    private final List<T> mOptions;
+    private final boolean mUseGrid;
+    @CheckmarkStyle
+    private final int mCheckmarkStyle;
+
+    private final Set<OptionSelectedListener> mListeners = new HashSet<>();
+    private RecyclerView.Adapter<TileViewHolder> mAdapter;
+    private T mSelectedOption;
+    private T mAppliedOption;
+
+    public OptionSelectorController(RecyclerView container, List<T> options) {
+        this(container, options, true, CheckmarkStyle.CORNER);
+    }
+
+    public OptionSelectorController(RecyclerView container, List<T> options,
+            boolean useGrid, @CheckmarkStyle int checkmarkStyle) {
+        mContainer = container;
+        mOptions = options;
+        mUseGrid = useGrid;
+        mCheckmarkStyle = checkmarkStyle;
+        TypedValue typedValue = new TypedValue();
+        mContainer.getResources().getValue(R.dimen.linear_layout_horizontal_display_options_max,
+                typedValue, true);
+        mLinearLayoutHorizontalDisplayOptionsMax = typedValue.getFloat();
+    }
+
+    public void addListener(OptionSelectedListener listener) {
+        mListeners.add(listener);
+    }
+
+    public void removeListener(OptionSelectedListener listener) {
+        mListeners.remove(listener);
+    }
+
+    /**
+     * Mark the given option as selected
+     */
+    public void setSelectedOption(T option) {
+        if (!mOptions.contains(option)) {
+            throw new IllegalArgumentException("Invalid option");
+        }
+        T lastSelectedOption = mSelectedOption;
+        mSelectedOption = option;
+        mAdapter.notifyItemChanged(mOptions.indexOf(option));
+        if (lastSelectedOption != null) {
+            mAdapter.notifyItemChanged(mOptions.indexOf(lastSelectedOption));
+        }
+        notifyListeners();
+    }
+
+    /**
+     * @return whether this controller contains the given option
+     */
+    public boolean containsOption(T option) {
+        return mOptions.contains(option);
+    }
+
+    /**
+     * Mark an option as the one which is currently applied on the device. This will result in a
+     * check being displayed in the lower-right corner of the corresponding ViewHolder.
+     */
+    public void setAppliedOption(T option) {
+        if (!mOptions.contains(option)) {
+            throw new IllegalArgumentException("Invalid option");
+        }
+        T lastAppliedOption = mAppliedOption;
+        mAppliedOption = option;
+        mAdapter.notifyItemChanged(mOptions.indexOf(option));
+        if (lastAppliedOption != null) {
+            mAdapter.notifyItemChanged(mOptions.indexOf(lastAppliedOption));
+        }
+    }
+
+    /**
+     * Notify that a given option has changed.
+     *
+     * @param option the option that changed
+     */
+    public void optionChanged(T option) {
+        if (!mOptions.contains(option)) {
+            throw new IllegalArgumentException("Invalid option");
+        }
+        mAdapter.notifyItemChanged(mOptions.indexOf(option));
+    }
+
+    /**
+     * Initializes the UI for the options passed in the constructor of this class.
+     */
+    public void initOptions(final CustomizationManager<T> manager) {
+        mContainer.setAccessibilityDelegateCompat(
+                new OptionSelectorAccessibilityDelegate(mContainer));
+
+        mAdapter = new RecyclerView.Adapter<TileViewHolder>() {
+            @Override
+            public int getItemViewType(int position) {
+                return mOptions.get(position).getLayoutResId();
+            }
+
+            @NonNull
+            @Override
+            public TileViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+                View v = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
+                // Provide width constraint when a grid layout manager is not use and width is set
+                // to match parent
+                if (!mUseGrid
+                        && v.getLayoutParams().width == RecyclerView.LayoutParams.MATCH_PARENT) {
+                    Resources res = mContainer.getContext().getResources();
+                    RecyclerView.LayoutParams layoutParams = new RecyclerView.LayoutParams(
+                            res.getDimensionPixelSize(R.dimen.option_tile_width),
+                            RecyclerView.LayoutParams.WRAP_CONTENT);
+                    v.setLayoutParams(layoutParams);
+                }
+                return new TileViewHolder(v);
+            }
+
+            @Override
+            public void onBindViewHolder(@NonNull TileViewHolder holder, int position) {
+                T option = mOptions.get(position);
+                if (option.isActive(manager)) {
+                    mAppliedOption = option;
+                    if (mSelectedOption == null) {
+                        mSelectedOption = option;
+                    }
+                }
+                if (holder.labelView != null) {
+                    holder.labelView.setText(option.getTitle());
+                    holder.labelView.setSelected(true);
+                }
+                holder.itemView.setActivated(option.equals(mSelectedOption));
+                option.bindThumbnailTile(holder.tileView);
+                holder.itemView.setOnClickListener(view -> setSelectedOption(option));
+
+                Resources res = mContainer.getContext().getResources();
+                if (mCheckmarkStyle == CheckmarkStyle.CORNER && option.equals(mAppliedOption)) {
+                    drawCheckmark(option, holder,
+                            res.getDrawable(R.drawable.check_circle_accent_24dp,
+                                    mContainer.getContext().getTheme()),
+                            Gravity.BOTTOM | Gravity.RIGHT,
+                            res.getDimensionPixelSize(R.dimen.check_size),
+                            res.getDimensionPixelOffset(R.dimen.check_offset), true);
+                } else if (mCheckmarkStyle == CheckmarkStyle.CENTER
+                        && option.equals(mAppliedOption)) {
+                    drawCheckmark(option, holder,
+                            res.getDrawable(R.drawable.check_circle_grey_large,
+                                    mContainer.getContext().getTheme()),
+                            Gravity.CENTER, res.getDimensionPixelSize(R.dimen.center_check_size),
+                            0, true);
+                } else if (mCheckmarkStyle == CheckmarkStyle.CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED
+                        && option.equals(mAppliedOption)) {
+                    int drawableRes = option.equals(mSelectedOption)
+                            ? R.drawable.check_circle_grey_large
+                            : R.drawable.check_circle_grey_large_not_select;
+                    drawCheckmark(option, holder,
+                            res.getDrawable(drawableRes,
+                                    mContainer.getContext().getTheme()),
+                            Gravity.CENTER, res.getDimensionPixelSize(R.dimen.center_check_size),
+                            0, option.equals(mSelectedOption));
+                } else if (option.equals(mAppliedOption)) {
+                    // Initialize with "previewed" description if we don't show checkmark
+                    holder.setContentDescription(mContainer.getContext(), option,
+                            R.string.option_previewed_description);
+                } else if (mCheckmarkStyle != CheckmarkStyle.NONE) {
+                    if (mCheckmarkStyle == CheckmarkStyle.CENTER_CHANGE_COLOR_WHEN_NOT_SELECTED) {
+                        if (option.equals(mSelectedOption)) {
+                            holder.setContentDescription(mContainer.getContext(), option,
+                                    R.string.option_previewed_description);
+                        } else {
+                            holder.setContentDescription(mContainer.getContext(), option,
+                                    R.string.option_change_applied_previewed_description);
+                        }
+                    }
+
+                    holder.tileView.setForeground(null);
+                }
+            }
+
+            @Override
+            public int getItemCount() {
+                return mOptions.size();
+            }
+
+            private void drawCheckmark(CustomizationOption<?> option, TileViewHolder holder,
+                    Drawable checkmark, int gravity, @Dimension int checkSize,
+                    @Dimension int checkOffset, boolean currentlyPreviewed) {
+                Drawable frame = holder.tileView.getForeground();
+                Drawable[] layers = {frame, checkmark};
+                if (frame == null) {
+                    layers = new Drawable[]{checkmark};
+                }
+                LayerDrawable checkedFrame = new LayerDrawable(layers);
+
+                // Position according to the given gravity and offset
+                int idx = layers.length - 1;
+                checkedFrame.setLayerGravity(idx, gravity);
+                checkedFrame.setLayerWidth(idx, checkSize);
+                checkedFrame.setLayerHeight(idx, checkSize);
+                checkedFrame.setLayerInsetBottom(idx, checkOffset);
+                checkedFrame.setLayerInsetRight(idx, checkOffset);
+                holder.tileView.setForeground(checkedFrame);
+
+                // Initialize the currently applied option
+                if (currentlyPreviewed) {
+                    holder.setContentDescription(mContainer.getContext(), option,
+                            R.string.option_applied_previewed_description);
+                } else {
+                    holder.setContentDescription(mContainer.getContext(), option,
+                            R.string.option_applied_description);
+                }
+            }
+        };
+
+        Resources res = mContainer.getContext().getResources();
+        mContainer.setAdapter(mAdapter);
+        final DisplayMetrics metrics = new DisplayMetrics();
+        mContainer.getContext().getSystemService(WindowManager.class)
+                .getDefaultDisplay().getMetrics(metrics);
+        final boolean hasDecoration = mContainer.getItemDecorationCount() != 0;
+
+        if (mUseGrid) {
+            int numColumns = res.getInteger(R.integer.options_grid_num_columns);
+            GridLayoutManager gridLayoutManager = new GridLayoutManager(mContainer.getContext(),
+                    numColumns);
+            mContainer.setLayoutManager(gridLayoutManager);
+        } else {
+            final int padding = res.getDimensionPixelSize(
+                    R.dimen.option_tile_linear_padding_horizontal);
+            final int widthPerItem = res.getDimensionPixelSize(R.dimen.option_tile_width) + (
+                    hasDecoration ? 0 : 2 * padding);
+            mContainer.setLayoutManager(new LinearLayoutManager(mContainer.getContext(),
+                    LinearLayoutManager.HORIZONTAL, false));
+            mContainer.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+            int availableWidth = metrics.widthPixels;
+            int extraSpace = availableWidth - mContainer.getMeasuredWidth();
+            if (extraSpace >= 0) {
+                mContainer.setOverScrollMode(View.OVER_SCROLL_NEVER);
+            }
+
+            if (mAdapter.getItemCount() >= mLinearLayoutHorizontalDisplayOptionsMax) {
+                int spaceBetweenItems = availableWidth
+                        - Math.round(widthPerItem * mLinearLayoutHorizontalDisplayOptionsMax)
+                        - mContainer.getPaddingLeft();
+                int itemEndMargin =
+                        spaceBetweenItems / (int) mLinearLayoutHorizontalDisplayOptionsMax;
+                itemEndMargin = Math.max(itemEndMargin, res.getDimensionPixelOffset(
+                        R.dimen.option_tile_margin_horizontal));
+                mContainer.addItemDecoration(new ItemEndHorizontalSpaceItemDecoration(
+                        mContainer.getContext(), itemEndMargin));
+                return;
+            }
+
+            int spaceBetweenItems = extraSpace / (mAdapter.getItemCount() + 1);
+            int itemSideMargin = spaceBetweenItems / 2;
+            mContainer.addItemDecoration(new HorizontalSpacerItemDecoration(itemSideMargin));
+        }
+    }
+
+    public void resetOptions(List<T> options) {
+        mOptions.clear();
+        mOptions.addAll(options);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private void notifyListeners() {
+        if (mListeners.isEmpty()) {
+            return;
+        }
+        T option = mSelectedOption;
+        Set<OptionSelectedListener> iterableListeners = new HashSet<>(mListeners);
+        for (OptionSelectedListener listener : iterableListeners) {
+            listener.onOptionSelected(option);
+        }
+    }
+
+    private static class TileViewHolder extends RecyclerView.ViewHolder {
+        TextView labelView;
+        View tileView;
+        CharSequence title;
+
+        TileViewHolder(@NonNull View itemView) {
+            super(itemView);
+            labelView = itemView.findViewById(R.id.option_label);
+            tileView = itemView.findViewById(R.id.option_tile);
+            title = null;
+        }
+
+        /**
+         * Set the content description for this holder using the given string id.
+         * If the option does not have a label, the description will be set on the tile view.
+         *
+         * @param context The view's context
+         * @param option  The customization option
+         * @param id      Resource ID of the string to use for the content description
+         */
+        public void setContentDescription(Context context, CustomizationOption<?> option, int id) {
+            title = option.getTitle();
+            if (TextUtils.isEmpty(title) && tileView != null) {
+                title = tileView.getContentDescription();
+            }
+
+            CharSequence cd = context.getString(id, title);
+            if (labelView != null && !TextUtils.isEmpty(labelView.getText())) {
+                labelView.setAccessibilityPaneTitle(cd);
+                labelView.setContentDescription(cd);
+            } else if (tileView != null) {
+                tileView.setAccessibilityPaneTitle(cd);
+                tileView.setContentDescription(cd);
+            }
+        }
+
+        public void resetContentDescription() {
+            if (labelView != null && !TextUtils.isEmpty(labelView.getText())) {
+                labelView.setAccessibilityPaneTitle(title);
+                labelView.setContentDescription(title);
+            } else if (tileView != null) {
+                tileView.setAccessibilityPaneTitle(title);
+                tileView.setContentDescription(title);
+            }
+        }
+    }
+
+    private class OptionSelectorAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+
+        OptionSelectorAccessibilityDelegate(RecyclerView recyclerView) {
+            super(recyclerView);
+        }
+
+        @Override
+        public boolean onRequestSendAccessibilityEvent(
+                ViewGroup host, View child, AccessibilityEvent event) {
+
+            // Apply this workaround to horizontal recyclerview only,
+            // since the symptom is TalkBack will lose focus when navigating horizontal list items.
+            if (mContainer.getLayoutManager() != null
+                    && mContainer.getLayoutManager().canScrollHorizontally()
+                    && event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+                int itemPos = mContainer.getChildLayoutPosition(child);
+                int itemWidth = mContainer.getContext().getResources()
+                        .getDimensionPixelOffset(R.dimen.option_tile_width);
+                int itemMarginHorizontal = mContainer.getContext().getResources()
+                        .getDimensionPixelOffset(R.dimen.option_tile_margin_horizontal) * 2;
+                int scrollOffset = itemWidth + itemMarginHorizontal;
+
+                // Make focusing item's previous/next item totally visible when changing focus,
+                // ensure TalkBack won't lose focus when recyclerview scrolling.
+                if (itemPos >= ((LinearLayoutManager) mContainer.getLayoutManager())
+                        .findLastCompletelyVisibleItemPosition()) {
+                    mContainer.scrollBy(scrollOffset, 0);
+                } else if (itemPos <= ((LinearLayoutManager) mContainer.getLayoutManager())
+                        .findFirstCompletelyVisibleItemPosition() && itemPos != 0) {
+                    mContainer.scrollBy(-scrollOffset, 0);
+                }
+            }
+            return super.onRequestSendAccessibilityEvent(host, child, event);
+        }
+    }
+
+    /** Custom ItemDecorator to add specific spacing between items in the list. */
+    private static final class ItemEndHorizontalSpaceItemDecoration
+            extends RecyclerView.ItemDecoration {
+        private final int mHorizontalSpacePx;
+        private final boolean mDirectionLTR;
+
+        private ItemEndHorizontalSpaceItemDecoration(Context context, int horizontalSpacePx) {
+            mDirectionLTR = context.getResources().getConfiguration().getLayoutDirection()
+                    == View.LAYOUT_DIRECTION_LTR;
+            mHorizontalSpacePx = horizontalSpacePx;
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView recyclerView,
+                RecyclerView.State state) {
+            if (recyclerView.getAdapter() == null) {
+                return;
+            }
+
+            if (recyclerView.getChildAdapterPosition(view)
+                    != checkNotNull(recyclerView.getAdapter()).getItemCount() - 1) {
+                // Don't add spacing behind the last item
+                if (mDirectionLTR) {
+                    outRect.right = mHorizontalSpacePx;
+                } else {
+                    outRect.left = mHorizontalSpacePx;
+                }
+            }
+        }
+    }
+}
diff --git a/src/org/leafos/customization/module/LeafThemePickerInjector.java b/src/org/leafos/customization/module/LeafThemePickerInjector.java
index f8c509c..2e4d9e2 100644
--- a/src/org/leafos/customization/module/LeafThemePickerInjector.java
+++ b/src/org/leafos/customization/module/LeafThemePickerInjector.java
@@ -16,7 +16,11 @@
 package org.leafos.customization.module;
 
 import androidx.activity.ComponentActivity;
+import androidx.fragment.app.FragmentActivity;
 
+import com.android.customization.model.theme.OverlayManagerCompat;
+import com.android.customization.model.theme.ThemeBundleProvider;
+import com.android.customization.model.theme.ThemeManager;
 import com.android.customization.module.ThemePickerInjector;
 import com.android.wallpaper.dispatchers.BackgroundDispatcher;
 import com.android.wallpaper.dispatchers.MainDispatcher;
@@ -46,4 +50,12 @@
         }
         return mCustomizationSections;
     }
+
+    public ThemeManager getThemeManager(
+        ThemeBundleProvider provider,
+        FragmentActivity activity,
+        OverlayManagerCompat overlayManagerCompat,
+        ThemesUserEventLogger logger) {
+        return new ThemeManager(provider, activity, overlayManagerCompat, logger);
+    }
 }