DefaultClockProvider implementation using AnimatableClockView

Bug: 229771520
Test: Automated
Change-Id: Iba6c81f8b469ef2d0a5bc38dd89454d75bfe8a43
(cherry picked from commit c742a9cb68be1becc820fb79344c0b151965fae5)
Merged-In: Iba6c81f8b469ef2d0a5bc38dd89454d75bfe8a43
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8129716..18bd6b4 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -56,6 +56,9 @@
         "dagger2",
         "jsr330",
     ],
+    resource_dirs: [
+        "res",
+    ],
     java_version: "1.8",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
diff --git a/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml b/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml
new file mode 100644
index 0000000..be72d0b
--- /dev/null
+++ b/packages/SystemUI/shared/res/drawable/clock_default_thumbnail.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 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
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#FFFF00FF" />
+</shape>
diff --git a/packages/SystemUI/shared/res/layout/clock_default_large.xml b/packages/SystemUI/shared/res/layout/clock_default_large.xml
new file mode 100644
index 0000000..8510a0a
--- /dev/null
+++ b/packages/SystemUI/shared/res/layout/clock_default_large.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2022, 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.
+*/
+-->
+<com.android.systemui.shared.clocks.AnimatableClockView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/animatable_clock_view_large"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:gravity="center_horizontal"
+    android:textSize="@dimen/large_clock_text_size"
+    android:fontFamily="@*android:string/config_clockFontFamily"
+    android:typeface="monospace"
+    android:elegantTextHeight="false"
+    chargeAnimationDelay="200"
+    dozeWeight="200"
+    lockScreenWeight="400" />
diff --git a/packages/SystemUI/shared/res/layout/clock_default_small.xml b/packages/SystemUI/shared/res/layout/clock_default_small.xml
new file mode 100644
index 0000000..ec0e427
--- /dev/null
+++ b/packages/SystemUI/shared/res/layout/clock_default_small.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 2022, 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.
+*/
+-->
+<com.android.systemui.shared.clocks.AnimatableClockView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/animatable_clock_view"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="start"
+    android:gravity="start"
+    android:textSize="@dimen/small_clock_text_size"
+    android:fontFamily="@*android:string/config_clockFontFamily"
+    android:elegantTextHeight="false"
+    android:singleLine="true"
+    android:fontFeatureSettings="pnum"
+    chargeAnimationDelay="350"
+    dozeWeight="200"
+    lockScreenWeight="400" />
diff --git a/packages/SystemUI/shared/res/values/dimens.xml b/packages/SystemUI/shared/res/values/dimens.xml
new file mode 100644
index 0000000..8f90f0f
--- /dev/null
+++ b/packages/SystemUI/shared/res/values/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (c) 2022, 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.
+*/
+-->
+<resources>
+    <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
+    <dimen name="large_clock_text_size">150sp</dimen>
+    <dimen name="small_clock_text_size">86sp</dimen>
+
+    <!-- Default line spacing multiplier between hours and minutes of the keyguard clock -->
+    <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item>
+    <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock -->
+    <item name="keyguard_clock_line_spacing_scale_burmese" type="dimen" format="float">1</item>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 5b1a23d..2739d59 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -331,9 +331,9 @@
         )
     }
 
-    fun refreshFormat() {
+    fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+    fun refreshFormat(use24HourFormat: Boolean) {
         Patterns.update(context)
-        val use24HourFormat = DateFormat.is24HourFormat(context)
 
         format = when {
             isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index 2cac44a..a4c03b0 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -34,16 +34,23 @@
 
 private val TAG = ClockRegistry::class.simpleName
 private val DEBUG = true
-const val DEFAULT_CLOCK_ID = "DEFAULT"
 
 typealias ClockChangeListener = () -> Unit
 
 /** ClockRegistry aggregates providers and plugins */
-open class ClockRegistry @Inject constructor(
+open class ClockRegistry(
     val context: Context,
     val pluginManager: PluginManager,
-    @Main val handler: Handler
+    val handler: Handler,
+    defaultClockProvider: ClockProvider
 ) {
+    @Inject constructor(
+        context: Context,
+        pluginManager: PluginManager,
+        @Main handler: Handler,
+        defaultClockProvider: DefaultClockProvider
+    ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
+
     private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -53,60 +60,71 @@
     }
 
     private val pluginListener = object : PluginListener<ClockProviderPlugin> {
-        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) {
-            val currentId = currentClockId
-            for (clock in plugin.getClocks()) {
-                val id = clock.clockId
-                val current = availableClocks[id]
-                if (current != null) {
-                    Log.e(TAG, "Clock Id conflict: $id is registered by both " +
-                            "${plugin::class.simpleName} and ${current.provider::class.simpleName}")
-                    return
-                }
+        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
+            connectClocks(plugin)
 
-                availableClocks[id] = ClockInfo(clock, plugin)
-
-                if (currentId == id) {
-                    if (DEBUG) {
-                        Log.i(TAG, "Current clock ($currentId) was connected")
-                    }
-                    clockChangeListeners.forEach { it() }
-                }
-            }
-        }
-
-        override fun onPluginDisconnected(plugin: ClockProviderPlugin) {
-            val currentId = currentClockId
-            for (clock in plugin.getClocks()) {
-                availableClocks.remove(clock.clockId)
-
-                if (currentId == clock.clockId) {
-                    Log.w(TAG, "Current clock ($currentId) was disconnected")
-                    clockChangeListeners.forEach { it() }
-                }
-            }
-        }
+        override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
+            disconnectClocks(plugin)
     }
 
     open var currentClockId: ClockId
         get() {
-            val json = Settings.Secure.getString(context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE)
+            val json = Settings.Secure.getString(
+                context.contentResolver,
+                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+            )
             return gson.fromJson(json, ClockSetting::class.java).clockId
         }
         set(value) {
             val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
-            Settings.Secure.putString(context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json)
+            Settings.Secure.putString(
+                context.contentResolver,
+                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
+            )
         }
 
     init {
+        connectClocks(defaultClockProvider)
         pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java)
         context.contentResolver.registerContentObserver(
             Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
             false,
             settingObserver,
-            UserHandle.USER_ALL)
+            UserHandle.USER_ALL
+        )
+    }
+
+    private fun connectClocks(provider: ClockProvider) {
+        val currentId = currentClockId
+        for (clock in provider.getClocks()) {
+            val id = clock.clockId
+            val current = availableClocks[id]
+            if (current != null) {
+                Log.e(TAG, "Clock Id conflict: $id is registered by both " +
+                    "${provider::class.simpleName} and ${current.provider::class.simpleName}")
+                return
+            }
+
+            availableClocks[id] = ClockInfo(clock, provider)
+            if (currentId == id) {
+                if (DEBUG) {
+                    Log.i(TAG, "Current clock ($currentId) was connected")
+                }
+                clockChangeListeners.forEach { it() }
+            }
+        }
+    }
+
+    private fun disconnectClocks(provider: ClockProvider) {
+        val currentId = currentClockId
+        for (clock in provider.getClocks()) {
+            availableClocks.remove(clock.clockId)
+
+            if (currentId == clock.clockId) {
+                Log.w(TAG, "Current clock ($currentId) was disconnected")
+                clockChangeListeners.forEach { it() }
+            }
+        }
     }
 
     fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata }
@@ -148,4 +166,4 @@
         val clockId: ClockId,
         val _applied_timestamp: Long
     )
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
new file mode 100644
index 0000000..5d8da59
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2022 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.systemui.shared.clocks
+
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.icu.text.NumberFormat
+import android.util.TypedValue
+import android.view.LayoutInflater
+import com.android.internal.colorextraction.ColorExtractor
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockAnimation
+import com.android.systemui.plugins.ClockEvents
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.shared.R
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+import javax.inject.Inject
+
+private val TAG = DefaultClockProvider::class.simpleName
+const val DEFAULT_CLOCK_NAME = "Default Clock"
+const val DEFAULT_CLOCK_ID = "DEFAULT"
+
+/** Provides the default system clock */
+class DefaultClockProvider @Inject constructor(
+    val layoutInflater: LayoutInflater,
+    @Main val resources: Resources
+) : ClockProvider {
+    override fun getClocks(): List<ClockMetadata> =
+        listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
+
+    override fun createClock(id: ClockId): Clock {
+        if (id != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("$id is unsupported by $TAG")
+        }
+        return DefaultClock(layoutInflater, resources)
+    }
+
+    override fun getClockThumbnail(id: ClockId): Drawable? {
+        if (id != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("$id is unsupported by $TAG")
+        }
+
+        // TODO: Update placeholder to actual resource
+        return resources.getDrawable(R.drawable.clock_default_thumbnail, null)
+    }
+}
+
+/**
+ * Controls the default clock visuals.
+ *
+ * This serves as an adapter between the clock interface and the
+ * AnimatableClockView used by the existing lockscreen clock.
+ */
+class DefaultClock(
+    private val layoutInflater: LayoutInflater,
+    private val resources: Resources
+) : Clock {
+    override val smallClock =
+        layoutInflater.inflate(R.layout.clock_default_small, null) as AnimatableClockView
+    override val largeClock =
+        layoutInflater.inflate(R.layout.clock_default_large, null) as AnimatableClockView
+    private val clocks = listOf(smallClock, largeClock)
+
+    private val burmeseNf = NumberFormat.getInstance(Locale.forLanguageTag("my"))
+    private val burmeseNumerals = burmeseNf.format(FORMAT_NUMBER.toLong())
+    private val burmeseLineSpacing =
+        resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
+    private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
+
+    override val events = object : ClockEvents {
+        override fun onTimeTick() = clocks.forEach { it.refreshTime() }
+
+        override fun onTimeFormatChanged(is24Hr: Boolean) =
+            clocks.forEach { it.refreshFormat(is24Hr) }
+
+        override fun onTimeZoneChanged(timeZone: TimeZone) =
+            clocks.forEach { it.onTimeZoneChanged(timeZone) }
+
+        override fun onFontSettingChanged() {
+            smallClock.setTextSize(
+                TypedValue.COMPLEX_UNIT_PX,
+                resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
+            )
+            largeClock.setTextSize(
+                TypedValue.COMPLEX_UNIT_PX,
+                resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
+            )
+        }
+
+        override fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) =
+            clocks.forEach { it.setColors(DOZE_COLOR, palette.mainColor) }
+
+        override fun onLocaleChanged(locale: Locale) {
+            val nf = NumberFormat.getInstance(locale)
+            if (nf.format(FORMAT_NUMBER.toLong()) == burmeseNumerals) {
+                clocks.forEach { it.setLineSpacingScale(burmeseLineSpacing) }
+            } else {
+                clocks.forEach { it.setLineSpacingScale(defaultLineSpacing) }
+            }
+
+            clocks.forEach { it.refreshFormat() }
+        }
+    }
+
+    override val animation = object : ClockAnimation {
+        override fun initialize(dozeFraction: Float, foldFraction: Float) {
+            dozeState = AnimationState(dozeFraction)
+            foldState = AnimationState(foldFraction)
+
+            if (foldState.isActive) {
+                clocks.forEach { it.animateFoldAppear(false) }
+            } else {
+                clocks.forEach { it.animateDoze(dozeState.isActive, false) }
+            }
+        }
+
+        override fun enter() {
+            if (dozeState.isActive) {
+                clocks.forEach { it.animateAppearOnLockscreen() }
+            }
+        }
+
+        override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } }
+
+        private var foldState = AnimationState(0f)
+        override fun fold(fraction: Float) {
+            val (hasChanged, hasJumped) = foldState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateFoldAppear(!hasJumped) }
+            }
+        }
+
+        private var dozeState = AnimationState(0f)
+        override fun doze(fraction: Float) {
+            val (hasChanged, hasJumped) = dozeState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
+            }
+        }
+    }
+
+    private class AnimationState(
+        var fraction: Float
+    ) {
+        var isActive: Boolean = fraction < 0.5f
+        fun update(newFraction: Float): Pair<Boolean, Boolean> {
+            val wasActive = isActive
+            val hasJumped = (fraction == 0f && newFraction == 1f) ||
+                (fraction == 1f && newFraction == 0f)
+            isActive = newFraction > fraction
+            fraction = newFraction
+            return Pair(wasActive != isActive, hasJumped)
+        }
+    }
+
+    init {
+        events.onLocaleChanged(Locale.getDefault())
+    }
+
+    override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) }
+
+    companion object {
+        private const val DOZE_COLOR = Color.WHITE
+        private const val FORMAT_NUMBER = 1234567890
+    }
+}