Merge "Fix stale home wallpaper info (1/3)" into main
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 6b615cd..1d16bc1 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -43,6 +43,7 @@
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.domain.interactor.ClockPickerSnapshotRestorer
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
+import com.android.customization.picker.clock.ui.view.ClockViewFactoryImpl
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
 import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
@@ -373,7 +374,7 @@
     override fun getClockViewFactory(activity: ComponentActivity): ClockViewFactory {
         val activityHashCode = activity.hashCode()
         return clockViewFactories[activityHashCode]
-            ?: ClockViewFactory(
+            ?: ClockViewFactoryImpl(
                     activity.applicationContext,
                     ScreenSizeCalculator.getInstance()
                         .getScreenSize(activity.windowManager.defaultDisplay),
diff --git a/src/com/android/customization/module/logging/StatsLogUserEventLogger.kt b/src/com/android/customization/module/logging/StatsLogUserEventLogger.kt
index 90a1c6f..344a4f8 100644
--- a/src/com/android/customization/module/logging/StatsLogUserEventLogger.kt
+++ b/src/com/android/customization/module/logging/StatsLogUserEventLogger.kt
@@ -22,6 +22,7 @@
 import com.android.customization.model.color.ColorOption
 import com.android.customization.model.grid.GridOption
 import com.android.customization.module.SysUiStatsLogger
+import com.android.customization.module.logging.ThemesUserEventLogger.ClockSize
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.wallpaper.module.WallpaperPersister.DEST_BOTH
 import com.android.wallpaper.module.WallpaperPersister.DEST_HOME_SCREEN
@@ -37,18 +38,6 @@
 class StatsLogUserEventLogger(private val preferences: WallpaperPreferences) :
     NoOpUserEventLogger(), ThemesUserEventLogger {
 
-    override fun logAppLaunched(launchSource: Intent) {
-        SysUiStatsLogger(SysUiStatsLog.STYLE_UICHANGED__ACTION__APP_LAUNCHED)
-            .setLaunchedPreference(getAppLaunchSource(launchSource))
-            .log()
-    }
-
-    override fun logActionClicked(collectionId: String, actionLabelResId: Int) {
-        SysUiStatsLogger(StyleEnums.WALLPAPER_EXPLORE)
-            .setWallpaperCategoryHash(getIdHashCode(collectionId))
-            .log()
-    }
-
     override fun logSnapshot() {
         SysUiStatsLogger(StyleEnums.SNAPSHOT)
             .setWallpaperCategoryHash(preferences.getHomeCategoryHash())
@@ -59,6 +48,12 @@
             .log()
     }
 
+    override fun logAppLaunched(launchSource: Intent) {
+        SysUiStatsLogger(StyleEnums.APP_LAUNCHED)
+            .setLaunchedPreference(launchSource.getAppLaunchSource())
+            .log()
+    }
+
     override fun logWallpaperApplied(
         collectionId: String?,
         wallpaperId: String?,
@@ -114,20 +109,69 @@
             .log()
     }
 
-    override fun logColorApplied(action: Int, colorOption: ColorOption) {
-        SysUiStatsLogger(action)
+    override fun logResetApplied() {
+        SysUiStatsLogger(StyleEnums.RESET_APPLIED).log()
+    }
+
+    override fun logWallpaperExploreButtonClicked() {
+        SysUiStatsLogger(StyleEnums.WALLPAPER_EXPLORE).log()
+    }
+
+    override fun logThemeColorApplied(colorOption: ColorOption) {
+        SysUiStatsLogger(StyleEnums.THEME_COLOR_APPLIED)
             .setColorPreference(colorOption.index)
             .setColorVariant(colorOption.style.ordinal + 1)
             .log()
     }
 
     override fun logGridApplied(grid: GridOption) {
-        SysUiStatsLogger(StyleEnums.PICKER_APPLIED).setLauncherGrid(grid.cols).log()
+        SysUiStatsLogger(StyleEnums.GRID_APPLIED).setLauncherGrid(grid.getLauncherGridInt()).log()
     }
 
-    private fun getAppLaunchSource(launchSource: Intent): Int {
-        return if (launchSource.hasExtra(LaunchSourceUtils.WALLPAPER_LAUNCH_SOURCE)) {
-            when (launchSource.getStringExtra(LaunchSourceUtils.WALLPAPER_LAUNCH_SOURCE)) {
+    override fun logClockApplied(clockId: String) {
+        SysUiStatsLogger(StyleEnums.CLOCK_APPLIED).setClockPackageHash(getIdHashCode(clockId)).log()
+    }
+
+    override fun logClockColorApplied(seedColor: Int) {
+        SysUiStatsLogger(StyleEnums.CLOCK_COLOR_APPLIED).setSeedColor(seedColor).log()
+    }
+
+    override fun logClockSizeApplied(@ClockSize clockSize: Int) {
+        SysUiStatsLogger(StyleEnums.CLOCK_SIZE_APPLIED).setClockSize(clockSize).log()
+    }
+
+    override fun logThemedIconApplied(useThemeIcon: Boolean) {
+        SysUiStatsLogger(StyleEnums.THEMED_ICON_APPLIED).setToggleOn(useThemeIcon).log()
+    }
+
+    override fun logLockScreenNotificationApplied(showLockScreenNotifications: Boolean) {
+        SysUiStatsLogger(StyleEnums.LOCK_SCREEN_NOTIFICATION_APPLIED)
+            .setToggleOn(showLockScreenNotifications)
+            .log()
+    }
+
+    override fun logShortcutApplied(shortcut: String, shortcutSlotId: String) {
+        SysUiStatsLogger(StyleEnums.SHORTCUT_APPLIED)
+            .setShortcut(shortcut)
+            .setShortcutSlotId(shortcutSlotId)
+            .log()
+    }
+
+    override fun logDarkThemeApplied(useDarkTheme: Boolean) {
+        SysUiStatsLogger(StyleEnums.DARK_THEME_APPLIED).setToggleOn(useDarkTheme).log()
+    }
+
+    /**
+     * The grid integer depends on the column and row numbers. For example: 4x5 is 405 13x37 is 1337
+     * The upper limit for the column / row count is 99.
+     */
+    private fun GridOption.getLauncherGridInt(): Int {
+        return cols * 100 + rows
+    }
+
+    private fun Intent.getAppLaunchSource(): Int {
+        return if (hasExtra(LaunchSourceUtils.WALLPAPER_LAUNCH_SOURCE)) {
+            when (getStringExtra(LaunchSourceUtils.WALLPAPER_LAUNCH_SOURCE)) {
                 LaunchSourceUtils.LAUNCH_SOURCE_LAUNCHER ->
                     SysUiStatsLog.STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_LAUNCHER
                 LaunchSourceUtils.LAUNCH_SOURCE_SETTINGS ->
@@ -142,17 +186,11 @@
                     SysUiStatsLog
                         .STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_PREFERENCE_UNSPECIFIED
             }
-        } else if (launchSource.hasExtra(LaunchSourceUtils.LAUNCH_SETTINGS_SEARCH)) {
+        } else if (hasExtra(LaunchSourceUtils.LAUNCH_SETTINGS_SEARCH)) {
             SysUiStatsLog.STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_SETTINGS_SEARCH
-        } else if (
-            launchSource.action != null &&
-                launchSource.action == WallpaperManager.ACTION_CROP_AND_SET_WALLPAPER
-        ) {
+        } else if (action != null && action == WallpaperManager.ACTION_CROP_AND_SET_WALLPAPER) {
             SysUiStatsLog.STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_CROP_AND_SET_ACTION
-        } else if (
-            launchSource.categories != null &&
-                launchSource.categories.contains(Intent.CATEGORY_LAUNCHER)
-        ) {
+        } else if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
             SysUiStatsLog.STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_LAUNCH_ICON
         } else {
             SysUiStatsLog.STYLE_UICHANGED__LAUNCHED_PREFERENCE__LAUNCHED_PREFERENCE_UNSPECIFIED
diff --git a/src/com/android/customization/module/logging/ThemesUserEventLogger.kt b/src/com/android/customization/module/logging/ThemesUserEventLogger.kt
index 4fd5334..1210343 100644
--- a/src/com/android/customization/module/logging/ThemesUserEventLogger.kt
+++ b/src/com/android/customization/module/logging/ThemesUserEventLogger.kt
@@ -15,19 +15,38 @@
  */
 package com.android.customization.module.logging
 
+import android.stats.style.StyleEnums
+import androidx.annotation.IntDef
 import com.android.customization.model.color.ColorOption
 import com.android.customization.model.grid.GridOption
 import com.android.wallpaper.module.logging.UserEventLogger
 
 /** Extension of [UserEventLogger] that adds ThemePicker specific events. */
 interface ThemesUserEventLogger : UserEventLogger {
-    /**
-     * Logs the color usage while color is applied.
-     *
-     * @param action color applied action.
-     * @param colorOption applied color option.
-     */
-    fun logColorApplied(action: Int, colorOption: ColorOption)
+
+    fun logThemeColorApplied(colorOption: ColorOption)
 
     fun logGridApplied(grid: GridOption)
+
+    fun logClockApplied(clockId: String)
+
+    fun logClockColorApplied(seedColor: Int)
+
+    fun logClockSizeApplied(@ClockSize clockSize: Int)
+
+    fun logThemedIconApplied(useThemeIcon: Boolean)
+
+    fun logLockScreenNotificationApplied(showLockScreenNotifications: Boolean)
+
+    fun logShortcutApplied(shortcut: String, shortcutSlotId: String)
+
+    fun logDarkThemeApplied(useDarkTheme: Boolean)
+
+    @IntDef(
+        StyleEnums.CLOCK_SIZE_UNSPECIFIED,
+        StyleEnums.CLOCK_SIZE_DYNAMIC,
+        StyleEnums.CLOCK_SIZE_SMALL,
+    )
+    @Retention(AnnotationRetention.SOURCE)
+    annotation class ClockSize
 }
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
index 3f6f423..1433e98 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
@@ -15,226 +15,38 @@
  */
 package com.android.customization.picker.clock.ui.view
 
-import android.app.WallpaperColors
-import android.app.WallpaperManager
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Point
-import android.graphics.Rect
 import android.view.View
-import android.widget.FrameLayout
 import androidx.annotation.ColorInt
-import androidx.core.text.util.LocalePreferences
 import androidx.lifecycle.LifecycleOwner
 import com.android.systemui.plugins.ClockController
-import com.android.systemui.plugins.WeatherData
-import com.android.systemui.shared.clocks.ClockRegistry
-import com.android.wallpaper.R
-import com.android.wallpaper.util.TimeUtils.TimeTicker
-import java.util.concurrent.ConcurrentHashMap
 
-/**
- * Provide reusable clock view and related util functions.
- *
- * @property screenSize The Activity or Fragment's window size.
- */
-class ClockViewFactory(
-    private val appContext: Context,
-    val screenSize: Point,
-    private val wallpaperManager: WallpaperManager,
-    private val registry: ClockRegistry,
-) {
-    private val resources = appContext.resources
-    private val timeTickListeners: ConcurrentHashMap<Int, TimeTicker> = ConcurrentHashMap()
-    private val clockControllers: HashMap<String, ClockController> = HashMap()
-    private val smallClockFrames: HashMap<String, FrameLayout> = HashMap()
+interface ClockViewFactory {
 
-    fun getController(clockId: String): ClockController {
-        return clockControllers[clockId]
-            ?: initClockController(clockId).also { clockControllers[clockId] = it }
-    }
+    fun getController(clockId: String): ClockController
 
     /**
      * Reset the large view to its initial state when getting the view. This is because some view
      * configs, e.g. animation state, might change during the reuse of the clock view in the app.
      */
-    fun getLargeView(clockId: String): View {
-        return getController(clockId).largeClock.let {
-            it.animations.onPickerCarouselSwiping(1F)
-            it.view
-        }
-    }
+    fun getLargeView(clockId: String): View
 
     /**
      * Reset the small view to its initial state when getting the view. This is because some view
      * configs, e.g. translation X, might change during the reuse of the clock view in the app.
      */
-    fun getSmallView(clockId: String): View {
-        val smallClockFrame =
-            smallClockFrames[clockId]
-                ?: createSmallClockFrame().also {
-                    it.addView(getController(clockId).smallClock.view)
-                    smallClockFrames[clockId] = it
-                }
-        smallClockFrame.translationX = 0F
-        smallClockFrame.translationY = 0F
-        return smallClockFrame
-    }
+    fun getSmallView(clockId: String): View
 
-    private fun createSmallClockFrame(): FrameLayout {
-        val smallClockFrame = FrameLayout(appContext)
-        val layoutParams =
-            FrameLayout.LayoutParams(
-                FrameLayout.LayoutParams.WRAP_CONTENT,
-                resources.getDimensionPixelSize(R.dimen.small_clock_height)
-            )
-        layoutParams.topMargin = getSmallClockTopMargin()
-        layoutParams.marginStart = getSmallClockStartPadding()
-        smallClockFrame.layoutParams = layoutParams
-        smallClockFrame.clipChildren = false
-        return smallClockFrame
-    }
+    fun updateColorForAllClocks(@ColorInt seedColor: Int?)
 
-    private fun getSmallClockTopMargin() =
-        getStatusBarHeight(appContext.resources) +
-            appContext.resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+    fun updateColor(clockId: String, @ColorInt seedColor: Int?)
 
-    private fun getSmallClockStartPadding() =
-        appContext.resources.getDimensionPixelSize(R.dimen.clock_padding_start)
+    fun updateRegionDarkness()
 
-    fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
-        clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
-    }
+    fun updateTimeFormat(clockId: String)
 
-    fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
-        clockControllers[clockId]?.events?.onSeedColorChanged(seedColor)
-    }
+    fun registerTimeTicker(owner: LifecycleOwner)
 
-    fun updateRegionDarkness() {
-        val isRegionDark = isLockscreenWallpaperDark()
-        clockControllers.values.forEach {
-            it.largeClock.events.onRegionDarknessChanged(isRegionDark)
-            it.smallClock.events.onRegionDarknessChanged(isRegionDark)
-        }
-    }
+    fun onDestroy()
 
-    private fun isLockscreenWallpaperDark(): Boolean {
-        val colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK)
-        return (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0
-    }
-
-    fun updateTimeFormat(clockId: String) {
-        getController(clockId)
-            .events
-            .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
-    }
-
-    fun registerTimeTicker(owner: LifecycleOwner) {
-        val hashCode = owner.hashCode()
-        if (timeTickListeners.keys.contains(hashCode)) {
-            return
-        }
-
-        timeTickListeners[hashCode] = TimeTicker.registerNewReceiver(appContext) { onTimeTick() }
-    }
-
-    fun onDestroy() {
-        timeTickListeners.forEach { (_, timeTicker) -> appContext.unregisterReceiver(timeTicker) }
-        timeTickListeners.clear()
-        clockControllers.clear()
-        smallClockFrames.clear()
-    }
-
-    private fun onTimeTick() {
-        clockControllers.values.forEach {
-            it.largeClock.events.onTimeTick()
-            it.smallClock.events.onTimeTick()
-        }
-    }
-
-    fun unregisterTimeTicker(owner: LifecycleOwner) {
-        val hashCode = owner.hashCode()
-        timeTickListeners[hashCode]?.let {
-            appContext.unregisterReceiver(it)
-            timeTickListeners.remove(hashCode)
-        }
-    }
-
-    private fun initClockController(clockId: String): ClockController {
-        val controller =
-            registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) }
-        checkNotNull(controller)
-
-        val isWallpaperDark = isLockscreenWallpaperDark()
-        // Initialize large clock
-        controller.largeClock.events.onRegionDarknessChanged(isWallpaperDark)
-        controller.largeClock.events.onFontSettingChanged(
-            resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
-        )
-        controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
-
-        // Initialize small clock
-        controller.smallClock.events.onRegionDarknessChanged(isWallpaperDark)
-        controller.smallClock.events.onFontSettingChanged(
-            resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
-        )
-        controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion())
-
-        // Use placeholder for weather clock preview in picker.
-        // Use locale default temp unit since assistant default is not available in this context.
-        val useCelsius =
-            LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS
-        controller.events.onWeatherDataChanged(
-            WeatherData(
-                description = DESCRIPTION_PLACEHODLER,
-                state = WEATHERICON_PLACEHOLDER,
-                temperature =
-                    if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER
-                    else TEMPERATURE_FAHRENHEIT_PLACEHOLDER,
-                useCelsius = useCelsius,
-            )
-        )
-        return controller
-    }
-
-    /**
-     * Simulate the function of getLargeClockRegion in KeyguardClockSwitch so that we can get a
-     * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
-     * and position the clock view
-     */
-    private fun getLargeClockRegion(): Rect {
-        val largeClockTopMargin =
-            resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
-        val targetHeight = resources.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2
-        val top = (screenSize.y / 2 - targetHeight / 2 + largeClockTopMargin / 2)
-        return Rect(0, top, screenSize.x, (top + targetHeight))
-    }
-
-    /**
-     * Simulate the function of getSmallClockRegion in KeyguardClockSwitch so that we can get a
-     * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
-     * and position the clock view
-     */
-    private fun getSmallClockRegion(): Rect {
-        val topMargin = getSmallClockTopMargin()
-        val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height)
-        return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight)
-    }
-
-    companion object {
-        const val DESCRIPTION_PLACEHODLER = ""
-        const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58
-        const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21
-        val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
-        const val USE_CELSIUS_PLACEHODLER = false
-
-        private fun getStatusBarHeight(resource: Resources): Int {
-            var result = 0
-            val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
-            if (resourceId > 0) {
-                result = resource.getDimensionPixelSize(resourceId)
-            }
-            return result
-        }
-    }
+    fun unregisterTimeTicker(owner: LifecycleOwner)
 }
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt
new file mode 100644
index 0000000..9116f3f
--- /dev/null
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactoryImpl.kt
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.customization.picker.clock.ui.view
+
+import android.app.WallpaperColors
+import android.app.WallpaperManager
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.ColorInt
+import androidx.core.text.util.LocalePreferences
+import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.WeatherData
+import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.R
+import com.android.wallpaper.util.TimeUtils.TimeTicker
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Provide reusable clock view and related util functions.
+ *
+ * @property screenSize The Activity or Fragment's window size.
+ */
+class ClockViewFactoryImpl(
+    private val appContext: Context,
+    val screenSize: Point,
+    private val wallpaperManager: WallpaperManager,
+    private val registry: ClockRegistry,
+) : ClockViewFactory {
+    private val resources = appContext.resources
+    private val timeTickListeners: ConcurrentHashMap<Int, TimeTicker> = ConcurrentHashMap()
+    private val clockControllers: HashMap<String, ClockController> = HashMap()
+    private val smallClockFrames: HashMap<String, FrameLayout> = HashMap()
+
+    override fun getController(clockId: String): ClockController {
+        return clockControllers[clockId]
+            ?: initClockController(clockId).also { clockControllers[clockId] = it }
+    }
+
+    /**
+     * Reset the large view to its initial state when getting the view. This is because some view
+     * configs, e.g. animation state, might change during the reuse of the clock view in the app.
+     */
+    override fun getLargeView(clockId: String): View {
+        return getController(clockId).largeClock.let {
+            it.animations.onPickerCarouselSwiping(1F)
+            it.view
+        }
+    }
+
+    /**
+     * Reset the small view to its initial state when getting the view. This is because some view
+     * configs, e.g. translation X, might change during the reuse of the clock view in the app.
+     */
+    override fun getSmallView(clockId: String): View {
+        val smallClockFrame =
+            smallClockFrames[clockId]
+                ?: createSmallClockFrame().also {
+                    it.addView(getController(clockId).smallClock.view)
+                    smallClockFrames[clockId] = it
+                }
+        smallClockFrame.translationX = 0F
+        smallClockFrame.translationY = 0F
+        return smallClockFrame
+    }
+
+    private fun createSmallClockFrame(): FrameLayout {
+        val smallClockFrame = FrameLayout(appContext)
+        val layoutParams =
+            FrameLayout.LayoutParams(
+                FrameLayout.LayoutParams.WRAP_CONTENT,
+                resources.getDimensionPixelSize(R.dimen.small_clock_height)
+            )
+        layoutParams.topMargin = getSmallClockTopMargin()
+        layoutParams.marginStart = getSmallClockStartPadding()
+        smallClockFrame.layoutParams = layoutParams
+        smallClockFrame.clipChildren = false
+        return smallClockFrame
+    }
+
+    private fun getSmallClockTopMargin() =
+        getStatusBarHeight(appContext.resources) +
+            appContext.resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+
+    private fun getSmallClockStartPadding() =
+        appContext.resources.getDimensionPixelSize(R.dimen.clock_padding_start)
+
+    override fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
+        clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
+    }
+
+    override fun updateColor(clockId: String, @ColorInt seedColor: Int?) {
+        clockControllers[clockId]?.events?.onSeedColorChanged(seedColor)
+    }
+
+    override fun updateRegionDarkness() {
+        val isRegionDark = isLockscreenWallpaperDark()
+        clockControllers.values.forEach {
+            it.largeClock.events.onRegionDarknessChanged(isRegionDark)
+            it.smallClock.events.onRegionDarknessChanged(isRegionDark)
+        }
+    }
+
+    private fun isLockscreenWallpaperDark(): Boolean {
+        val colors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK)
+        return (colors?.colorHints?.and(WallpaperColors.HINT_SUPPORTS_DARK_TEXT)) == 0
+    }
+
+    override fun updateTimeFormat(clockId: String) {
+        getController(clockId)
+            .events
+            .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
+    }
+
+    override fun registerTimeTicker(owner: LifecycleOwner) {
+        val hashCode = owner.hashCode()
+        if (timeTickListeners.keys.contains(hashCode)) {
+            return
+        }
+
+        timeTickListeners[hashCode] = TimeTicker.registerNewReceiver(appContext) { onTimeTick() }
+    }
+
+    override fun onDestroy() {
+        timeTickListeners.forEach { (_, timeTicker) -> appContext.unregisterReceiver(timeTicker) }
+        timeTickListeners.clear()
+        clockControllers.clear()
+        smallClockFrames.clear()
+    }
+
+    private fun onTimeTick() {
+        clockControllers.values.forEach {
+            it.largeClock.events.onTimeTick()
+            it.smallClock.events.onTimeTick()
+        }
+    }
+
+    override fun unregisterTimeTicker(owner: LifecycleOwner) {
+        val hashCode = owner.hashCode()
+        timeTickListeners[hashCode]?.let {
+            appContext.unregisterReceiver(it)
+            timeTickListeners.remove(hashCode)
+        }
+    }
+
+    private fun initClockController(clockId: String): ClockController {
+        val controller =
+            registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) }
+        checkNotNull(controller)
+
+        val isWallpaperDark = isLockscreenWallpaperDark()
+        // Initialize large clock
+        controller.largeClock.events.onRegionDarknessChanged(isWallpaperDark)
+        controller.largeClock.events.onFontSettingChanged(
+            resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
+        )
+        controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
+
+        // Initialize small clock
+        controller.smallClock.events.onRegionDarknessChanged(isWallpaperDark)
+        controller.smallClock.events.onFontSettingChanged(
+            resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
+        )
+        controller.smallClock.events.onTargetRegionChanged(getSmallClockRegion())
+
+        // Use placeholder for weather clock preview in picker.
+        // Use locale default temp unit since assistant default is not available in this context.
+        val useCelsius =
+            LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS
+        controller.events.onWeatherDataChanged(
+            WeatherData(
+                description = DESCRIPTION_PLACEHODLER,
+                state = WEATHERICON_PLACEHOLDER,
+                temperature =
+                    if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER
+                    else TEMPERATURE_FAHRENHEIT_PLACEHOLDER,
+                useCelsius = useCelsius,
+            )
+        )
+        return controller
+    }
+
+    /**
+     * Simulate the function of getLargeClockRegion in KeyguardClockSwitch so that we can get a
+     * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
+     * and position the clock view
+     */
+    private fun getLargeClockRegion(): Rect {
+        val largeClockTopMargin =
+            resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
+        val targetHeight = resources.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2
+        val top = (screenSize.y / 2 - targetHeight / 2 + largeClockTopMargin / 2)
+        return Rect(0, top, screenSize.x, (top + targetHeight))
+    }
+
+    /**
+     * Simulate the function of getSmallClockRegion in KeyguardClockSwitch so that we can get a
+     * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
+     * and position the clock view
+     */
+    private fun getSmallClockRegion(): Rect {
+        val topMargin = getSmallClockTopMargin()
+        val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height)
+        return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight)
+    }
+
+    companion object {
+        const val DESCRIPTION_PLACEHODLER = ""
+        const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58
+        const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21
+        val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
+        const val USE_CELSIUS_PLACEHODLER = false
+
+        private fun getStatusBarHeight(resource: Resources): Int {
+            var result = 0
+            val resourceId: Int = resource.getIdentifier("status_bar_height", "dimen", "android")
+            if (resourceId > 0) {
+                result = resource.getDimensionPixelSize(resourceId)
+            }
+            return result
+        }
+    }
+}
diff --git a/tests/common/src/com/android/customization/testing/TestThemesUserEventLogger.kt b/tests/common/src/com/android/customization/testing/TestThemesUserEventLogger.kt
index 2236b62..8b14688 100644
--- a/tests/common/src/com/android/customization/testing/TestThemesUserEventLogger.kt
+++ b/tests/common/src/com/android/customization/testing/TestThemesUserEventLogger.kt
@@ -23,7 +23,21 @@
 /** Test implementation of [ThemesUserEventLogger]. */
 class TestThemesUserEventLogger : TestUserEventLogger(), ThemesUserEventLogger {
 
-    override fun logColorApplied(action: Int, colorOption: ColorOption) {}
+    override fun logThemeColorApplied(colorOption: ColorOption) {}
 
     override fun logGridApplied(grid: GridOption) {}
+
+    override fun logClockApplied(clockId: String) {}
+
+    override fun logClockColorApplied(seedColor: Int) {}
+
+    override fun logClockSizeApplied(clockSize: Int) {}
+
+    override fun logThemedIconApplied(useThemeIcon: Boolean) {}
+
+    override fun logLockScreenNotificationApplied(showLockScreenNotifications: Boolean) {}
+
+    override fun logShortcutApplied(shortcut: String, shortcutSlotId: String) {}
+
+    override fun logDarkThemeApplied(useDarkTheme: Boolean) {}
 }
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
new file mode 100644
index 0000000..31999fb
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/picker/clock/ui/FakeClockViewFactory.kt
@@ -0,0 +1,52 @@
+package com.android.customization.picker.clock.ui
+
+import android.view.View
+import androidx.lifecycle.LifecycleOwner
+import com.android.customization.picker.clock.ui.view.ClockViewFactory
+import com.android.systemui.plugins.ClockController
+
+/**
+ * This is a fake [ClockViewFactory]. Only implement the function if it's actually called in a test.
+ */
+class FakeClockViewFactory : ClockViewFactory {
+
+    override fun getController(clockId: String): ClockController {
+        TODO("Not yet implemented")
+    }
+
+    override fun getLargeView(clockId: String): View {
+        TODO("Not yet implemented")
+    }
+
+    override fun getSmallView(clockId: String): View {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateColorForAllClocks(seedColor: Int?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateColor(clockId: String, seedColor: Int?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateRegionDarkness() {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateTimeFormat(clockId: String) {
+        TODO("Not yet implemented")
+    }
+
+    override fun registerTimeTicker(owner: LifecycleOwner) {
+        TODO("Not yet implemented")
+    }
+
+    override fun onDestroy() {
+        TODO("Not yet implemented")
+    }
+
+    override fun unregisterTimeTicker(owner: LifecycleOwner) {
+        TODO("Not yet implemented")
+    }
+}