Clock floating sheet (2/2)
Test: Manually tested
Bug: 350718184
Flag: com.android.wallpaper.new_picker_ui_flag
Change-Id: I0b753d1cfb71c9cf26aafa237000be7131396fa9
diff --git a/res/drawable/horizontal_divider_16dp.xml b/res/drawable/horizontal_divider_16dp.xml
new file mode 100644
index 0000000..a1a17df
--- /dev/null
+++ b/res/drawable/horizontal_divider_16dp.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <size
+ android:width="16dp"
+ android:height="0dp" />
+</shape>
diff --git a/res/drawable/ic_open_in_full_24px.xml b/res/drawable/ic_open_in_full_24px.xml
new file mode 100644
index 0000000..4864792
--- /dev/null
+++ b/res/drawable/ic_open_in_full_24px.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ ~
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+ <path android:fillColor="@android:color/white" android:pathData="M120,840L120,520L200,520L200,704L704,200L520,200L520,120L840,120L840,440L760,440L760,256L256,760L440,760L440,840L120,840Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_palette_filled_24px.xml b/res/drawable/ic_palette_filled_24px.xml
new file mode 100644
index 0000000..941335f
--- /dev/null
+++ b/res/drawable/ic_palette_filled_24px.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ ~
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+ <path android:fillColor="@android:color/white" android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 112.5,324Q145,251 200.5,197Q256,143 330,111.5Q404,80 488,80Q568,80 639,107.5Q710,135 763.5,183.5Q817,232 848.5,298.5Q880,365 880,442Q880,557 810,618.5Q740,680 640,680L566,680Q557,680 553.5,685Q550,690 550,696Q550,708 565,730.5Q580,753 580,782Q580,832 552.5,856Q525,880 480,880ZM260,520Q286,520 303,503Q320,486 320,460Q320,434 303,417Q286,400 260,400Q234,400 217,417Q200,434 200,460Q200,486 217,503Q234,520 260,520ZM380,360Q406,360 423,343Q440,326 440,300Q440,274 423,257Q406,240 380,240Q354,240 337,257Q320,274 320,300Q320,326 337,343Q354,360 380,360ZM580,360Q606,360 623,343Q640,326 640,300Q640,274 623,257Q606,240 580,240Q554,240 537,257Q520,274 520,300Q520,326 537,343Q554,360 580,360ZM700,520Q726,520 743,503Q760,486 760,460Q760,434 743,417Q726,400 700,400Q674,400 657,417Q640,434 640,460Q640,486 657,503Q674,520 700,520Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_style_filled_24px.xml b/res/drawable/ic_style_filled_24px.xml
new file mode 100644
index 0000000..0b9ec32
--- /dev/null
+++ b/res/drawable/ic_style_filled_24px.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ ~
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+ <path android:fillColor="@android:color/white" android:pathData="M159,792L125,778Q94,765 83.5,733Q73,701 87,670L159,514L159,792ZM319,880Q286,880 262.5,856.5Q239,833 239,800L239,560L345,854Q348,861 351,867.5Q354,874 359,880L319,880ZM525,876Q493,888 463,873Q433,858 421,826L243,338Q231,306 245,275.5Q259,245 291,234L593,124Q625,112 655,127Q685,142 697,174L875,662Q887,694 873,724.5Q859,755 827,766L525,876ZM439,400Q456,400 467.5,388.5Q479,377 479,360Q479,343 467.5,331.5Q456,320 439,320Q422,320 410.5,331.5Q399,343 399,360Q399,377 410.5,388.5Q422,400 439,400Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/layout/clock_color_option_noop_item.xml b/res/layout/clock_color_option_noop_item.xml
new file mode 100644
index 0000000..a4ea877
--- /dev/null
+++ b/res/layout/clock_color_option_noop_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2023 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.
+-->
+<!-- Content description is set programmatically on the parent FrameLayout -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible"
+ android:orientation="vertical">
+ <include
+ layout="@layout/color_option"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="4dp"/>
+ <include
+ layout="@layout/color_option"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+</LinearLayout>
+
diff --git a/res/layout/floating_sheet_clock.xml b/res/layout/floating_sheet_clock.xml
index f917d9f..ff3a83a 100644
--- a/res/layout/floating_sheet_clock.xml
+++ b/res/layout/floating_sheet_clock.xml
@@ -14,13 +14,145 @@
~ limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="200dp"
- android:background="#00ff00">
- <TextView
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
+ android:background="@drawable/floating_sheet_content_background"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/clock_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+
+ <LinearLayout
+ android:id="@+id/clock_color_sheet_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/clock_color_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ <!--
+ This is an invisible placeholder put in place so that the parent keeps its height
+ stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows
+ the layout logic to keep the size of the preview container stable as well, which
+ bodes well for setting up the SurfaceView for remote rendering without changing its
+ size after the content is loaded into the RecyclerView.
+
+ It's critical for any TextViews inside the included layout to have text.
+ -->
+ <include
+ layout="@layout/clock_color_option_noop_item"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="invisible" />
+ </FrameLayout>
+
+
+ <SeekBar
+ android:id="@+id/clock_color_slider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:paddingHorizontal="@dimen/floating_sheet_content_horizontal_padding"
+ android:minHeight="@dimen/touch_target_min_height"
+ android:thumb="@null"
+ android:contentDescription="@string/accessibility_clock_slider_description"
+ android:background="@null"
+ android:progressDrawable="@drawable/saturation_progress_drawable"
+ android:splitTrack="false" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/clock_size_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:showDividers="middle"
+ android:baselineAligned="false"
+ android:divider="@drawable/horizontal_divider_16dp"
+ android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
+ android:paddingHorizontal="@dimen/floating_sheet_content_horizontal_padding">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+ <ImageView
+ android:layout_width="@dimen/floating_sheet_clock_size_icon_size"
+ android:layout_height="@dimen/floating_sheet_clock_size_icon_size"
+ android:background="#ff00ff"
+ android:layout_marginBottom="@dimen/floating_sheet_clock_size_icon_margin_bottom" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/SectionTitleTextStyle"
+ android:gravity="center"
+ android:text="@string/clock_size_dynamic"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/SectionSubtitleTextStyle"
+ android:gravity="center"
+ android:text="@string/clock_size_dynamic_description"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_horizontal">
+ <ImageView
+ android:layout_width="@dimen/floating_sheet_clock_size_icon_size"
+ android:layout_height="@dimen/floating_sheet_clock_size_icon_size"
+ android:background="#ff00ff"
+ android:layout_marginBottom="@dimen/floating_sheet_clock_size_icon_margin_bottom" />
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/SectionTitleTextStyle"
+ android:gravity="center"
+ android:text="@string/clock_size_small"/>
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/SectionSubtitleTextStyle"
+ android:gravity="center"
+ android:text="@string/clock_size_small_description"/>
+ </LinearLayout>
+ </LinearLayout>
+ </FrameLayout>
+
+ <com.android.wallpaper.picker.customization.ui.view.FloatingTabToolbar
+ android:id="@+id/floating_bar_tabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Clock customization bottom sheet"
- android:layout_gravity="center" />
-</FrameLayout>
\ No newline at end of file
+ android:layout_gravity="center_horizontal"
+ android:layout_marginVertical="@dimen/floating_sheet_tab_toolbar_vertical_margin" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/floating_sheet_colors.xml b/res/layout/floating_sheet_colors.xml
index 8e6ed64..d763054 100644
--- a/res/layout/floating_sheet_colors.xml
+++ b/res/layout/floating_sheet_colors.xml
@@ -18,15 +18,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding"
- android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
+ android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/floating_sheet_content_background"
- android:paddingVertical="@dimen/floating_sheet_list_vertical_padding"
+ android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
android:orientation="vertical">
<TextView
@@ -53,7 +51,7 @@
android:layout_height="wrap_content"
android:clickable="true"
android:gravity="center_vertical"
- android:layout_marginHorizontal="20dp"
+ android:layout_marginHorizontal="@dimen/floating_sheet_content_horizontal_padding"
android:orientation="horizontal">
<TextView
diff --git a/res/layout/floating_sheet_shortcut.xml b/res/layout/floating_sheet_shortcut.xml
index ce4665e..e752ba7 100644
--- a/res/layout/floating_sheet_shortcut.xml
+++ b/res/layout/floating_sheet_shortcut.xml
@@ -18,14 +18,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding"
- android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
+ android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingVertical="@dimen/floating_sheet_list_vertical_padding"
+ android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
android:background="@drawable/floating_sheet_content_background"
android:clipToPadding="false"
android:clipChildren="false">
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a643c0a..7d24e3f 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -176,7 +176,12 @@
<dimen name="notification_section_title_padding">8dp</dimen>
<!-- Floating sheet dimensions -->
- <dimen name="floating_sheet_list_vertical_padding">20dp</dimen>
+ <dimen name="floating_sheet_content_vertical_padding">20dp</dimen>
+ <dimen name="floating_sheet_content_horizontal_padding">20dp</dimen>
<dimen name="floating_sheet_horizontal_padding">16dp</dimen>
<dimen name="floating_sheet_tab_toolbar_vertical_margin">8dp</dimen>
+ <dimen name="floating_sheet_list_item_horizontal_space">4dp</dimen>
+ <dimen name="floating_sheet_list_item_vertical_space">4dp</dimen>
+ <dimen name="floating_sheet_clock_size_icon_size">80dp</dimen>
+ <dimen name="floating_sheet_clock_size_icon_margin_bottom">8dp</dimen>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 271a74c..aee2393 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -45,6 +45,9 @@
<!-- Description of a section of the customization picker where the user can configure clock color and size, e.g. Violet, small. [CHAR LIMIT=NONE] -->
<string name="clock_color_and_size_description"><xliff:g name="color">%1$s</xliff:g>, <xliff:g name="size">%2$s</xliff:g></string>
+ <!-- Title of a tab to change the clock style. [CHAR LIMIT=15] -->
+ <string name="clock_style">Style</string>
+
<!-- Title of a tab to change the clock color. [CHAR LIMIT=15] -->
<string name="clock_color">Color</string>
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt
index dea6777..e85f435 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt
@@ -257,7 +257,6 @@
const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58
const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21
val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
- const val USE_CELSIUS_PLACEHODLER = false
private fun getStatusBarHeight(resource: Resources): Int {
var result = 0
diff --git a/src/com/android/customization/picker/common/ui/view/DoubleRowListItemSpacing.kt b/src/com/android/customization/picker/common/ui/view/DoubleRowListItemSpacing.kt
index 746061c..dedbfe5 100644
--- a/src/com/android/customization/picker/common/ui/view/DoubleRowListItemSpacing.kt
+++ b/src/com/android/customization/picker/common/ui/view/DoubleRowListItemSpacing.kt
@@ -19,8 +19,12 @@
import android.view.View
import androidx.recyclerview.widget.RecyclerView
-/** Item spacing used by the RecyclerView with 2 rows. */
-class DoubleRowListItemSpacing(private val rowSpaceDp: Int) : RecyclerView.ItemDecoration() {
+/** Item spacing used by the horizontal RecyclerView with 2 rows. */
+class DoubleRowListItemSpacing(
+ private val edgeItemSpacePx: Int,
+ private val itemHorizontalSpacePx: Int,
+ private val itemVerticalSpacePx: Int,
+) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
@@ -36,42 +40,25 @@
val columnCount = (itemCount + 1) / 2
when {
columnCount == 1 -> {
- outRect.left = EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
- outRect.right = EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
+ outRect.left = edgeItemSpacePx
+ outRect.right = edgeItemSpacePx
}
columnIndex > 0 && columnIndex < columnCount - 1 -> {
- outRect.left = COMMON_HORIZONTAL_SPACING_DP.toPx(density)
- outRect.right = COMMON_HORIZONTAL_SPACING_DP.toPx(density)
+ outRect.left = itemHorizontalSpacePx
+ outRect.right = itemHorizontalSpacePx
}
columnIndex == 0 -> {
- outRect.left =
- if (!isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
- else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
- outRect.right =
- if (isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
- else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
+ outRect.left = if (!isRtl) edgeItemSpacePx else itemHorizontalSpacePx
+ outRect.right = if (isRtl) edgeItemSpacePx else itemHorizontalSpacePx
}
columnIndex == columnCount - 1 -> {
- outRect.right =
- if (!isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
- else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
- outRect.left =
- if (isRtl) EDGE_ITEM_HORIZONTAL_SPACING_DP.toPx(density)
- else COMMON_HORIZONTAL_SPACING_DP.toPx(density)
+ outRect.right = if (!isRtl) edgeItemSpacePx else itemHorizontalSpacePx
+ outRect.left = if (isRtl) edgeItemSpacePx else itemHorizontalSpacePx
}
}
if (itemIndex % 2 == 0) {
- outRect.bottom = rowSpaceDp.toPx(density)
+ outRect.bottom = itemVerticalSpacePx
}
}
-
- private fun Int.toPx(density: Float): Int {
- return (this * density).toInt()
- }
-
- companion object {
- const val EDGE_ITEM_HORIZONTAL_SPACING_DP = 20
- const val COMMON_HORIZONTAL_SPACING_DP = 4
- }
}
diff --git a/src/com/android/wallpaper/customization/ui/binder/ClockFloatingSheetBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ClockFloatingSheetBinder.kt
new file mode 100644
index 0000000..4cb9bb5
--- /dev/null
+++ b/src/com/android/wallpaper/customization/ui/binder/ClockFloatingSheetBinder.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2024 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.wallpaper.customization.ui.binder
+
+import android.annotation.DrawableRes
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.widget.SeekBar
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder
+import com.android.customization.picker.color.ui.view.ColorOptionIconView
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
+import com.android.customization.picker.common.ui.view.DoubleRowListItemSpacing
+import com.android.themepicker.R
+import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel
+import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab.COLOR
+import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab.SIZE
+import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab.STYLE
+import com.android.wallpaper.picker.customization.ui.view.FloatingTabToolbar
+import com.android.wallpaper.picker.customization.ui.view.FloatingTabToolbar.Tab.PRIMARY
+import com.android.wallpaper.picker.customization.ui.view.FloatingTabToolbar.Tab.SECONDARY
+import com.android.wallpaper.picker.customization.ui.view.FloatingTabToolbar.Tab.TERTIARY
+import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
+import kotlinx.coroutines.launch
+
+object ClockFloatingSheetBinder {
+ private const val SLIDER_ENABLED_ALPHA = 1f
+ private const val SLIDER_DISABLED_ALPHA = .3f
+
+ fun bind(
+ view: View,
+ viewModel: ClockPickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ val appContext = view.context.applicationContext
+
+ val tabs =
+ view.requireViewById<FloatingTabToolbar>(R.id.floating_bar_tabs).apply {
+ showTertiaryTab(true)
+ setTabText(PRIMARY, appContext.getString(R.string.clock_style))
+ primaryIcon.setImageDrawable(
+ getDrawable(appContext, R.drawable.ic_style_filled_24px)
+ )
+ setOnTabClick(PRIMARY) { viewModel.setTab(STYLE) }
+ setTabText(SECONDARY, appContext.getString(R.string.clock_color))
+ secondaryIcon.setImageDrawable(
+ getDrawable(appContext, R.drawable.ic_palette_filled_24px)
+ )
+ setOnTabClick(SECONDARY) { viewModel.setTab(COLOR) }
+ setTabText(TERTIARY, appContext.getString(R.string.clock_size))
+ tertiaryIcon.setImageDrawable(
+ getDrawable(appContext, R.drawable.ic_open_in_full_24px)
+ )
+ setOnTabClick(TERTIARY) { viewModel.setTab(SIZE) }
+ }
+
+ // Clock style
+ val clockList = view.requireViewById<View>(R.id.clock_list)
+
+ // Cloc color
+ val clockColorSheetContent = view.requireViewById<View>(R.id.clock_color_sheet_content)
+ val clockColorAdapter =
+ createOptionItemAdapter(
+ view.resources.configuration.uiMode,
+ lifecycleOwner,
+ )
+ val clockColorList =
+ view.requireViewById<RecyclerView>(R.id.clock_color_list).also {
+ it.initColorsList(appContext, clockColorAdapter)
+ }
+ val clockColorSlider: SeekBar = view.requireViewById(R.id.clock_color_slider)
+ clockColorSlider.setOnSeekBarChangeListener(
+ object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ viewModel.onSliderProgressChanged(progress)
+ }
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
+
+ override fun onStopTrackingTouch(seekBar: SeekBar?) {
+ seekBar?.progress?.let {
+ lifecycleOwner.lifecycleScope.launch { viewModel.onSliderProgressStop(it) }
+ }
+ }
+ }
+ )
+
+ // Clock size
+ val clockSizeContainer = view.requireViewById<View>(R.id.clock_size_container)
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.selectedTab.collect {
+ when (it) {
+ STYLE -> tabs.setTabSelected(PRIMARY)
+ COLOR -> tabs.setTabSelected(SECONDARY)
+ SIZE -> tabs.setTabSelected(TERTIARY)
+ }
+
+ clockList.isVisible = it == STYLE
+ clockColorSheetContent.isVisible = it == COLOR
+ clockSizeContainer.isVisible = it == SIZE
+ }
+ }
+
+ launch {
+ viewModel.clockColorOptions.collect { colorOptions ->
+ clockColorAdapter.setItems(colorOptions) {
+ var indexToFocus = colorOptions.indexOfFirst { it.isSelected.value }
+ indexToFocus = if (indexToFocus < 0) 0 else indexToFocus
+ (clockColorList.layoutManager as LinearLayoutManager)
+ .scrollToPositionWithOffset(indexToFocus, 0)
+ }
+ }
+ }
+
+ launch {
+ viewModel.sliderProgress.collect { progress ->
+ clockColorSlider.setProgress(progress, true)
+ }
+ }
+
+ launch {
+ viewModel.isSliderEnabled.collect { isEnabled ->
+ clockColorSlider.isEnabled = isEnabled
+ clockColorSlider.alpha =
+ if (isEnabled) SLIDER_ENABLED_ALPHA else SLIDER_DISABLED_ALPHA
+ }
+ }
+ }
+ }
+ }
+
+ private fun createOptionItemAdapter(
+ uiMode: Int,
+ lifecycleOwner: LifecycleOwner
+ ): OptionItemAdapter<ColorOptionIconViewModel> =
+ OptionItemAdapter(
+ layoutResourceId = R.layout.color_option,
+ lifecycleOwner = lifecycleOwner,
+ bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel ->
+ val colorOptionIconView = foregroundView as? ColorOptionIconView
+ val night =
+ uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+ colorOptionIconView?.let { ColorOptionIconBinder.bind(it, colorIcon, night) }
+ }
+ )
+
+ private fun RecyclerView.initColorsList(
+ context: Context,
+ adapter: OptionItemAdapter<ColorOptionIconViewModel>,
+ ) {
+ apply {
+ this.adapter = adapter
+ layoutManager =
+ GridLayoutManager(
+ context,
+ 2,
+ GridLayoutManager.HORIZONTAL,
+ false,
+ )
+ addItemDecoration(
+ DoubleRowListItemSpacing(
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_content_horizontal_padding
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_horizontal_space
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_vertical_space
+ ),
+ )
+ )
+ }
+ }
+
+ private fun getDrawable(context: Context, @DrawableRes res: Int): Drawable? {
+ return ResourcesCompat.getDrawable(
+ context.resources,
+ res,
+ null,
+ )
+ }
+}
diff --git a/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
index 82f5291..6c6a587 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
@@ -124,7 +124,19 @@
GridLayoutManager.HORIZONTAL,
false,
)
- addItemDecoration(DoubleRowListItemSpacing(4))
+ addItemDecoration(
+ DoubleRowListItemSpacing(
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_content_horizontal_padding
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_horizontal_space
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_vertical_space
+ ),
+ )
+ )
}
}
diff --git a/src/com/android/wallpaper/customization/ui/binder/ShortcutFloatingSheetBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ShortcutFloatingSheetBinder.kt
index 6fc1b7f..e1c1d0b 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ShortcutFloatingSheetBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ShortcutFloatingSheetBinder.kt
@@ -184,7 +184,19 @@
GridLayoutManager.HORIZONTAL,
false,
)
- addItemDecoration(DoubleRowListItemSpacing(12))
+ addItemDecoration(
+ DoubleRowListItemSpacing(
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_content_horizontal_padding
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_horizontal_space
+ ),
+ context.resources.getDimensionPixelSize(
+ R.dimen.floating_sheet_list_item_vertical_space
+ ),
+ )
+ )
}
}
diff --git a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
index 250c3d1..3fda451 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
@@ -30,10 +30,8 @@
import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModel
import javax.inject.Inject
import javax.inject.Singleton
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
-@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class ThemePickerCustomizationOptionsBinder
@Inject
@@ -44,6 +42,7 @@
view: View,
lockScreenCustomizationOptionEntries: List<Pair<CustomizationOption, View>>,
homeScreenCustomizationOptionEntries: List<Pair<CustomizationOption, View>>,
+ customizationOptionFloatingSheetViewMap: Map<CustomizationOption, View>?,
viewModel: CustomizationOptionsViewModel,
lifecycleOwner: LifecycleOwner
) {
@@ -51,6 +50,7 @@
view,
lockScreenCustomizationOptionEntries,
homeScreenCustomizationOptionEntries,
+ customizationOptionFloatingSheetViewMap,
viewModel,
lifecycleOwner
)
@@ -91,16 +91,34 @@
}
}
- ShortcutFloatingSheetBinder.bind(
- view,
- viewModel.keyguardQuickAffordancePickerViewModel2,
- lifecycleOwner,
- )
+ customizationOptionFloatingSheetViewMap
+ ?.get(ThemePickerLockCustomizationOption.CLOCK)
+ ?.let {
+ ClockFloatingSheetBinder.bind(
+ it,
+ viewModel.clockPickerViewModel,
+ lifecycleOwner,
+ )
+ }
- ColorsFloatingSheetBinder.bind(
- view,
- viewModel.colorPickerViewModel2,
- lifecycleOwner,
- )
+ customizationOptionFloatingSheetViewMap
+ ?.get(ThemePickerLockCustomizationOption.SHORTCUTS)
+ ?.let {
+ ShortcutFloatingSheetBinder.bind(
+ it,
+ viewModel.keyguardQuickAffordancePickerViewModel2,
+ lifecycleOwner,
+ )
+ }
+
+ customizationOptionFloatingSheetViewMap
+ ?.get(ThemePickerHomeCustomizationOption.COLORS)
+ ?.let {
+ ColorsFloatingSheetBinder.bind(
+ it,
+ viewModel.colorPickerViewModel2,
+ lifecycleOwner,
+ )
+ }
}
}
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ClockOptionItemViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ClockOptionItemViewModel.kt
new file mode 100644
index 0000000..dd8f5b2
--- /dev/null
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ClockOptionItemViewModel.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.wallpaper.customization.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class ClockOptionItemViewModel(
+ val clockId: String,
+ val isSelected: Boolean,
+ val contentDescription: String,
+ val thumbnail: Drawable? = null,
+)
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ClockPickerViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ClockPickerViewModel.kt
new file mode 100644
index 0000000..3da3252
--- /dev/null
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ClockPickerViewModel.kt
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2024 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.wallpaper.customization.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Resources
+import androidx.core.graphics.ColorUtils
+import com.android.customization.model.color.ColorOptionImpl
+import com.android.customization.module.logging.ThemesUserEventLogger
+import com.android.customization.module.logging.ThemesUserEventLogger.Companion.NULL_SEED_COLOR
+import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import com.android.customization.picker.clock.shared.ClockSize
+import com.android.customization.picker.clock.shared.model.ClockMetadataModel
+import com.android.customization.picker.clock.shared.toClockSizeForLogging
+import com.android.customization.picker.clock.ui.viewmodel.ClockColorViewModel
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
+import com.android.themepicker.R
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.scopes.ViewModelScoped
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** View model for the clock customization screen. */
+class ClockPickerViewModel
+@AssistedInject
+constructor(
+ @ApplicationContext context: Context,
+ resources: Resources,
+ private val clockPickerInteractor: ClockPickerInteractor,
+ colorPickerInteractor: ColorPickerInteractor,
+ private val logger: ThemesUserEventLogger,
+ @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher,
+ @Assisted private val viewModelScope: CoroutineScope,
+) {
+
+ enum class Tab {
+ STYLE,
+ COLOR,
+ SIZE,
+ }
+
+ private val colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
+
+ private val _selectedTab = MutableStateFlow(Tab.STYLE)
+ val selectedTab: StateFlow<Tab> = _selectedTab.asStateFlow()
+
+ fun setTab(tab: Tab) {
+ _selectedTab.value = tab
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val allClocks: StateFlow<List<ClockOptionItemViewModel>> =
+ clockPickerInteractor.allClocks
+ .mapLatest { allClocks ->
+ // Delay to avoid the case that the full list of clocks is not initiated.
+ delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+ allClocks.map {
+ val contentDescription =
+ resources.getString(
+ R.string.select_clock_action_description,
+ // TODO (b/350718184): Get ClockConfig.description from ClockRegistry
+ "description"
+ )
+ ClockOptionItemViewModel(
+ clockId = it.clockId,
+ isSelected = it.isSelected,
+ contentDescription = contentDescription,
+ )
+ }
+ }
+ // makes sure that the operations above this statement are executed on I/O dispatcher
+ // while parallelism limits the number of threads this can run on which makes sure that
+ // the flows run sequentially
+ .flowOn(backgroundDispatcher.limitedParallelism(1))
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ val selectedClockId: StateFlow<String?> =
+ clockPickerInteractor.selectedClockId
+ .distinctUntilChanged()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ private var setSelectedClockJob: Job? = null
+
+ fun setSelectedClock(clockId: String) {
+ setSelectedClockJob?.cancel()
+ setSelectedClockJob =
+ viewModelScope.launch(backgroundDispatcher) {
+ clockPickerInteractor.setSelectedClock(clockId)
+ logger.logClockApplied(clockId)
+ }
+ }
+
+ private val selectedColorId: StateFlow<String?> =
+ clockPickerInteractor.selectedColorId.stateIn(viewModelScope, SharingStarted.Eagerly, null)
+
+ private val sliderColorToneProgress =
+ MutableStateFlow(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
+ val isSliderEnabled: Flow<Boolean> =
+ combine(selectedClockId, clockPickerInteractor.selectedColorId) { clockId, colorId ->
+ if (colorId == null || clockId == null) {
+ false
+ } else {
+ // TODO (b/350718184): Get ClockConfig.isReactiveToTone from ClockRegistry
+ false
+ }
+ }
+ .distinctUntilChanged()
+ val sliderProgress: Flow<Int> =
+ merge(clockPickerInteractor.colorToneProgress, sliderColorToneProgress)
+
+ private val _seedColor: MutableStateFlow<Int?> = MutableStateFlow(null)
+ val seedColor: Flow<Int?> = merge(clockPickerInteractor.seedColor, _seedColor)
+
+ /**
+ * The slider color tone updates are quick. Do not set color tone and the blended color to the
+ * settings until [onSliderProgressStop] is called. Update to a locally cached temporary
+ * [sliderColorToneProgress] and [_seedColor] instead.
+ */
+ fun onSliderProgressChanged(progress: Int) {
+ sliderColorToneProgress.value = progress
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ _seedColor.value =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ }
+
+ suspend fun onSliderProgressStop(progress: Int) {
+ val selectedColorId = selectedColorId.value ?: return
+ val clockColorViewModel = colorMap[selectedColorId] ?: return
+ val seedColor =
+ blendColorWithTone(
+ color = clockColorViewModel.color,
+ colorTone = clockColorViewModel.getColorTone(progress),
+ )
+ clockPickerInteractor.setClockColor(
+ selectedColorId = selectedColorId,
+ colorToneProgress = progress,
+ seedColor = seedColor,
+ )
+ logger.logClockColorApplied(seedColor)
+ }
+
+ val clockColorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
+ colorPickerInteractor.colorOptions.map { colorOptions ->
+ // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
+ // events from ClockRegistry upstream, caused by sliding the saturation level bar.
+ delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+ buildList {
+ val defaultThemeColorOptionViewModel =
+ (colorOptions[ColorType.WALLPAPER_COLOR]?.find { it.isSelected })
+ ?.toOptionItemViewModel(context)
+ ?: (colorOptions[ColorType.PRESET_COLOR]?.find { it.isSelected })
+ ?.toOptionItemViewModel(context)
+ if (defaultThemeColorOptionViewModel != null) {
+ add(defaultThemeColorOptionViewModel)
+ }
+
+ colorMap.values.forEachIndexed { index, colorModel ->
+ val isSelectedFlow =
+ selectedColorId
+ .map { colorMap.keys.indexOf(it) == index }
+ .stateIn(viewModelScope)
+ val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+ add(
+ OptionItemViewModel<ColorOptionIconViewModel>(
+ key = MutableStateFlow(colorModel.colorId) as StateFlow<String>,
+ payload =
+ ColorOptionIconViewModel(
+ lightThemeColor0 = colorModel.color,
+ lightThemeColor1 = colorModel.color,
+ lightThemeColor2 = colorModel.color,
+ lightThemeColor3 = colorModel.color,
+ darkThemeColor0 = colorModel.color,
+ darkThemeColor1 = colorModel.color,
+ darkThemeColor2 = colorModel.color,
+ darkThemeColor3 = colorModel.color,
+ ),
+ text =
+ Text.Loaded(
+ context.getString(
+ R.string.content_description_color_option,
+ index,
+ )
+ ),
+ isTextUserVisible = false,
+ isSelected = isSelectedFlow,
+ onClicked =
+ isSelectedFlow.map { isSelected ->
+ if (isSelected) {
+ null
+ } else {
+ {
+ viewModelScope.launch {
+ val seedColor =
+ blendColorWithTone(
+ color = colorModel.color,
+ colorTone =
+ colorModel.getColorTone(
+ colorToneProgress,
+ ),
+ )
+ clockPickerInteractor.setClockColor(
+ selectedColorId = colorModel.colorId,
+ colorToneProgress = colorToneProgress,
+ seedColor = seedColor,
+ )
+ logger.logClockColorApplied(seedColor)
+ }
+ }
+ }
+ },
+ )
+ )
+ }
+ }
+ }
+
+ private suspend fun ColorOptionModel.toOptionItemViewModel(
+ context: Context
+ ): OptionItemViewModel<ColorOptionIconViewModel> {
+ val lightThemeColors =
+ (colorOption as ColorOptionImpl)
+ .previewInfo
+ .resolveColors(
+ /** darkTheme= */
+ false
+ )
+ val darkThemeColors =
+ colorOption.previewInfo.resolveColors(
+ /** darkTheme= */
+ true
+ )
+ val isSelectedFlow = selectedColorId.map { it == null }.stateIn(viewModelScope)
+ return OptionItemViewModel<ColorOptionIconViewModel>(
+ key = MutableStateFlow(key) as StateFlow<String>,
+ payload =
+ ColorOptionIconViewModel(
+ lightThemeColor0 = lightThemeColors[0],
+ lightThemeColor1 = lightThemeColors[1],
+ lightThemeColor2 = lightThemeColors[2],
+ lightThemeColor3 = lightThemeColors[3],
+ darkThemeColor0 = darkThemeColors[0],
+ darkThemeColor1 = darkThemeColors[1],
+ darkThemeColor2 = darkThemeColors[2],
+ darkThemeColor3 = darkThemeColors[3],
+ ),
+ text = Text.Loaded(context.getString(R.string.default_theme_title)),
+ isTextUserVisible = true,
+ isSelected = isSelectedFlow,
+ onClicked =
+ isSelectedFlow.map { isSelected ->
+ if (isSelected) {
+ null
+ } else {
+ {
+ viewModelScope.launch {
+ clockPickerInteractor.setClockColor(
+ selectedColorId = null,
+ colorToneProgress =
+ ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+ seedColor = null,
+ )
+ logger.logClockColorApplied(NULL_SEED_COLOR)
+ }
+ }
+ }
+ },
+ )
+ }
+
+ val selectedClockSize: Flow<ClockSize> = clockPickerInteractor.selectedClockSize
+
+ fun setClockSize(size: ClockSize) {
+ viewModelScope.launch {
+ clockPickerInteractor.setClockSize(size)
+ logger.logClockSizeApplied(size.toClockSizeForLogging())
+ }
+ }
+
+ companion object {
+ private val helperColorLab: DoubleArray by lazy { DoubleArray(3) }
+
+ fun blendColorWithTone(color: Int, colorTone: Double): Int {
+ ColorUtils.colorToLAB(color, helperColorLab)
+ return ColorUtils.LABToColor(
+ colorTone,
+ helperColorLab[1],
+ helperColorLab[2],
+ )
+ }
+
+ const val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+ const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+ }
+
+ @ViewModelScoped
+ @AssistedFactory
+ interface Factory {
+ fun create(viewModelScope: CoroutineScope): ClockPickerViewModel
+ }
+}
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
index bc0366a..e50a090 100644
--- a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
@@ -34,12 +34,14 @@
defaultCustomizationOptionsViewModelFactory: DefaultCustomizationOptionsViewModel.Factory,
keyguardQuickAffordancePickerViewModel2Factory: KeyguardQuickAffordancePickerViewModel2.Factory,
colorPickerViewModel2Factory: ColorPickerViewModel2.Factory,
+ clockPickerViewModelFactory: ClockPickerViewModel.Factory,
@Assisted private val viewModelScope: CoroutineScope,
) : CustomizationOptionsViewModel {
private val defaultCustomizationOptionsViewModel =
defaultCustomizationOptionsViewModelFactory.create(viewModelScope)
+ val clockPickerViewModel = clockPickerViewModelFactory.create(viewModelScope = viewModelScope)
val keyguardQuickAffordancePickerViewModel2 =
keyguardQuickAffordancePickerViewModel2Factory.create(viewModelScope = viewModelScope)
val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope)
diff --git a/tests/module/src/com/android/wallpaper/ThemePickerTestModule.kt b/tests/module/src/com/android/wallpaper/ThemePickerTestModule.kt
index 4a484f3..8731f7f 100644
--- a/tests/module/src/com/android/wallpaper/ThemePickerTestModule.kt
+++ b/tests/module/src/com/android/wallpaper/ThemePickerTestModule.kt
@@ -23,12 +23,18 @@
import com.android.customization.module.CustomizationPreferences
import com.android.customization.module.logging.TestThemesUserEventLogger
import com.android.customization.module.logging.ThemesUserEventLogger
+import com.android.customization.picker.clock.data.repository.ClockPickerRepository
+import com.android.customization.picker.clock.data.repository.ClockPickerRepositoryImpl
+import com.android.customization.picker.clock.data.repository.ClockRegistryProvider
import com.android.customization.picker.color.data.repository.ColorPickerRepository
import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
import com.android.customization.testing.TestCustomizationInjector
import com.android.customization.testing.TestDefaultCustomizationPreferences
+import com.android.systemui.shared.clocks.ClockRegistry
import com.android.systemui.shared.customization.data.content.CustomizationProviderClient
import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
+import com.android.systemui.shared.settings.data.repository.SecureSettingsRepositoryImpl
import com.android.wallpaper.effects.EffectsController
import com.android.wallpaper.effects.FakeEffectsController
import com.android.wallpaper.module.Injector
@@ -42,6 +48,7 @@
import com.android.wallpaper.picker.customization.ui.binder.DefaultCustomizationOptionsBinder
import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
import com.android.wallpaper.picker.di.modules.EffectsModule
+import com.android.wallpaper.picker.di.modules.MainDispatcher
import com.android.wallpaper.picker.preview.ui.util.DefaultImageEffectDialogUtil
import com.android.wallpaper.picker.preview.ui.util.ImageEffectDialogUtil
import com.android.wallpaper.testing.FakeDefaultRequester
@@ -56,6 +63,7 @@
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
@Module
@TestInstallIn(
@@ -121,6 +129,10 @@
@Singleton
abstract fun bindColorPickerRepository(impl: ColorPickerRepositoryImpl): ColorPickerRepository
+ @Binds
+ @Singleton
+ abstract fun bindClockPickerRepository(impl: ClockPickerRepositoryImpl): ClockPickerRepository
+
companion object {
@Provides
@Singleton
@@ -139,5 +151,31 @@
): CustomizationProviderClient {
return CustomizationProviderClientImpl(context, bgDispatcher)
}
+
+ @Provides
+ @Singleton
+ fun provideSecureSettingsRepository(
+ @ApplicationContext context: Context,
+ @BackgroundDispatcher bgDispatcher: CoroutineDispatcher,
+ ): SecureSettingsRepository {
+ return SecureSettingsRepositoryImpl(context.contentResolver, bgDispatcher)
+ }
+
+ @Provides
+ @Singleton
+ fun provideClockRegistry(
+ @ApplicationContext context: Context,
+ @MainDispatcher mainScope: CoroutineScope,
+ @MainDispatcher mainDispatcher: CoroutineDispatcher,
+ @BackgroundDispatcher bgDispatcher: CoroutineDispatcher,
+ ): ClockRegistry {
+ return ClockRegistryProvider(
+ context = context,
+ coroutineScope = mainScope,
+ mainDispatcher = mainDispatcher,
+ backgroundDispatcher = bgDispatcher,
+ )
+ .get()
+ }
}
}