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);
+ }
}