Merge "Update ClockChangeListener to match new interface type" into tm-qpr-dev
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f4d525e..0d446df 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -33,7 +33,7 @@
     <!-- The content description of clock entry. [CHAR LIMIT=NONE] -->
     <string name="clock_picker_entry_content_description">Change a custom clock</string>
 
-    <!-- Title of a section of the customization picker where the user can configure Clock face. [CHAR LIMIT=15] -->
+    <!-- Title of a section of the customization picker where the user can configure Clock face. [CHAR LIMIT=19] -->
     <string name="clock_settings_title">Clock Settings</string>
 
     <!-- Title of a tab to change the clock color. [CHAR LIMIT=15] -->
@@ -420,4 +420,22 @@
     [CHAR LIMIT=128].
     -->
     <string name="more_colors">More Colors</string>
+
+    <!--
+    Accessibility string for a button that allows the user to select the default color for their
+    lock screen clock on the device. This is shown next to other buttons that allow the user to
+    select from a set of custom colors.
+
+    [CHAR LIMIT=NONE].
+    -->
+    <string name="content_description_default_color_option">Default color option</string>
+
+    <!--
+    Accessibility string for a button that allows the user to select a custom color for their lock
+    screen clock on the device. This is shown next to other such buttons that allow the user to
+    select from a set of custom colors.
+
+    [CHAR LIMIT=NONE].
+    -->
+    <string name="content_description_color_option">Color option <xliff:g name="color_number" example="1">%1$d</xliff:g></string>
 </resources>
diff --git a/src/com/android/customization/module/CustomizationInjector.kt b/src/com/android/customization/module/CustomizationInjector.kt
index 900431e..e5c1424 100644
--- a/src/com/android/customization/module/CustomizationInjector.kt
+++ b/src/com/android/customization/module/CustomizationInjector.kt
@@ -25,6 +25,7 @@
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
@@ -77,4 +78,9 @@
         context: Context,
         registry: ClockRegistry,
     ): ClockViewFactory
+
+    fun getClockSettingsViewModelFactory(
+        context: Context,
+        registry: ClockRegistry,
+    ): ClockSettingsViewModel.Factory
 }
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 2924135..aed5a8a 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -38,6 +38,7 @@
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
 import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
@@ -96,6 +97,7 @@
     private var darkModeSnapshotRestorer: DarkModeSnapshotRestorer? = null
     private var themedIconSnapshotRestorer: ThemedIconSnapshotRestorer? = null
     private var themedIconInteractor: ThemedIconInteractor? = null
+    private var clockSettingsViewModelFactory: ClockSettingsViewModel.Factory? = null
 
     override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
         return customizationSections
@@ -409,6 +411,18 @@
                 .also { themedIconInteractor = it }
     }
 
+    override fun getClockSettingsViewModelFactory(
+        context: Context,
+        registry: ClockRegistry,
+    ): ClockSettingsViewModel.Factory {
+        return clockSettingsViewModelFactory
+            ?: ClockSettingsViewModel.Factory(
+                    context,
+                    getClockPickerInteractor(context, registry),
+                )
+                .also { clockSettingsViewModelFactory = it }
+    }
+
     companion object {
         @JvmStatic
         private val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER =
diff --git a/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt b/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt
new file mode 100644
index 0000000..06cf753
--- /dev/null
+++ b/src/com/android/customization/picker/HorizontalTouchMovementAwareNestedScrollView.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package com.android.customization.picker
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.ViewConfiguration
+import androidx.core.widget.NestedScrollView
+import kotlin.math.abs
+
+/**
+ * This nested scroll view will detect horizontal touch movements and stop vertical scrolls when a
+ * horizontal touch movement is detected.
+ */
+class HorizontalTouchMovementAwareNestedScrollView(context: Context, attrs: AttributeSet?) :
+    NestedScrollView(context, attrs) {
+
+    private var startXPosition = 0f
+    private var startYPosition = 0f
+    private var isHorizontalTouchMovement = false
+
+    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
+        when (event.action) {
+            MotionEvent.ACTION_DOWN -> {
+                startXPosition = event.x
+                startYPosition = event.y
+                isHorizontalTouchMovement = false
+            }
+            MotionEvent.ACTION_MOVE -> {
+                val xMoveDistance = abs(event.x - startXPosition)
+                val yMoveDistance = abs(event.y - startYPosition)
+                if (
+                    !isHorizontalTouchMovement &&
+                        xMoveDistance > yMoveDistance &&
+                        xMoveDistance > ViewConfiguration.get(context).scaledTouchSlop
+                ) {
+                    isHorizontalTouchMovement = true
+                }
+            }
+            else -> {}
+        }
+        return if (isHorizontalTouchMovement) {
+            // We only want to intercept the touch event when the touch moves more vertically than
+            // horizontally. So we return false.
+            false
+        } else {
+            super.onInterceptTouchEvent(event)
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
index 4e838aa..e785ebd 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
@@ -16,6 +16,7 @@
 package com.android.customization.picker.clock.ui.binder
 
 import android.view.View
+import android.widget.SeekBar
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
@@ -41,22 +42,11 @@
         lifecycleOwner: LifecycleOwner,
     ) {
         val tabView: RecyclerView = view.requireViewById(R.id.tabs)
-        val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container)
-        val sizeOptions =
-            view.requireViewById<ClockSizeRadioButtonGroup>(R.id.clock_size_radio_button_group)
-
         val tabAdapter = ClockSettingsTabAdapter()
         tabView.adapter = tabAdapter
         tabView.layoutManager = LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
         tabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
 
-        sizeOptions.onRadioButtonClickListener =
-            object : ClockSizeRadioButtonGroup.OnRadioButtonClickListener {
-                override fun onClick(size: ClockSize) {
-                    viewModel.setClockSize(size)
-                }
-            }
-
         val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options)
         val colorOptionAdapter = ColorOptionAdapter()
         colorOptionContainerView.adapter = colorOptionAdapter
@@ -64,6 +54,30 @@
             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
         colorOptionContainerView.addItemDecoration(ItemSpacing(ItemSpacing.ITEM_SPACING_DP))
 
+        val slider: SeekBar = view.requireViewById(R.id.slider)
+        slider.setOnSeekBarChangeListener(
+            object : SeekBar.OnSeekBarChangeListener {
+                override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) {
+                    if (fromUser) {
+                        viewModel.onSliderProgressChanged(progress)
+                    }
+                }
+
+                override fun onStartTrackingTouch(p0: SeekBar?) = Unit
+                override fun onStopTrackingTouch(p0: SeekBar?) = Unit
+            }
+        )
+
+        val sizeOptions =
+            view.requireViewById<ClockSizeRadioButtonGroup>(R.id.clock_size_radio_button_group)
+        sizeOptions.onRadioButtonClickListener =
+            object : ClockSizeRadioButtonGroup.OnRadioButtonClickListener {
+                override fun onClick(size: ClockSize) {
+                    viewModel.setClockSize(size)
+                }
+            }
+
+        val colorOptionContainer = view.requireViewById<View>(R.id.color_picker_container)
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch { viewModel.tabs.collect { tabAdapter.setItems(it) } }
@@ -103,6 +117,18 @@
                         }
                     }
                 }
+
+                launch {
+                    viewModel.sliderProgress.collect { progress ->
+                        progress?.let { slider.setProgress(progress, false) }
+                    }
+                }
+
+                launch {
+                    viewModel.isSliderEnabled.collect { isEnabled ->
+                        slider.isInvisible = !isEnabled
+                    }
+                }
             }
         }
     }
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
index d716102..ed14889 100644
--- a/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockSettingsFragment.kt
@@ -27,7 +27,6 @@
 import com.android.customization.picker.clock.ui.binder.ClockCarouselViewBinder
 import com.android.customization.picker.clock.ui.binder.ClockSettingsBinder
 import com.android.customization.picker.clock.ui.view.ClockCarouselView
-import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
 import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants
 import com.android.wallpaper.R
 import com.android.wallpaper.model.WallpaperColorsViewModel
@@ -131,10 +130,14 @@
                 .show()
             ClockSettingsBinder.bind(
                 view,
-                ClockSettingsViewModel(
-                    context,
-                    injector.getClockPickerInteractor(context, registry)
-                ),
+                ViewModelProvider(
+                        requireActivity(),
+                        injector.getClockSettingsViewModelFactory(
+                            context = context,
+                            registry = registry,
+                        ),
+                    )
+                    .get(),
                 this@ClockSettingsFragment,
             )
         }
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
index 11e0273..54aaec3 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -17,64 +17,174 @@
 
 import android.content.Context
 import android.graphics.Color
+import androidx.core.graphics.ColorUtils
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.shared.ClockSize
 import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
 import com.android.wallpaper.R
+import kotlin.math.abs
+import kotlin.math.roundToInt
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 
 /** View model for the clock settings screen. */
-class ClockSettingsViewModel(val context: Context, val interactor: ClockPickerInteractor) {
+class ClockSettingsViewModel
+private constructor(context: Context, private val interactor: ClockPickerInteractor) : ViewModel() {
 
     enum class Tab {
         COLOR,
         SIZE,
     }
 
-    val colorOptions: Flow<List<ColorOptionViewModel>> =
-        interactor.selectedClockColor.map { selectedColor ->
-            buildList {
-                // TODO (b/241966062) Change design of the placeholder for default theme color
-                add(
-                    ColorOptionViewModel(
-                        color0 = Color.TRANSPARENT,
-                        color1 = Color.TRANSPARENT,
-                        color2 = Color.TRANSPARENT,
-                        color3 = Color.TRANSPARENT,
-                        contentDescription = "description",
-                        isSelected = selectedColor == null,
-                        onClick =
-                            if (selectedColor == null) {
-                                null
-                            } else {
-                                { interactor.setClockColor(null) }
-                            },
-                    )
-                )
-                COLOR_LIST.forEach { color ->
+    private val helperColorHsl: FloatArray by lazy { FloatArray(3) }
+
+    /**
+     * Saturation level of the current selected color. Note that this can be null if the selected
+     * color is null, which means that the clock color respects the system theme color. In this
+     * case, the saturation level is no longer needed since we do not allow changing saturation
+     * level of the system theme color.
+     */
+    private val saturationLevel: Flow<Float?> =
+        interactor.selectedClockColor
+            .map { selectedColor ->
+                if (selectedColor == null) {
+                    null
+                } else {
+                    ColorUtils.colorToHSL(selectedColor, helperColorHsl)
+                    helperColorHsl[1]
+                }
+            }
+            .shareIn(
+                scope = viewModelScope,
+                started = SharingStarted.WhileSubscribed(),
+                replay = 1,
+            )
+
+    /**
+     * When the selected clock color is null, it means that the clock will respect the system theme
+     * color. And we no longer need the slider, which determines the saturation level of the clock's
+     * overridden color.
+     */
+    val isSliderEnabled: Flow<Boolean> = saturationLevel.map { it != null }
+
+    /**
+     * Slide progress from 0 to 100. Note that this can be null if the selected color is null, which
+     * means that the clock color respects the system theme color. In this case, the saturation
+     * level is no longer needed since we do not allow changing saturation level of the system theme
+     * color.
+     */
+    val sliderProgress: Flow<Int?> =
+        saturationLevel.map { saturation -> saturation?.let { (it * 100).roundToInt() } }
+
+    fun onSliderProgressChanged(progress: Int) {
+        val saturation = progress / 100f
+        val selectedOption = colorOptions.value.find { option -> option.isSelected }
+        selectedOption?.let { option ->
+            ColorUtils.colorToHSL(option.color0, helperColorHsl)
+            helperColorHsl[1] = saturation
+            interactor.setClockColor(ColorUtils.HSLToColor(helperColorHsl))
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val colorOptions: StateFlow<List<ColorOptionViewModel>> =
+        interactor.selectedClockColor
+            .mapLatest { selectedColor ->
+                // 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 {
+                    // TODO (b/241966062) Change design of the placeholder for default theme color
                     add(
                         ColorOptionViewModel(
-                            color0 = color,
-                            color1 = color,
-                            color2 = color,
-                            color3 = color,
-                            contentDescription = "description",
-                            isSelected = selectedColor == color,
+                            color0 = Color.TRANSPARENT,
+                            color1 = Color.TRANSPARENT,
+                            color2 = Color.TRANSPARENT,
+                            color3 = Color.TRANSPARENT,
+                            contentDescription =
+                                context.getString(
+                                    R.string.content_description_color_option,
+                                ),
+                            isSelected = selectedColor == null,
                             onClick =
-                                if (selectedColor == color) {
+                                if (selectedColor == null) {
                                     null
                                 } else {
-                                    { interactor.setClockColor(color) }
+                                    { interactor.setClockColor(null) }
                                 },
                         )
                     )
+
+                    if (selectedColor != null) {
+                        ColorUtils.colorToHSL(selectedColor, helperColorHsl)
+                    }
+
+                    val selectedColorPosition =
+                        if (selectedColor != null) {
+                            getSelectedColorPosition(helperColorHsl)
+                        } else {
+                            -1
+                        }
+
+                    COLOR_LIST_HSL.forEachIndexed { index, colorHSL ->
+                        val color = ColorUtils.HSLToColor(colorHSL)
+                        val isSelected = selectedColorPosition == index
+                        val colorToSet: Int by lazy {
+                            val saturation =
+                                if (selectedColor != null) {
+                                    helperColorHsl[1]
+                                } else {
+                                    colorHSL[1]
+                                }
+                            ColorUtils.HSLToColor(
+                                listOf(
+                                        colorHSL[0],
+                                        saturation,
+                                        colorHSL[2],
+                                    )
+                                    .toFloatArray()
+                            )
+                        }
+                        add(
+                            ColorOptionViewModel(
+                                color0 = color,
+                                color1 = color,
+                                color2 = color,
+                                color3 = color,
+                                contentDescription =
+                                    context.getString(
+                                        R.string.content_description_color_option,
+                                        index,
+                                    ),
+                                isSelected = isSelected,
+                                onClick =
+                                    if (isSelected) {
+                                        null
+                                    } else {
+                                        { interactor.setClockColor(colorToSet) }
+                                    },
+                            )
+                        )
+                    }
                 }
             }
-        }
+            .stateIn(
+                scope = viewModelScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = emptyList(),
+            )
 
     val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize
 
@@ -113,6 +223,32 @@
     companion object {
         // TODO (b/241966062) The color integers here are temporary for dev purposes. We need to
         //                    finalize the overridden colors.
-        val COLOR_LIST = listOf(-2563329, -8775, -1777665, -5442872)
+        val COLOR_LIST_HSL =
+            listOf(
+                arrayOf(225f, 0.65f, 0.74f).toFloatArray(),
+                arrayOf(30f, 0.65f, 0.74f).toFloatArray(),
+                arrayOf(249f, 0.65f, 0.74f).toFloatArray(),
+                arrayOf(144f, 0.65f, 0.74f).toFloatArray(),
+            )
+
+        val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+
+        fun getSelectedColorPosition(selectedColorHsl: FloatArray): Int {
+            return COLOR_LIST_HSL.withIndex().minBy { abs(it.value[0] - selectedColorHsl[0]) }.index
+        }
+    }
+
+    class Factory(
+        private val context: Context,
+        private val interactor: ClockPickerInteractor,
+    ) : ViewModelProvider.Factory {
+        override fun <T : ViewModel> create(modelClass: Class<T>): T {
+            @Suppress("UNCHECKED_CAST")
+            return ClockSettingsViewModel(
+                context = context,
+                interactor = interactor,
+            )
+                as T
+        }
     }
 }
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt
index a484027..72f5055 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsTabViewModelTest.kt
@@ -7,10 +7,12 @@
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.shared.ClockSize
 import com.android.wallpaper.testing.collectLastValue
+import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.test.setMain
@@ -35,7 +37,11 @@
         Dispatchers.setMain(testDispatcher)
         context = InstrumentationRegistry.getInstrumentation().targetContext
         underTest =
-            ClockSettingsViewModel(context, ClockPickerInteractor(FakeClockPickerRepository()))
+            ClockSettingsViewModel.Factory(
+                    context = context,
+                    interactor = ClockPickerInteractor(FakeClockPickerRepository()),
+                )
+                .create(ClockSettingsViewModel::class.java)
     }
 
     @After
@@ -46,15 +52,46 @@
     @Test
     fun setClockColor() = runTest {
         val observedClockColorOptions = collectLastValue(underTest.colorOptions)
+        // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
+        advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
         assertThat(observedClockColorOptions()!![0].isSelected).isTrue()
         assertThat(observedClockColorOptions()!![0].onClick).isNull()
 
         observedClockColorOptions()!![1].onClick?.invoke()
+        // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
+        advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
         assertThat(observedClockColorOptions()!![1].isSelected).isTrue()
         assertThat(observedClockColorOptions()!![1].onClick).isNull()
     }
 
     @Test
+    fun setClockSaturation() = runTest {
+        val observedClockColorOptions = collectLastValue(underTest.colorOptions)
+        val observedIsSliderEnabled = collectLastValue(underTest.isSliderEnabled)
+        val observedSliderProgress = collectLastValue(underTest.sliderProgress)
+        // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
+        advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+        assertThat(observedIsSliderEnabled()).isFalse()
+        assertThat(observedSliderProgress()).isNull()
+
+        observedClockColorOptions()!![1].onClick?.invoke()
+        // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
+        advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+        assertThat(observedIsSliderEnabled()).isTrue()
+        val targetProgress = 99
+        underTest.onSliderProgressChanged(targetProgress)
+        advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+        assertThat(observedClockColorOptions()!![1].isSelected).isTrue()
+        assertThat(observedSliderProgress())
+            .isIn(
+                Range.closed(
+                    targetProgress - 1,
+                    targetProgress + 1,
+                )
+            )
+    }
+
+    @Test
     fun setClockSize() = runTest {
         val observedClockSize = collectLastValue(underTest.selectedClockSize)
         underTest.setClockSize(ClockSize.DYNAMIC)
diff --git a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
index 71b2028..2627f92 100644
--- a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
+++ b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
@@ -14,6 +14,7 @@
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
 import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
@@ -53,6 +54,7 @@
     private var colorPickerInteractor: ColorPickerInteractor? = null
     private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
     private var clockCarouselViewModel: ClockCarouselViewModel? = null
+    private var clockSettingsViewModelFactory: ClockSettingsViewModel.Factory? = null
 
     override fun getCustomizationPreferences(context: Context): CustomizationPreferences {
         return customizationPreferences
@@ -202,6 +204,18 @@
             ?: ClockViewFactory(context, registry).also { clockViewFactory = it }
     }
 
+    override fun getClockSettingsViewModelFactory(
+        context: Context,
+        registry: ClockRegistry
+    ): ClockSettingsViewModel.Factory {
+        return clockSettingsViewModelFactory
+            ?: ClockSettingsViewModel.Factory(
+                    context,
+                    getClockPickerInteractor(context, registry),
+                )
+                .also { clockSettingsViewModelFactory = it }
+    }
+
     companion object {
         private const val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER = 1
     }