Merge "Fix for unresponsive color picker collection" into udc-dev
diff --git a/src/com/android/customization/module/CustomizationInjector.kt b/src/com/android/customization/module/CustomizationInjector.kt
index 479bca2..c0e4124 100644
--- a/src/com/android/customization/module/CustomizationInjector.kt
+++ b/src/com/android/customization/module/CustomizationInjector.kt
@@ -63,7 +63,9 @@
         wallpaperColorsViewModel: WallpaperColorsViewModel,
     ): ColorPickerViewModel.Factory
 
-    fun getClockCarouselViewModel(context: Context): ClockCarouselViewModel
+    fun getClockCarouselViewModelFactory(
+        interactor: ClockPickerInteractor,
+    ): ClockCarouselViewModel.Factory
 
     fun getClockViewFactory(activity: Activity): ClockViewFactory
 
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index da11d51..2d36db4 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -26,6 +26,7 @@
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
 import com.android.customization.model.color.ColorCustomizationManager
 import com.android.customization.model.color.ColorOptionsProvider
 import com.android.customization.model.grid.GridOptionsManager
@@ -102,7 +103,7 @@
     private var clockRegistry: ClockRegistry? = null
     private var clockPickerInteractor: ClockPickerInteractor? = null
     private var clockSectionViewModel: ClockSectionViewModel? = null
-    private var clockCarouselViewModel: ClockCarouselViewModel? = null
+    private var clockCarouselViewModelFactory: ClockCarouselViewModel.Factory? = null
     private var clockViewFactory: ClockViewFactory? = null
     private var notificationsInteractor: NotificationsInteractor? = null
     private var notificationSectionViewModelFactory: NotificationSectionViewModel.Factory? = null
@@ -119,6 +120,14 @@
     private var gridScreenViewModelFactory: GridScreenViewModel.Factory? = null
 
     override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
+        val clockCarouselViewModel =
+            ViewModelProvider(
+                    activity,
+                    getClockCarouselViewModelFactory(
+                        getClockPickerInteractor(activity.applicationContext),
+                    ),
+                )
+                .get() as ClockCarouselViewModel
         return customizationSections
             ?: DefaultCustomizationSections(
                     getColorPickerViewModelFactory(
@@ -131,7 +140,7 @@
                         interactor = getNotificationsInteractor(activity),
                     ),
                     getFlags(),
-                    getClockCarouselViewModel(activity),
+                    clockCarouselViewModel,
                     getClockViewFactory(activity),
                     getDarkModeSnapshotRestorer(activity),
                     getThemedIconSnapshotRestorer(activity),
@@ -346,10 +355,12 @@
             }
     }
 
-    override fun getClockCarouselViewModel(context: Context): ClockCarouselViewModel {
-        return clockCarouselViewModel
-            ?: ClockCarouselViewModel(getClockPickerInteractor(context)).also {
-                clockCarouselViewModel = it
+    override fun getClockCarouselViewModelFactory(
+        interactor: ClockPickerInteractor,
+    ): ClockCarouselViewModel.Factory {
+        return clockCarouselViewModelFactory
+            ?: ClockCarouselViewModel.Factory(interactor).also {
+                clockCarouselViewModelFactory = it
             }
     }
 
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 a2d5aec..d71e202 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -38,7 +38,7 @@
 
     var isCarouselInTransition = false
 
-    private val carousel: Carousel
+    val carousel: Carousel
     private val motionLayout: MotionLayout
     private lateinit var adapter: ClockCarouselAdapter
     private lateinit var scalingUpClockController: ClockController
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 c01f56a..34ccdb6 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -15,30 +15,39 @@
  */
 package com.android.customization.picker.clock.ui.viewmodel
 
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
 
 /**
  * Clock carousel view model that provides data for the carousel of clock previews. When there is
  * only one item, we should show a single clock preview instead of a carousel.
  */
-class ClockCarouselViewModel(
+class ClockCarouselViewModel
+constructor(
     private val interactor: ClockPickerInteractor,
-) {
+) : ViewModel() {
     @OptIn(ExperimentalCoroutinesApi::class)
-    val allClockIds: Flow<List<String>> =
-        interactor.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 { it.clockId }
-        }
+    val allClockIds: StateFlow<List<String>> =
+        interactor.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 { it.clockId }
+            }
+            .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
 
     val seedColor: Flow<Int?> = interactor.seedColor
 
@@ -72,6 +81,18 @@
         interactor.setSelectedClock(clockId)
     }
 
+    class Factory(
+        private val interactor: ClockPickerInteractor,
+    ) : ViewModelProvider.Factory {
+        override fun <T : ViewModel> create(modelClass: Class<T>): T {
+            @Suppress("UNCHECKED_CAST")
+            return ClockCarouselViewModel(
+                interactor = interactor,
+            )
+                as T
+        }
+    }
+
     companion object {
         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
     }
diff --git a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
index 43fb85b..2f83fa7 100644
--- a/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
+++ b/src/com/android/customization/picker/preview/ui/section/PreviewWithClockCarouselSectionController.kt
@@ -20,8 +20,10 @@
 import android.app.Activity
 import android.content.Context
 import android.view.View
+import android.view.View.OnAttachStateChangeListener
 import android.view.ViewGroup
 import android.view.ViewStub
+import androidx.constraintlayout.helper.widget.Carousel
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import com.android.customization.picker.clock.ui.binder.ClockCarouselViewBinder
@@ -39,6 +41,7 @@
 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewSectionController
 import com.android.wallpaper.picker.customization.ui.section.ScreenPreviewView
 import com.android.wallpaper.util.DisplayUtils
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 
 /** Controls the screen preview section. */
@@ -90,15 +93,39 @@
             val singleClockViewStub: ViewStub = view.requireViewById(R.id.single_clock_view_stub)
             singleClockViewStub.layoutResource = R.layout.single_clock_view
             val singleClockView = singleClockViewStub.inflate() as ViewGroup
-            lifecycleOwner.lifecycleScope.launch {
-                ClockCarouselViewBinder.bind(
-                    carouselView = carouselView,
-                    singleClockView = singleClockView,
-                    viewModel = clockCarouselViewModel,
-                    clockViewFactory = clockViewFactory,
-                    lifecycleOwner = lifecycleOwner,
-                )
-            }
+
+            /**
+             * Only bind after [Carousel.onAttachedToWindow]. This is to avoid the race condition
+             * that the flow emits before attached to window where [Carousel.mMotionLayout] is still
+             * null.
+             */
+            var onAttachStateChangeListener: OnAttachStateChangeListener? = null
+            var bindJob: Job? = null
+            onAttachStateChangeListener =
+                object : OnAttachStateChangeListener {
+                    override fun onViewAttachedToWindow(view: View?) {
+                        bindJob =
+                            lifecycleOwner.lifecycleScope.launch {
+                                ClockCarouselViewBinder.bind(
+                                    carouselView = carouselView,
+                                    singleClockView = singleClockView,
+                                    viewModel = clockCarouselViewModel,
+                                    clockViewFactory = clockViewFactory,
+                                    lifecycleOwner = lifecycleOwner,
+                                )
+                                if (onAttachStateChangeListener != null) {
+                                    carouselView.carousel.removeOnAttachStateChangeListener(
+                                        onAttachStateChangeListener,
+                                    )
+                                }
+                            }
+                    }
+
+                    override fun onViewDetachedFromWindow(view: View?) {
+                        bindJob?.cancel()
+                    }
+                }
+            carouselView.carousel.addOnAttachStateChangeListener(onAttachStateChangeListener)
         }
 
         return view