Merge "[TP] Make all clocks a flow" into tm-qpr-dev am: 19878d12c6 am: d3dd7f8d71 am: b4dc72782f

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/21468439

Change-Id: I8c3071412a3345ac361231b8e7958737ea7ccd74
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index df92556..9a58d70 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -318,7 +318,6 @@
                         secureSettingsRepository = getSecureSettingsRepository(context),
                         registry = clockRegistry,
                         scope = GlobalScope,
-                        backgroundDispatcher = Dispatchers.IO,
                     ),
                 )
                 .also { clockPickerInteractor = it }
@@ -339,9 +338,8 @@
         clockRegistry: ClockRegistry
     ): ClockCarouselViewModel {
         return clockCarouselViewModel
-            ?: ClockCarouselViewModel(getClockPickerInteractor(context, clockRegistry)).also {
-                clockCarouselViewModel = it
-            }
+            ?: ClockCarouselViewModel(getClockPickerInteractor(context, clockRegistry))
+                .also { clockCarouselViewModel = it }
     }
 
     override fun getClockViewFactory(
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 690b649..66793cd 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepository.kt
@@ -25,7 +25,7 @@
  * clocks.
  */
 interface ClockPickerRepository {
-    val allClocks: Array<ClockMetadataModel>
+    val allClocks: Flow<List<ClockMetadataModel>>
 
     val selectedClock: Flow<ClockMetadataModel>
 
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 6c845f9..a360b6c 100644
--- a/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/clock/data/repository/ClockPickerRepositoryImpl.kt
@@ -17,64 +17,84 @@
 package com.android.customization.picker.clock.data.repository
 
 import android.provider.Settings
-import android.util.Log
 import com.android.customization.picker.clock.shared.ClockSize
 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
 import com.android.systemui.plugins.ClockMetadata
 import com.android.systemui.shared.clocks.ClockRegistry
 import com.android.wallpaper.settings.data.repository.SecureSettingsRepository
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
 /** Implementation of [ClockPickerRepository], using [ClockRegistry]. */
 class ClockPickerRepositoryImpl(
     private val secureSettingsRepository: SecureSettingsRepository,
     private val registry: ClockRegistry,
-    private val scope: CoroutineScope,
-    private val backgroundDispatcher: CoroutineDispatcher,
+    scope: CoroutineScope,
 ) : ClockPickerRepository {
 
-    override val allClocks: Array<ClockMetadataModel> =
-        registry
-            .getClocks()
-            .filter { "NOT_IN_USE" !in it.clockId }
-            .map { it.toModel(null) }
-            .toTypedArray()
+    @OptIn(ExperimentalCoroutinesApi::class)
+    override val allClocks: Flow<List<ClockMetadataModel>> =
+        callbackFlow {
+                fun send() {
+                    val allClocks =
+                        registry
+                            .getClocks()
+                            .filter { "NOT_IN_USE" !in it.clockId }
+                            .map { it.toModel(null) }
+                    trySend(allClocks)
+                }
+
+                val listener =
+                    object : ClockRegistry.ClockChangeListener {
+                        override fun onAvailableClocksChanged() {
+                            send()
+                        }
+                    }
+                registry.registerClockChangeListener(listener)
+                send()
+                awaitClose { registry.unregisterClockChangeListener(listener) }
+            }
+            .mapLatest { allClocks ->
+                // Loading list of clock plugins can cause many consecutive calls of
+                // onAvailableClocksChanged(). We only care about the final fully-initiated clock
+                // list. Delay to avoid unnecessary too many emits.
+                delay(100)
+                allClocks
+            }
 
     /** The currently-selected clock. */
     override val selectedClock: Flow<ClockMetadataModel> =
         callbackFlow {
-                suspend fun send() {
-                    val currentClockId =
-                        withContext(backgroundDispatcher) { registry.currentClockId }
+                fun send() {
+                    val currentClockId = registry.currentClockId
+                    // It is possible that the model can be null since the full clock list is not
+                    // initiated.
                     val model =
                         registry
                             .getClocks()
                             .find { clockMetadata -> clockMetadata.clockId == currentClockId }
                             ?.toModel(registry.seedColor)
-                    if (model == null) {
-                        Log.w(
-                            TAG,
-                            "Clock with ID \"$currentClockId\" not found!",
-                        )
-                    }
                     trySend(model)
                 }
 
                 val listener =
                     object : ClockRegistry.ClockChangeListener {
                         override fun onCurrentClockChanged() {
-                            scope.launch { send() }
+                            send()
+                        }
+
+                        override fun onAvailableClocksChanged() {
+                            send()
                         }
                     }
                 registry.registerClockChangeListener(listener)
@@ -105,19 +125,13 @@
             )
 
     override suspend fun setClockSize(size: ClockSize) {
-        withContext(backgroundDispatcher) {
-            secureSettingsRepository.set(
-                name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
-                value = if (size == ClockSize.DYNAMIC) 1 else 0,
-            )
-        }
+        secureSettingsRepository.set(
+            name = Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
+            value = if (size == ClockSize.DYNAMIC) 1 else 0,
+        )
     }
 
     private fun ClockMetadata.toModel(color: Int?): ClockMetadataModel {
         return ClockMetadataModel(clockId = clockId, name = name, color = color)
     }
-
-    companion object {
-        private const val TAG = "ClockPickerRepositoryImpl"
-    }
 }
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 c12778b..677fcfa 100644
--- a/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
+++ b/src/com/android/customization/picker/clock/domain/interactor/ClockPickerInteractor.kt
@@ -28,7 +28,8 @@
  * clocks.
  */
 class ClockPickerInteractor(private val repository: ClockPickerRepository) {
-    val allClocks: Array<ClockMetadataModel> = repository.allClocks
+
+    val allClocks: Flow<List<ClockMetadataModel>> = repository.allClocks
 
     val selectedClock: Flow<ClockMetadataModel> = repository.selectedClock
 
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index 841c411..35a78cb 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -23,6 +23,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.customization.picker.clock.ui.view.ClockCarouselView
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 
 object ClockCarouselViewBinder {
@@ -42,14 +43,22 @@
         clockViewFactory: (clockId: String) -> View,
         lifecycleOwner: LifecycleOwner,
     ): Binding {
-        view.setUpImageCarouselView(
-            clockIds = viewModel.allClockIds,
-            onGetClockPreview = clockViewFactory,
-            onClockSelected = { clockId -> viewModel.setSelectedClock(clockId) }
-        )
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                launch { viewModel.selectedClockId.collect { view.setSelectedClockId(it) } }
+                launch {
+                    viewModel.allClockIds.collect { allClockIds ->
+                        view.setUpClockCarouselView(
+                            clockIds = allClockIds,
+                            onGetClockPreview = clockViewFactory,
+                            onClockSelected = { clockId -> viewModel.setSelectedClock(clockId) },
+                        )
+                    }
+                }
+                launch {
+                    viewModel.selectedIndex.collect { selectedIndex ->
+                        view.setSelectedClockIndex(selectedIndex)
+                    }
+                }
             }
         }
         return object : Binding {
diff --git a/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt b/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt
index a4b1c1e..d0186b2 100644
--- a/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt
+++ b/src/com/android/customization/picker/clock/ui/fragment/ClockCarouselDemoFragment.kt
@@ -49,10 +49,9 @@
                 }
             ClockCarouselViewBinder.bind(
                 view = carouselView,
-                viewModel =
-                    ClockCarouselViewModel(
-                        injector.getClockPickerInteractor(requireContext(), registry)
-                    ),
+                viewModel = ClockCarouselViewModel(
+                    injector.getClockPickerInteractor(requireContext(), registry),
+                ),
                 clockViewFactory = { clockId ->
                     registry.createExampleClock(clockId)?.largeClock?.view!!
                 },
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
index e16492f..90d7c42 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -42,23 +42,24 @@
         carousel = view.requireViewById(R.id.carousel)
     }
 
-    fun setUpImageCarouselView(
-        clockIds: Array<String>,
+    fun setUpClockCarouselView(
+        clockIds: List<String>,
         onGetClockPreview: (clockId: String) -> View,
         onClockSelected: (clockId: String) -> Unit,
     ) {
         adapter = ClockCarouselAdapter(clockIds, onGetClockPreview, onClockSelected)
         carousel.setAdapter(adapter)
+        carousel.refresh()
     }
 
-    fun setSelectedClockId(
-        selectedClockId: String,
+    fun setSelectedClockIndex(
+        index: Int,
     ) {
-        carousel.jumpToIndex(adapter.clockIds.indexOf(selectedClockId))
+        carousel.jumpToIndex(index)
     }
 
     class ClockCarouselAdapter(
-        val clockIds: Array<String>,
+        val clockIds: List<String>,
         val onGetClockPreview: (clockId: String) -> View,
         val onClockSelected: (clockId: String) -> Unit,
     ) : Carousel.Adapter {
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 b126a73..669c047 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -16,15 +16,49 @@
 package com.android.customization.picker.clock.ui.viewmodel
 
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.mapNotNull
 
-class ClockCarouselViewModel(private val interactor: ClockPickerInteractor) {
-    val selectedClockId: Flow<String> = interactor.selectedClock.map { it.clockId }
+class ClockCarouselViewModel(
+    private val interactor: ClockPickerInteractor,
+) {
 
-    val allClockIds: Array<String> = interactor.allClocks.map { it.clockId }.toTypedArray()
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val allClockIds: Flow<List<String>> =
+        interactor.allClocks
+            .mapLatest { clockArray ->
+                // Delay to avoid the case that the full list of clocks is not initiated.
+                delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
+                clockArray.map { it.clockId }
+            }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val selectedIndex: Flow<Int> =
+        allClockIds
+            .flatMapLatest { allClockIds ->
+                interactor.selectedClock.map { selectedClock ->
+                    val index = allClockIds.indexOf(selectedClock.clockId)
+                    if (index >= 0) {
+                        index
+                    } else {
+                        null
+                    }
+                }
+            }
+            .mapNotNull { it }
 
     fun setSelectedClock(clockId: String) {
         interactor.setSelectedClock(clockId)
     }
+
+    companion object {
+        const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+    }
 }
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 1ffb7b8..c15bc67 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -232,7 +232,7 @@
                 arrayOf(144f, 0.65f, 0.74f).toFloatArray(),
             )
 
-        val COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+        const 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
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 1614c61..d2a9efc 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
@@ -23,14 +23,14 @@
 import kotlinx.coroutines.flow.combine
 
 class FakeClockPickerRepository : ClockPickerRepository {
+    override val allClocks: Flow<List<ClockMetadataModel>> =
+        MutableStateFlow(fakeClocks).asStateFlow()
 
-    override val allClocks: Array<ClockMetadataModel> = fakeClocks
-
-    private val _selectedClockId = MutableStateFlow(fakeClocks[0].clockId)
-    private val _clockColor = MutableStateFlow<Int?>(null)
+    private val selectedClockId = MutableStateFlow(fakeClocks[0].clockId)
+    private val clockColor = MutableStateFlow<Int?>(null)
     override val selectedClock: Flow<ClockMetadataModel> =
-        combine(_selectedClockId, _clockColor) { selectedClockId, clockColor ->
-            val selectedClock = allClocks.find { clock -> clock.clockId == selectedClockId }
+        combine(selectedClockId, clockColor) { selectedClockId, clockColor ->
+            val selectedClock = fakeClocks.find { clock -> clock.clockId == selectedClockId }
             checkNotNull(selectedClock)
             ClockMetadataModel(selectedClock.clockId, selectedClock.name, clockColor)
         }
@@ -39,11 +39,11 @@
     override val selectedClockSize: Flow<ClockSize> = _selectedClockSize.asStateFlow()
 
     override fun setSelectedClock(clockId: String) {
-        _selectedClockId.value = clockId
+        selectedClockId.value = clockId
     }
 
     override fun setClockColor(color: Int?) {
-        _clockColor.value = color
+        clockColor.value = color
     }
 
     override suspend fun setClockSize(size: ClockSize) {
@@ -52,7 +52,7 @@
 
     companion object {
         val fakeClocks =
-            arrayOf(
+            listOf(
                 ClockMetadataModel("clock0", "clock0", null),
                 ClockMetadataModel("clock1", "clock1", null),
                 ClockMetadataModel("clock2", "clock2", null),
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 776663e..0ce9714 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
@@ -23,6 +23,7 @@
 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
@@ -43,7 +44,8 @@
     fun setUp() {
         val testDispatcher = StandardTestDispatcher()
         Dispatchers.setMain(testDispatcher)
-        underTest = ClockCarouselViewModel(ClockPickerInteractor(FakeClockPickerRepository()))
+        underTest =
+            ClockCarouselViewModel(ClockPickerInteractor(FakeClockPickerRepository()))
     }
 
     @After
@@ -53,10 +55,9 @@
 
     @Test
     fun setSelectedClock() = runTest {
-        val observedSelectedClock = collectLastValue(underTest.selectedClockId)
-
+        val observedSelectedIndex = collectLastValue(underTest.selectedIndex)
+        advanceTimeBy(ClockCarouselViewModel.CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
         underTest.setSelectedClock(FakeClockPickerRepository.fakeClocks[2].clockId)
-        assertThat(observedSelectedClock())
-            .isEqualTo(FakeClockPickerRepository.fakeClocks[2].clockId)
+        assertThat(observedSelectedIndex()).isEqualTo(2)
     }
 }
diff --git a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
index 0aac5cc..2fe3309 100644
--- a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
+++ b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
@@ -195,9 +195,8 @@
         clockRegistry: ClockRegistry
     ): ClockCarouselViewModel {
         return clockCarouselViewModel
-            ?: ClockCarouselViewModel(getClockPickerInteractor(context, clockRegistry)).also {
-                clockCarouselViewModel = it
-            }
+            ?: ClockCarouselViewModel(getClockPickerInteractor(context, clockRegistry))
+                .also { clockCarouselViewModel = it }
     }
 
     override fun getClockViewFactory(