Introduce small clock on the settings screen

This is to introduce the small clock on the settings preview.

Test: Manually tested that the small clock shows correctly. See bug
Bug: 274927017
Change-Id: I6b7a741c06e0da2d97dd57d7d6c263cb69fd20b6
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 1ef739b..16b5de7 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -176,6 +176,10 @@
     copied from sysui resources
     -->
     <dimen name="keyguard_large_clock_top_margin">-60dp</dimen>
+    <!-- Dimension for the clock view, copied from sysui resources. -->
+    <dimen name="small_clock_height">114dp</dimen>
+    <dimen name="small_clock_padding_top">28dp</dimen>
+    <dimen name="clock_padding_start">28dp</dimen>
 
     <dimen name="tab_touch_delegate_height_padding">8dp</dimen>
 </resources>
diff --git a/src/com/android/customization/module/CustomizationInjector.kt b/src/com/android/customization/module/CustomizationInjector.kt
index c0e4124..dbcff27 100644
--- a/src/com/android/customization/module/CustomizationInjector.kt
+++ b/src/com/android/customization/module/CustomizationInjector.kt
@@ -15,8 +15,8 @@
  */
 package com.android.customization.module
 
-import android.app.Activity
 import android.content.Context
+import androidx.activity.ComponentActivity
 import androidx.fragment.app.FragmentActivity
 import com.android.customization.model.theme.OverlayManagerCompat
 import com.android.customization.model.theme.ThemeBundleProvider
@@ -67,7 +67,7 @@
         interactor: ClockPickerInteractor,
     ): ClockCarouselViewModel.Factory
 
-    fun getClockViewFactory(activity: Activity): ClockViewFactory
+    fun getClockViewFactory(activity: ComponentActivity): ClockViewFactory
 
     fun getClockSettingsViewModelFactory(
         context: Context,
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 7f27650..93856b6 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -15,7 +15,6 @@
  */
 package com.android.customization.module
 
-import android.app.Activity
 import android.app.UiModeManager
 import android.content.Context
 import android.content.Intent
@@ -25,6 +24,8 @@
 import androidx.activity.ComponentActivity
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.get
 import com.android.customization.model.color.ColorCustomizationManager
@@ -80,6 +81,7 @@
 import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository
 import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
 import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer
+import com.android.wallpaper.util.ScreenSizeCalculator
 import kotlinx.coroutines.Dispatchers
 
 open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInjector {
@@ -101,7 +103,7 @@
     private var clockPickerInteractor: ClockPickerInteractor? = null
     private var clockSectionViewModel: ClockSectionViewModel? = null
     private var clockCarouselViewModelFactory: ClockCarouselViewModel.Factory? = null
-    private var clockViewFactory: ClockViewFactory? = null
+    private var clockViewFactories: MutableMap<Int, ClockViewFactory> = HashMap()
     private var notificationsInteractor: NotificationsInteractor? = null
     private var notificationSectionViewModelFactory: NotificationSectionViewModel.Factory? = null
     private var colorPickerInteractor: ColorPickerInteractor? = null
@@ -356,9 +358,29 @@
             }
     }
 
-    override fun getClockViewFactory(activity: Activity): ClockViewFactory {
-        return clockViewFactory
-            ?: ClockViewFactory(activity, getClockRegistry(activity)).also { clockViewFactory = it }
+    override fun getClockViewFactory(activity: ComponentActivity): ClockViewFactory {
+        val activityHashCode = activity.hashCode()
+        return clockViewFactories[activityHashCode]
+            ?: ClockViewFactory(
+                    activity.applicationContext,
+                    ScreenSizeCalculator.getInstance()
+                        .getScreenSize(activity.windowManager.defaultDisplay),
+                    getClockRegistry(
+                        activity.applicationContext,
+                    ),
+                )
+                .also {
+                    clockViewFactories[activityHashCode] = it
+                    activity.lifecycle.addObserver(
+                        object : DefaultLifecycleObserver {
+                            override fun onDestroy(owner: LifecycleOwner) {
+                                super.onDestroy(owner)
+                                clockViewFactories[activityHashCode]?.onDestroy()
+                                clockViewFactories.remove(activityHashCode)
+                            }
+                        }
+                    )
+                }
     }
 
     private fun getNotificationsInteractor(
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index c95561c..5323cf4 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -91,7 +91,7 @@
                 launch {
                     viewModel.clockId.collect { clockId ->
                         singleClockHostView.removeAllViews()
-                        val clockView = clockViewFactory.getView(clockId)
+                        val clockView = clockViewFactory.getLargeView(clockId)
                         // The clock view might still be attached to an existing parent. Detach
                         // before adding to another parent.
                         (clockView.parent as? ViewGroup)?.removeView(clockView)
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
index 4a8ebeb..671a7ae 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
@@ -21,6 +21,7 @@
 import android.view.ViewGroup
 import android.widget.LinearLayout
 import android.widget.SeekBar
+import androidx.core.view.doOnPreDraw
 import androidx.core.view.isInvisible
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
@@ -40,6 +41,7 @@
 import com.android.customization.picker.common.ui.view.ItemSpacing
 import com.android.wallpaper.R
 import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.launch
 
@@ -91,17 +93,6 @@
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
-                    viewModel.selectedClockId
-                        .mapNotNull { it }
-                        .collect { clockId ->
-                            val clockView = clockViewFactory.getView(clockId)
-                            (clockView.parent as? ViewGroup)?.removeView(clockView)
-                            clockHostView.removeAllViews()
-                            clockHostView.addView(clockView)
-                        }
-                }
-
-                launch {
                     viewModel.seedColor.collect { seedColor ->
                         viewModel.selectedClockId.value?.let { selectedClockId ->
                             clockViewFactory.updateColor(selectedClockId, seedColor)
@@ -176,18 +167,41 @@
                 }
 
                 launch {
-                    viewModel.selectedClockSize.collect { size ->
-                        when (size) {
-                            ClockSize.DYNAMIC -> {
-                                sizeOptions.radioButtonDynamic.isChecked = true
-                                sizeOptions.radioButtonSmall.isChecked = false
-                            }
-                            ClockSize.SMALL -> {
-                                sizeOptions.radioButtonDynamic.isChecked = false
-                                sizeOptions.radioButtonSmall.isChecked = true
+                    combine(
+                            viewModel.selectedClockId.mapNotNull { it },
+                            viewModel.selectedClockSize,
+                            ::Pair,
+                        )
+                        .collect { (clockId, size) ->
+                            val clockView =
+                                if (size == ClockSize.DYNAMIC) {
+                                    clockViewFactory.getLargeView(clockId)
+                                } else {
+                                    clockViewFactory.getSmallView(clockId)
+                                }
+                            (clockView.parent as? ViewGroup)?.removeView(clockView)
+                            clockHostView.removeAllViews()
+                            clockHostView.addView(clockView)
+
+                            when (size) {
+                                ClockSize.DYNAMIC -> {
+                                    sizeOptions.radioButtonDynamic.isChecked = true
+                                    sizeOptions.radioButtonSmall.isChecked = false
+                                    clockHostView.doOnPreDraw {
+                                        it.pivotX = (it.width / 2).toFloat()
+                                        it.pivotY = (it.height / 2).toFloat()
+                                    }
+                                }
+                                ClockSize.SMALL -> {
+                                    sizeOptions.radioButtonDynamic.isChecked = false
+                                    sizeOptions.radioButtonSmall.isChecked = true
+                                    clockHostView.doOnPreDraw {
+                                        it.pivotX = 0F
+                                        it.pivotY = 0F
+                                    }
+                                }
                             }
                         }
-                    }
                 }
 
                 launch {
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 d085b7b..cfc05dc 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
@@ -15,35 +15,76 @@
  */
 package com.android.customization.picker.clock.ui.view
 
-import android.app.Activity
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
 import android.graphics.Rect
 import android.util.TypedValue
 import android.view.View
+import android.widget.FrameLayout
 import androidx.annotation.ColorInt
 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.ScreenSizeCalculator
 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 activity: Activity,
+    private val appContext: Context,
+    val screenSize: Point,
     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()
 
     fun getController(clockId: String): ClockController {
-        return clockControllers[clockId] ?: initClockController(clockId)
+        return clockControllers[clockId]
+            ?: initClockController(clockId).also { clockControllers[clockId] = it }
     }
 
-    fun getView(clockId: String): View {
+    fun getLargeView(clockId: String): View {
         return getController(clockId).largeClock.view
     }
 
+    fun getSmallView(clockId: String): View {
+        return smallClockFrames[clockId]
+            ?: createSmallClockFrame().also {
+                it.addView(getController(clockId).smallClock.view)
+                smallClockFrames[clockId] = it
+            }
+    }
+
+    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 =
+            getStatusBarHeight(resources) +
+                resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+        smallClockFrame.layoutParams = layoutParams
+
+        smallClockFrame.setPaddingRelative(
+            resources.getDimensionPixelSize(R.dimen.clock_padding_start),
+            0,
+            0,
+            0
+        )
+        smallClockFrame.clipChildren = false
+        return smallClockFrame
+    }
+
     fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
         clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
     }
@@ -57,7 +98,7 @@
     fun updateTimeFormat(clockId: String) {
         getController(clockId)
             .events
-            .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(activity))
+            .onTimeFormatChanged(android.text.format.DateFormat.is24HourFormat(appContext))
     }
 
     fun registerTimeTicker(owner: LifecycleOwner) {
@@ -66,37 +107,53 @@
             return
         }
 
-        timeTickListeners[hashCode] =
-            TimeTicker.registerNewReceiver(activity.applicationContext) { onTimeTick() }
+        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() }
+        clockControllers.values.forEach {
+            it.largeClock.events.onTimeTick()
+            it.smallClock.events.onTimeTick()
+        }
     }
 
     fun unregisterTimeTicker(owner: LifecycleOwner) {
         val hashCode = owner.hashCode()
         timeTickListeners[hashCode]?.let {
-            activity.applicationContext.unregisterReceiver(it)
+            appContext.unregisterReceiver(it)
             timeTickListeners.remove(hashCode)
         }
     }
 
     private fun initClockController(clockId: String): ClockController {
         val controller =
-            registry.createExampleClock(clockId).also { it?.initialize(activity.resources, 0f, 0f) }
+            registry.createExampleClock(clockId).also { it?.initialize(resources, 0f, 0f) }
         checkNotNull(controller)
 
         // Configure light/dark theme
         val isLightTheme = TypedValue()
-        activity.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
+        appContext.theme.resolveAttribute(android.R.attr.isLightTheme, isLightTheme, true)
         val isRegionDark = isLightTheme.data == 0
         controller.largeClock.events.onRegionDarknessChanged(isRegionDark)
         // Configure font size
         controller.largeClock.events.onFontSettingChanged(
-            activity.resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
+            resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
         )
         controller.largeClock.events.onTargetRegionChanged(getLargeClockRegion())
+
+        controller.smallClock.events.onRegionDarknessChanged(isRegionDark)
+        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
         controller.events.onWeatherDataChanged(
             WeatherData(
@@ -106,7 +163,6 @@
                 useCelsius = USE_CELSIUS_PLACEHODLER,
             )
         )
-        clockControllers[clockId] = controller
         return controller
     }
 
@@ -115,21 +171,41 @@
      * proper region corresponding to lock screen in picker and for onTargetRegionChanged to scale
      * and position the clock view
      */
-    fun getLargeClockRegion(): Rect {
-        val screenSizeCalculator = ScreenSizeCalculator.getInstance()
-        val screenSize = screenSizeCalculator.getScreenSize(activity.windowManager.defaultDisplay)
+    private fun getLargeClockRegion(): Rect {
         val largeClockTopMargin =
-            activity.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
-        val targetHeight =
-            activity.resources.getDimensionPixelSize(R.dimen.large_clock_text_size) * 2
+            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 =
+            getStatusBarHeight(resources) +
+                resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+        val start = resources.getDimensionPixelSize(R.dimen.clock_padding_start)
+        val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height)
+        return Rect(start, topMargin, screenSize.x, topMargin + targetHeight)
+    }
+
     companion object {
-        val DESCRIPTION_PLACEHODLER = ""
-        val TEMPERATURE_PLACEHOLDER = 58
+        const val DESCRIPTION_PLACEHODLER = ""
+        const val TEMPERATURE_PLACEHOLDER = 58
         val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
-        val USE_CELSIUS_PLACEHODLER = false
+        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
+        }
     }
 }