Modify null clock handling in picker

The plugin crash protection layer is predicated on ClockRegistry
returning null when a plugin has a crashing problem. As a result,
ThemePicker requiring a non-null response appears to hide the
underlying error. This fixes that by handling the null value at
all the relevant callsites by doing nothing.

The clock parts of ThemePicker will still undoubtedly be broken in
some way since a clock is failing to load or function, but this is
preferrable to ThemePicker crashing as a result of changes to the
library, especially since picker presubmits are not currently
running there.

Bug: 398070481
Test: Manual + Presubmits
Flag: NONE Bugfix
Change-Id: Id8e6f9888bc3b84c084587b2b4adade3934de286
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 27bc42c..dccef6e 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -467,15 +467,16 @@
                 it.pivotY = it.height / 2F
             }
 
-            val controller = clockViewFactory.getController(clockId)
-            if (isMiddleView) {
-                clockScaleView.scaleX = 1f
-                clockScaleView.scaleY = 1f
-                controller.largeClock.animations.onPickerCarouselSwiping(1F)
-            } else {
-                clockScaleView.scaleX = clockViewScale
-                clockScaleView.scaleY = clockViewScale
-                controller.largeClock.animations.onPickerCarouselSwiping(0F)
+            clockViewFactory.getController(clockId)?.let { controller ->
+                if (isMiddleView) {
+                    clockScaleView.scaleX = 1f
+                    clockScaleView.scaleY = 1f
+                    controller.largeClock.animations.onPickerCarouselSwiping(1F)
+                } else {
+                    clockScaleView.scaleX = clockViewScale
+                    clockScaleView.scaleY = clockViewScale
+                    controller.largeClock.animations.onPickerCarouselSwiping(0F)
+                }
             }
         }
 
diff --git a/src/com/android/customization/picker/clock/ui/view/ThemePickerClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ThemePickerClockViewFactory.kt
index 73ebb0f..c48d810 100644
--- a/src/com/android/customization/picker/clock/ui/view/ThemePickerClockViewFactory.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ThemePickerClockViewFactory.kt
@@ -43,7 +43,7 @@
 class ThemePickerClockViewFactory
 @Inject
 constructor(
-    activity: Activity,
+    private val activity: Activity,
     private val wallpaperManager: WallpaperManager,
     private val registry: ClockRegistry,
 ) : ClockViewFactory {
@@ -55,9 +55,9 @@
     private val clockControllers: ConcurrentHashMap<String, ClockController> = ConcurrentHashMap()
     private val smallClockFrames: HashMap<String, FrameLayout> = HashMap()
 
-    override fun getController(clockId: String): ClockController {
+    override fun getController(clockId: String): ClockController? {
         return clockControllers[clockId]
-            ?: initClockController(clockId).also { clockControllers[clockId] = it }
+            ?: initClockController(clockId)?.also { clockControllers[clockId] = it }
     }
 
     /**
@@ -66,10 +66,10 @@
      */
     override fun getLargeView(clockId: String): View {
         assert(!Flags.newCustomizationPickerUi())
-        return getController(clockId).largeClock.let {
+        return getController(clockId)?.largeClock?.let {
             it.animations.onPickerCarouselSwiping(1F)
             it.view
-        }
+        } ?: FrameLayout(activity)
     }
 
     /**
@@ -83,9 +83,9 @@
                 (layoutParams as FrameLayout.LayoutParams).topMargin = getSmallClockTopMargin()
                 (layoutParams as FrameLayout.LayoutParams).marginStart = getSmallClockStartPadding()
             }
-                ?: createSmallClockFrame().also {
-                    it.addView(getController(clockId).smallClock.view)
-                    smallClockFrames[clockId] = it
+                ?: createSmallClockFrame().also { frame ->
+                    getController(clockId)?.let { frame.addView(it.smallClock.view) }
+                    smallClockFrames[clockId] = frame
                 }
         smallClockFrame.translationX = 0F
         smallClockFrame.translationY = 0F
@@ -130,14 +130,14 @@
     }
 
     override fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
-        getController(clockId).let {
+        getController(clockId)?.let {
             it.largeClock.run { events.onThemeChanged(theme.copy(seedColor = seedColor)) }
             it.smallClock.run { events.onThemeChanged(theme.copy(seedColor = seedColor)) }
         }
     }
 
     override fun updateFontAxes(clockId: String, settings: List<ClockFontAxisSetting>) {
-        getController(clockId).let { it.events.onFontAxesChanged(settings) }
+        getController(clockId)?.let { it.events.onFontAxesChanged(settings) }
     }
 
     override fun updateRegionDarkness() {
@@ -155,8 +155,8 @@
 
     override fun updateTimeFormat(clockId: String) {
         getController(clockId)
-            .events
-            .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
+            ?.events
+            ?.onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
     }
 
     override fun registerTimeTicker(owner: LifecycleOwner) {
@@ -190,33 +190,32 @@
         }
     }
 
-    private fun initClockController(clockId: String): ClockController {
+    private fun initClockController(clockId: String): ClockController? {
         val isWallpaperDark = isLockscreenWallpaperDark()
-        val controller =
-            registry.createExampleClock(clockId).also { it?.initialize(isWallpaperDark, 0f, 0f) }
-        checkNotNull(controller)
+        return registry.createExampleClock(clockId)?.also { controller ->
+            controller.initialize(isWallpaperDark, 0f, 0f)
 
-        // Initialize large clock
-        controller.largeClock.events.onFontSettingChanged(
-            resources
-                .getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.large_clock_text_size
-                )
-                .toFloat()
-        )
-        controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
+            // Initialize large clock
+            controller.largeClock.events.onFontSettingChanged(
+                resources
+                    .getDimensionPixelSize(
+                        com.android.systemui.customization.R.dimen.large_clock_text_size
+                    )
+                    .toFloat()
+            )
+            controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
 
-        // Initialize small clock
-        controller.smallClock.events.onFontSettingChanged(
-            resources
-                .getDimensionPixelSize(
-                    com.android.systemui.customization.R.dimen.small_clock_text_size
-                )
-                .toFloat()
-        )
-        controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion())
-        controller.events.onWeatherDataChanged(WeatherData.getPlaceholderWeatherData())
-        return controller
+            // Initialize small clock
+            controller.smallClock.events.onFontSettingChanged(
+                resources
+                    .getDimensionPixelSize(
+                        com.android.systemui.customization.R.dimen.small_clock_text_size
+                    )
+                    .toFloat()
+            )
+            controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion())
+            controller.events.onWeatherDataChanged(WeatherData.getPlaceholderWeatherData())
+        }
     }
 
     /**
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 28f58c4..874b49d 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -32,6 +32,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
@@ -57,14 +58,22 @@
             .mapLatest { allClocks ->
                 // Delay to avoid the case that the full list of clocks is not initiated.
                 delay(CLOCKS_EVENT_UPDATE_DELAY_MILLIS)
-                allClocks.map {
-                    val contentDescription =
-                        resources.getString(
-                            R.string.select_clock_action_description,
-                            clockViewFactory.getController(it.clockId).config.description
-                        )
-                    ClockCarouselItemViewModel(it.clockId, it.isSelected, contentDescription)
-                }
+                allClocks
+                    .map { model ->
+                        clockViewFactory.getController(model.clockId)?.let { clock ->
+                            val contentDescription =
+                                resources.getString(
+                                    R.string.select_clock_action_description,
+                                    clock.config.description,
+                                )
+                            ClockCarouselItemViewModel(
+                                model.clockId,
+                                model.isSelected,
+                                contentDescription,
+                            )
+                        }
+                    }
+                    .filterNotNull()
             }
             // makes sure that the operations above this statement are executed on I/O dispatcher
             // while parallelism limits the number of threads this can run on which makes sure that
@@ -126,6 +135,7 @@
             .mapNotNull { it }
 
     private var setSelectedClockJob: Job? = null
+
     fun setSelectedClock(clockId: String) {
         setSelectedClockJob?.cancel()
         setSelectedClockJob =
diff --git a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
index b44b152..9ce7b61 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
@@ -418,7 +418,7 @@
                             clockHostView.removeAllViews()
                             // For new customization picker, we should get views from clocklayout
                             if (Flags.newCustomizationPickerUi()) {
-                                clockViewFactory.getController(clock.clockId).let { clockController
+                                clockViewFactory.getController(clock.clockId)?.let { clockController
                                     ->
                                     val udfpsTop =
                                         clockPickerViewModel.getUdfpsLocation()?.let {
diff --git a/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt b/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
index 9fc59c7..1f2a0f3 100644
--- a/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
+++ b/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
@@ -146,14 +146,14 @@
                             )
                             .collect { (previewingClock, previewingClockSize) ->
                                 val hideSmartspace =
-                                    clockViewFactory.getController(previewingClock.clockId).let {
+                                    clockViewFactory.getController(previewingClock.clockId)?.let {
                                         when (previewingClockSize) {
                                             ClockSize.DYNAMIC ->
                                                 it.largeClock.config.hasCustomWeatherDataDisplay
                                             ClockSize.SMALL ->
                                                 it.smallClock.config.hasCustomWeatherDataDisplay
                                         }
-                                    }
+                                    } ?: false
                                 workspaceCallback.sendMessage(
                                     MESSAGE_ID_HIDE_SMART_SPACE,
                                     Bundle().apply {
diff --git a/tests/robotests/src/com/android/customization/picker/clock/ui/FakeClockViewFactory.kt b/tests/robotests/src/com/android/customization/picker/clock/ui/FakeClockViewFactory.kt
index 418b439..d6286a4 100644
--- a/tests/robotests/src/com/android/customization/picker/clock/ui/FakeClockViewFactory.kt
+++ b/tests/robotests/src/com/android/customization/picker/clock/ui/FakeClockViewFactory.kt
@@ -35,7 +35,7 @@
         override fun dump(pw: PrintWriter) = TODO("Not yet implemented")
     }
 
-    override fun getController(clockId: String): ClockController = clockControllers[clockId]!!
+    override fun getController(clockId: String): ClockController? = clockControllers[clockId]
 
     override fun getLargeView(clockId: String): View {
         TODO("Not yet implemented")