Suspend set selected clock function

1. Make the function suspend to void UI blocking.
2. Only set the clock after a short delay to avoid users scrolling
   frquently and too many heavy calls of setting clocks.

Test: Manually tested the clock carousel is smooth
Bug: 278850684
Change-Id: Ie3c1e6f274597f97d44cea59eb533aaf9c8cc949
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index e716406..98b0f5c 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -357,7 +357,7 @@
         interactor: ClockPickerInteractor,
     ): ClockCarouselViewModel.Factory {
         return clockCarouselViewModelFactory
-            ?: ClockCarouselViewModel.Factory(interactor).also {
+            ?: ClockCarouselViewModel.Factory(interactor, Dispatchers.IO).also {
                 clockCarouselViewModelFactory = it
             }
     }
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
index ae66ce3..cb2c86e 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
@@ -33,7 +33,7 @@
 
     val selectedClockSize: Flow<ClockSize>
 
-    fun setSelectedClock(clockId: String)
+    suspend fun setSelectedClock(clockId: String)
 
     /**
      * Set clock color to the settings.
diff --git a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
index 9537365..747f174 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
@@ -110,7 +110,7 @@
             }
             .mapNotNull { it }
 
-    override fun setSelectedClock(clockId: String) {
+    override suspend fun setSelectedClock(clockId: String) {
         registry.mutateSetting { oldSettings ->
             val newSettings = oldSettings.copy(clockId = clockId)
             newSettings.metadata = oldSettings.metadata
diff --git a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
index 6f3657a..91b2773 100644
--- a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
@@ -47,7 +47,7 @@
 
     val selectedClockSize: Flow<ClockSize> = repository.selectedClockSize
 
-    fun setSelectedClock(clockId: String) {
+    suspend fun setSelectedClock(clockId: String) {
         repository.setSelectedClock(clockId)
     }
 
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
index 34ccdb6..47c6d2d 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -19,7 +19,9 @@
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
@@ -30,6 +32,7 @@
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * Clock carousel view model that provides data for the carousel of clock previews. When there is
@@ -38,6 +41,7 @@
 class ClockCarouselViewModel
 constructor(
     private val interactor: ClockPickerInteractor,
+    private val backgroundDispatcher: CoroutineDispatcher,
 ) : ViewModel() {
     @OptIn(ExperimentalCoroutinesApi::class)
     val allClockIds: StateFlow<List<String>> =
@@ -77,17 +81,25 @@
             .map { allClockIds -> if (allClockIds.size == 1) allClockIds[0] else null }
             .mapNotNull { it }
 
+    private var setSelectedClockJob: Job? = null
     fun setSelectedClock(clockId: String) {
-        interactor.setSelectedClock(clockId)
+        setSelectedClockJob?.cancel()
+        setSelectedClockJob =
+            viewModelScope.launch(backgroundDispatcher) {
+                delay(SET_SELECTED_CLOCK_DELAY_MILLIS)
+                interactor.setSelectedClock(clockId)
+            }
     }
 
     class Factory(
         private val interactor: ClockPickerInteractor,
+        private val backgroundDispatcher: CoroutineDispatcher,
     ) : ViewModelProvider.Factory {
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
             @Suppress("UNCHECKED_CAST")
             return ClockCarouselViewModel(
                 interactor = interactor,
+                backgroundDispatcher = backgroundDispatcher,
             )
                 as T
         }
@@ -95,5 +107,9 @@
 
     companion object {
         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+
+        // In the case if the user scroll the clock carousel frequently, we make a delay for
+        // setting the selected clock to avoid too many heavy calls.
+        const val SET_SELECTED_CLOCK_DELAY_MILLIS: Long = 650
     }
 }
diff --git a/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt b/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
index 2b0b47f..38bf25a 100644
--- a/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
+++ b/tests/src/com/android/customization/picker/clock/data/repository/FakeClockPickerRepository.kt
@@ -56,7 +56,7 @@
     private val _selectedClockSize = MutableStateFlow(ClockSize.SMALL)
     override val selectedClockSize: Flow<ClockSize> = _selectedClockSize.asStateFlow()
 
-    override fun setSelectedClock(clockId: String) {
+    override suspend fun setSelectedClock(clockId: String) {
         selectedClockId.value = clockId
     }
 
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
index 5bc7461..4cda0d5 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModelTest.kt
@@ -21,6 +21,7 @@
 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
 import com.android.wallpaper.testing.collectLastValue
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -52,11 +53,12 @@
             )
         )
     }
+    private lateinit var testDispatcher: CoroutineDispatcher
     private lateinit var underTest: ClockCarouselViewModel
 
     @Before
     fun setUp() {
-        val testDispatcher = StandardTestDispatcher()
+        testDispatcher = StandardTestDispatcher()
         Dispatchers.setMain(testDispatcher)
     }
 
@@ -67,16 +69,25 @@
 
     @Test
     fun setSelectedClock() = runTest {
-        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithMultipleClocks))
+        underTest =
+            ClockCarouselViewModel(
+                ClockPickerInteractor(repositoryWithMultipleClocks),
+                testDispatcher,
+            )
         val observedSelectedIndex = collectLastValue(underTest.selectedIndex)
         advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
         underTest.setSelectedClock(FakeClockPickerRepository.fakeClocks[2].clockId)
+        advanceTimeBy(ClockCarouselViewModel.SET_SELECTED_CLOCK_DELAY_MILLIS)
         assertThat(observedSelectedIndex()).isEqualTo(2)
     }
 
     @Test
     fun multipleClockCase() = runTest {
-        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithMultipleClocks))
+        underTest =
+            ClockCarouselViewModel(
+                ClockPickerInteractor(repositoryWithMultipleClocks),
+                testDispatcher,
+            )
         val observedIsCarouselVisible = collectLastValue(underTest.isCarouselVisible)
         val observedIsSingleClockViewVisible = collectLastValue(underTest.isSingleClockViewVisible)
         advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
@@ -86,7 +97,11 @@
 
     @Test
     fun singleClockCase() = runTest {
-        underTest = ClockCarouselViewModel(ClockPickerInteractor(repositoryWithSingleClock))
+        underTest =
+            ClockCarouselViewModel(
+                ClockPickerInteractor(repositoryWithSingleClock),
+                testDispatcher,
+            )
         val observedIsCarouselVisible = collectLastValue(underTest.isCarouselVisible)
         val observedIsSingleClockViewVisible = collectLastValue(underTest.isSingleClockViewVisible)
         advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
index 92059e3..a329bb3 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
@@ -15,8 +15,10 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runTest
@@ -33,6 +35,7 @@
 class ClockSettingsViewModelTest {
 
     private lateinit var context: Context
+    private lateinit var testScope: TestScope
     private lateinit var colorPickerInteractor: ColorPickerInteractor
     private lateinit var clockPickerInteractor: ClockPickerInteractor
     private lateinit var underTest: ClockSettingsViewModel
@@ -53,6 +56,7 @@
         val testDispatcher = StandardTestDispatcher()
         Dispatchers.setMain(testDispatcher)
         context = InstrumentationRegistry.getInstrumentation().targetContext
+        testScope = TestScope(testDispatcher)
         clockPickerInteractor = ClockPickerInteractor(FakeClockPickerRepository())
         colorPickerInteractor =
             ColorPickerInteractor(
@@ -73,7 +77,9 @@
                 .create(ClockSettingsViewModel::class.java)
         colorMap = ClockColorViewModel.getPresetColorMap(context.resources)
 
-        clockPickerInteractor.setSelectedClock(FakeClockPickerRepository.CLOCK_ID_0)
+        testScope.launch {
+            clockPickerInteractor.setSelectedClock(FakeClockPickerRepository.CLOCK_ID_0)
+        }
     }
 
     @After