Fix race condition for clock carousel

Carousel.mMotionLayout is only ready after attachedToWindow. We can only
bind the view after attachedToWindow; otherwise, whenever the flow emits
any events before attachedToWindow and triggers calls to
Carousel.mMotionLayout, there will be a null pointer exception.

Test: Manually tested the app does not crash when emits early
Fixes: 278784117
Change-Id: Id65ed932b1526062063e453e910d16e01e1508dd
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 a27d019..dccfa6e 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/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