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