Add Customization Lib

Bug: 258424862
Test: Manually rebuilt everything
Change-Id: If683023d2f592bb09827a50fbc79559b5b57cb3a
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
new file mode 100644
index 0000000..dc450bb
--- /dev/null
+++ b/packages/SystemUI/customization/Android.bp
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+    name: "SystemUICustomizationLib",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+        "src/**/*.aidl",
+    ],
+    static_libs: [
+        "PluginCoreLib",
+        "SystemUIAnimationLib",
+        "SystemUIPluginLib",
+        "SystemUIUnfoldLib",
+        "androidx.dynamicanimation_dynamicanimation",
+        "androidx.concurrent_concurrent-futures",
+        "androidx.lifecycle_lifecycle-runtime-ktx",
+        "androidx.lifecycle_lifecycle-viewmodel-ktx",
+        "androidx.recyclerview_recyclerview",
+        "kotlinx_coroutines_android",
+        "kotlinx_coroutines",
+        "dagger2",
+        "jsr330",
+    ],
+    resource_dirs: [
+        "res",
+    ],
+    min_sdk_version: "current",
+    plugins: ["dagger2-compiler"],
+    kotlincflags: ["-Xjvm-default=enable"],
+}
diff --git a/packages/SystemUI/customization/AndroidManifest.xml b/packages/SystemUI/customization/AndroidManifest.xml
new file mode 100644
index 0000000..3277bff
--- /dev/null
+++ b/packages/SystemUI/customization/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.systemui.customization">
+
+</manifest>
diff --git a/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml b/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml
new file mode 100644
index 0000000..be72d0b
--- /dev/null
+++ b/packages/SystemUI/customization/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/customization/res/layout/clock_default_large.xml b/packages/SystemUI/customization/res/layout/clock_default_large.xml
new file mode 100644
index 0000000..0139d50
--- /dev/null
+++ b/packages/SystemUI/customization/res/layout/clock_default_large.xml
@@ -0,0 +1,31 @@
+<?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: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/customization/res/layout/clock_default_small.xml b/packages/SystemUI/customization/res/layout/clock_default_small.xml
new file mode 100644
index 0000000..ff6d7f9
--- /dev/null
+++ b/packages/SystemUI/customization/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:layout_width="match_parent"
+    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:ellipsize="none"
+    android:singleLine="true"
+    android:fontFeatureSettings="pnum"
+    chargeAnimationDelay="350"
+    dozeWeight="200"
+    lockScreenWeight="400" />
diff --git a/packages/SystemUI/customization/res/values/attrs.xml b/packages/SystemUI/customization/res/values/attrs.xml
new file mode 100644
index 0000000..f9d66ee
--- /dev/null
+++ b/packages/SystemUI/customization/res/values/attrs.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+
+<!-- Formatting note: terminate all comments with a period, to avoid breaking
+     the documentation output. To suppress comment lines from the documentation
+     output, insert an eat-comment element after the comment lines.
+-->
+
+<resources>
+    <declare-styleable name="AnimatableClockView">
+        <attr name="dozeWeight" format="integer" />
+        <attr name="lockScreenWeight" format="integer" />
+        <attr name="chargeAnimationDelay" format="integer" />
+    </declare-styleable>
+</resources>
diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml
new file mode 100644
index 0000000..8f90f0f
--- /dev/null
+++ b/packages/SystemUI/customization/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/customization/res/values/donottranslate.xml b/packages/SystemUI/customization/res/values/donottranslate.xml
new file mode 100644
index 0000000..383d5521
--- /dev/null
+++ b/packages/SystemUI/customization/res/values/donottranslate.xml
@@ -0,0 +1,23 @@
+<?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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Skeleton string format for displaying the time in 12-hour format. -->
+    <string name="clock_12hr_format">hm</string>
+
+    <!-- Skeleton string format for displaying the time in 24-hour format. -->
+    <string name="clock_24hr_format">Hm</string>
+</resources>
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
new file mode 100644
index 0000000..22944b8
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2021 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.animation.TimeInterpolator
+import android.annotation.ColorInt
+import android.annotation.FloatRange
+import android.annotation.IntRange
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.text.Layout
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.util.MathUtils
+import android.widget.TextView
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.animation.GlyphCallback
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.animation.TextAnimator
+import com.android.systemui.customization.R
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
+import java.io.PrintWriter
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
+ * The time's text color is a gradient that changes its colors based on its controller.
+ */
+@SuppressLint("AppCompatCustomView")
+class AnimatableClockView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0
+) : TextView(context, attrs, defStyleAttr, defStyleRes) {
+    var tag: String = "UnnamedClockView"
+    var logBuffer: LogBuffer? = null
+
+    private val time = Calendar.getInstance()
+
+    private val dozingWeightInternal: Int
+    private val lockScreenWeightInternal: Int
+    private val isSingleLineInternal: Boolean
+
+    private var format: CharSequence? = null
+    private var descFormat: CharSequence? = null
+
+    @ColorInt
+    private var dozingColor = 0
+
+    @ColorInt
+    private var lockScreenColor = 0
+
+    private var lineSpacingScale = 1f
+    private val chargeAnimationDelay: Int
+    private var textAnimator: TextAnimator? = null
+    private var onTextAnimatorInitialized: Runnable? = null
+
+    @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
+        { layout, invalidateCb -> TextAnimator(layout, invalidateCb) }
+    @VisibleForTesting var isAnimationEnabled: Boolean = true
+    @VisibleForTesting var timeOverrideInMillis: Long? = null
+
+    val dozingWeight: Int
+        get() = if (useBoldedVersion()) dozingWeightInternal + 100 else dozingWeightInternal
+
+    val lockScreenWeight: Int
+        get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
+
+    /**
+     * The number of pixels below the baseline. For fonts that support languages such as
+     * Burmese, this space can be significant and should be accounted for when computing layout.
+     */
+    val bottom get() = paint?.fontMetrics?.bottom ?: 0f
+
+    init {
+        val animatableClockViewAttributes = context.obtainStyledAttributes(
+            attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
+        )
+
+        try {
+            dozingWeightInternal = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_dozeWeight,
+                100
+            )
+            lockScreenWeightInternal = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_lockScreenWeight,
+                300
+            )
+            chargeAnimationDelay = animatableClockViewAttributes.getInt(
+                R.styleable.AnimatableClockView_chargeAnimationDelay, 200
+            )
+        } finally {
+            animatableClockViewAttributes.recycle()
+        }
+
+        val textViewAttributes = context.obtainStyledAttributes(
+            attrs, android.R.styleable.TextView,
+            defStyleAttr, defStyleRes
+        )
+
+        isSingleLineInternal =
+            try {
+                textViewAttributes.getBoolean(android.R.styleable.TextView_singleLine, false)
+            } finally {
+                textViewAttributes.recycle()
+            }
+
+        refreshFormat()
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
+        refreshFormat()
+    }
+
+    /**
+     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
+     */
+    fun useBoldedVersion(): Boolean {
+        // "Bold text" fontWeightAdjustment is 300.
+        return resources.configuration.fontWeightAdjustment > 100
+    }
+
+    fun refreshTime() {
+        time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
+        contentDescription = DateFormat.format(descFormat, time)
+        val formattedText = DateFormat.format(format, time)
+        logBuffer?.log(tag, DEBUG,
+                { str1 = formattedText?.toString() },
+                { "refreshTime: new formattedText=$str1" }
+        )
+        // Setting text actually triggers a layout pass (because the text view is set to
+        // wrap_content width and TextView always relayouts for this). Avoid needless
+        // relayout if the text didn't actually change.
+        if (!TextUtils.equals(text, formattedText)) {
+            text = formattedText
+            logBuffer?.log(tag, DEBUG,
+                    { str1 = formattedText?.toString() },
+                    { "refreshTime: done setting new time text to: $str1" }
+            )
+            // Because the TextLayout may mutate under the hood as a result of the new text, we
+            // notify the TextAnimator that it may have changed and request a measure/layout. A
+            // crash will occur on the next invocation of setTextStyle if the layout is mutated
+            // without being notified TextInterpolator being notified.
+            if (layout != null) {
+                textAnimator?.updateLayout(layout)
+                logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
+            }
+            requestLayout()
+            logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
+        }
+    }
+
+    fun onTimeZoneChanged(timeZone: TimeZone?) {
+        time.timeZone = timeZone
+        refreshFormat()
+        logBuffer?.log(tag, DEBUG,
+                { str1 = timeZone?.toString() },
+                { "onTimeZoneChanged newTimeZone=$str1" }
+        )
+    }
+
+    @SuppressLint("DrawAllocation")
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        val animator = textAnimator
+        if (animator == null) {
+            textAnimator = textAnimatorFactory(layout, ::invalidate)
+            onTextAnimatorInitialized?.run()
+            onTextAnimatorInitialized = null
+        } else {
+            animator.updateLayout(layout)
+        }
+        logBuffer?.log(tag, DEBUG, "onMeasure")
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        // Use textAnimator to render text if animation is enabled.
+        // Otherwise default to using standard draw functions.
+        if (isAnimationEnabled) {
+            // intentionally doesn't call super.onDraw here or else the text will be rendered twice
+            textAnimator?.draw(canvas)
+        } else {
+            super.onDraw(canvas)
+        }
+        logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
+    }
+
+    override fun invalidate() {
+        super.invalidate()
+        logBuffer?.log(tag, DEBUG, "invalidate")
+    }
+
+    override fun onTextChanged(
+            text: CharSequence,
+            start: Int,
+            lengthBefore: Int,
+            lengthAfter: Int
+    ) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter)
+        logBuffer?.log(tag, DEBUG,
+                { str1 = text.toString() },
+                { "onTextChanged text=$str1" }
+        )
+    }
+
+    fun setLineSpacingScale(scale: Float) {
+        lineSpacingScale = scale
+        setLineSpacing(0f, lineSpacingScale)
+    }
+
+    fun setColors(@ColorInt dozingColor: Int, lockScreenColor: Int) {
+        this.dozingColor = dozingColor
+        this.lockScreenColor = lockScreenColor
+    }
+
+    fun animateAppearOnLockscreen() {
+        logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
+        setTextStyle(
+            weight = dozingWeight,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = false,
+            duration = 0,
+            delay = 0,
+            onAnimationEnd = null
+        )
+        setTextStyle(
+            weight = lockScreenWeight,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = isAnimationEnabled,
+            duration = APPEAR_ANIM_DURATION,
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    fun animateFoldAppear(animate: Boolean = true) {
+        if (isAnimationEnabled && textAnimator == null) {
+            return
+        }
+        logBuffer?.log(tag, DEBUG, "animateFoldAppear")
+        setTextStyle(
+            weight = lockScreenWeightInternal,
+            textSize = -1f,
+            color = lockScreenColor,
+            animate = false,
+            duration = 0,
+            delay = 0,
+            onAnimationEnd = null
+        )
+        setTextStyle(
+            weight = dozingWeightInternal,
+            textSize = -1f,
+            color = dozingColor,
+            animate = animate && isAnimationEnabled,
+            interpolator = Interpolators.EMPHASIZED_DECELERATE,
+            duration = ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    fun animateCharge(isDozing: () -> Boolean) {
+        if (textAnimator == null || textAnimator!!.isRunning()) {
+            // Skip charge animation if dozing animation is already playing.
+            return
+        }
+        logBuffer?.log(tag, DEBUG, "animateCharge")
+        val startAnimPhase2 = Runnable {
+            setTextStyle(
+                weight = if (isDozing()) dozingWeight else lockScreenWeight,
+                textSize = -1f,
+                color = null,
+                animate = isAnimationEnabled,
+                duration = CHARGE_ANIM_DURATION_PHASE_1,
+                delay = 0,
+                onAnimationEnd = null
+            )
+        }
+        setTextStyle(
+            weight = if (isDozing()) lockScreenWeight else dozingWeight,
+            textSize = -1f,
+            color = null,
+            animate = isAnimationEnabled,
+            duration = CHARGE_ANIM_DURATION_PHASE_0,
+            delay = chargeAnimationDelay.toLong(),
+            onAnimationEnd = startAnimPhase2
+        )
+    }
+
+    fun animateDoze(isDozing: Boolean, animate: Boolean) {
+        logBuffer?.log(tag, DEBUG, "animateDoze")
+        setTextStyle(
+            weight = if (isDozing) dozingWeight else lockScreenWeight,
+            textSize = -1f,
+            color = if (isDozing) dozingColor else lockScreenColor,
+            animate = animate && isAnimationEnabled,
+            duration = DOZE_ANIM_DURATION,
+            delay = 0,
+            onAnimationEnd = null
+        )
+    }
+
+    // The offset of each glyph from where it should be.
+    private var glyphOffsets = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+
+    private var lastSeenAnimationProgress = 1.0f
+
+    // If the animation is being reversed, the target offset for each glyph for the "stop".
+    private var animationCancelStartPosition = mutableListOf(0.0f, 0.0f, 0.0f, 0.0f)
+    private var animationCancelStopPosition = 0.0f
+
+    // Whether the currently playing animation needed a stop (and thus, is shortened).
+    private var currentAnimationNeededStop = false
+
+    private val glyphFilter: GlyphCallback = { positionedGlyph, _ ->
+        val offset = positionedGlyph.lineNo * DIGITS_PER_LINE + positionedGlyph.glyphIndex
+        if (offset < glyphOffsets.size) {
+            positionedGlyph.x += glyphOffsets[offset]
+        }
+    }
+
+    /**
+     * Set text style with an optional animation.
+     *
+     * By passing -1 to weight, the view preserves its current weight.
+     * By passing -1 to textSize, the view preserves its current text size.
+     *
+     * @param weight text weight.
+     * @param textSize font size.
+     * @param animate true to animate the text style change, otherwise false.
+     */
+    private fun setTextStyle(
+        @IntRange(from = 0, to = 1000) weight: Int,
+        @FloatRange(from = 0.0) textSize: Float,
+        color: Int?,
+        animate: Boolean,
+        interpolator: TimeInterpolator?,
+        duration: Long,
+        delay: Long,
+        onAnimationEnd: Runnable?
+    ) {
+        if (textAnimator != null) {
+            textAnimator?.setTextStyle(
+                weight = weight,
+                textSize = textSize,
+                color = color,
+                animate = animate && isAnimationEnabled,
+                duration = duration,
+                interpolator = interpolator,
+                delay = delay,
+                onAnimationEnd = onAnimationEnd
+            )
+            textAnimator?.glyphFilter = glyphFilter
+            if (color != null && !isAnimationEnabled) {
+                setTextColor(color)
+            }
+        } else {
+            // when the text animator is set, update its start values
+            onTextAnimatorInitialized = Runnable {
+                textAnimator?.setTextStyle(
+                    weight = weight,
+                    textSize = textSize,
+                    color = color,
+                    animate = false,
+                    duration = duration,
+                    interpolator = interpolator,
+                    delay = delay,
+                    onAnimationEnd = onAnimationEnd
+                )
+                textAnimator?.glyphFilter = glyphFilter
+                if (color != null && !isAnimationEnabled) {
+                    setTextColor(color)
+                }
+            }
+        }
+    }
+
+    private fun setTextStyle(
+        @IntRange(from = 0, to = 1000) weight: Int,
+        @FloatRange(from = 0.0) textSize: Float,
+        color: Int?,
+        animate: Boolean,
+        duration: Long,
+        delay: Long,
+        onAnimationEnd: Runnable?
+    ) {
+        setTextStyle(
+            weight = weight,
+            textSize = textSize,
+            color = color,
+            animate = animate && isAnimationEnabled,
+            interpolator = null,
+            duration = duration,
+            delay = delay,
+            onAnimationEnd = onAnimationEnd
+        )
+    }
+
+    fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
+    fun refreshFormat(use24HourFormat: Boolean) {
+        Patterns.update(context)
+
+        format = when {
+            isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
+            !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
+            isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
+            else -> DOUBLE_LINE_FORMAT_12_HOUR
+        }
+        logBuffer?.log(tag, DEBUG,
+                { str1 = format?.toString() },
+                { "refreshFormat format=$str1" }
+        )
+
+        descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
+        refreshTime()
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("$this")
+        pw.println("    measuredWidth=$measuredWidth")
+        pw.println("    measuredHeight=$measuredHeight")
+        pw.println("    singleLineInternal=$isSingleLineInternal")
+        pw.println("    currText=$text")
+        pw.println("    currTimeContextDesc=$contentDescription")
+        pw.println("    dozingWeightInternal=$dozingWeightInternal")
+        pw.println("    lockScreenWeightInternal=$lockScreenWeightInternal")
+        pw.println("    dozingColor=$dozingColor")
+        pw.println("    lockScreenColor=$lockScreenColor")
+        pw.println("    time=$time")
+    }
+
+    fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+        // Do we need to cancel an in-flight animation?
+        // Need to also check against 0.0f here; we can sometimes get two calls with fraction == 0,
+        // which trips up the check otherwise.
+        if (lastSeenAnimationProgress != 1.0f &&
+                lastSeenAnimationProgress != 0.0f &&
+                fraction == 0.0f) {
+            // New animation, but need to stop the old one. Figure out where each glyph currently
+            // is in relation to the box position. After that, use the leading digit's current
+            // position as the stop target.
+            currentAnimationNeededStop = true
+
+            // We assume that the current glyph offsets would be relative to the "from" position.
+            val moveAmount = toRect.left - fromRect.left
+
+            // Remap the current glyph offsets to be relative to the new "end" position, and figure
+            // out the start/end positions for the stop animation.
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + glyphOffsets[i]
+                animationCancelStartPosition[i] = glyphOffsets[i]
+            }
+
+            // Use the leading digit's offset as the stop position.
+            if (toRect.left > fromRect.left) {
+                // It _was_ moving left
+                animationCancelStopPosition = glyphOffsets[0]
+            } else {
+                // It was moving right
+                animationCancelStopPosition = glyphOffsets[1]
+            }
+        }
+
+        // Is there a cancellation in progress?
+        if (currentAnimationNeededStop && fraction < ANIMATION_CANCELLATION_TIME) {
+            val animationStopProgress = MathUtils.constrainedMap(
+                    0.0f, 1.0f, 0.0f, ANIMATION_CANCELLATION_TIME, fraction
+            )
+
+            // One of the digits has already stopped.
+            val animationStopStep = 1.0f / (NUM_DIGITS - 1)
+
+            for (i in 0 until NUM_DIGITS) {
+                val stopAmount = if (toRect.left > fromRect.left) {
+                    // It was moving left (before flipping)
+                    MOVE_LEFT_DELAYS[i] * animationStopStep
+                } else {
+                    // It was moving right (before flipping)
+                    MOVE_RIGHT_DELAYS[i] * animationStopStep
+                }
+
+                // Leading digit stops immediately.
+                if (stopAmount == 0.0f) {
+                    glyphOffsets[i] = animationCancelStopPosition
+                } else {
+                    val actualStopAmount = MathUtils.constrainedMap(
+                            0.0f, 1.0f, 0.0f, stopAmount, animationStopProgress
+                    )
+                    val easedProgress = MOVE_INTERPOLATOR.getInterpolation(actualStopAmount)
+                    val glyphMoveAmount =
+                            animationCancelStopPosition - animationCancelStartPosition[i]
+                    glyphOffsets[i] =
+                            animationCancelStartPosition[i] + glyphMoveAmount * easedProgress
+                }
+            }
+        } else {
+            // Normal part of the animation.
+            // Do we need to remap the animation progress to take account of the cancellation?
+            val actualFraction = if (currentAnimationNeededStop) {
+                MathUtils.constrainedMap(
+                        0.0f, 1.0f, ANIMATION_CANCELLATION_TIME, 1.0f, fraction
+                )
+            } else {
+                fraction
+            }
+
+            val digitFractions = (0 until NUM_DIGITS).map {
+                // The delay for each digit, in terms of fraction (i.e. the digit should not move
+                // during 0.0 - 0.1).
+                val initialDelay = if (toRect.left > fromRect.left) {
+                    MOVE_RIGHT_DELAYS[it] * MOVE_DIGIT_STEP
+                } else {
+                    MOVE_LEFT_DELAYS[it] * MOVE_DIGIT_STEP
+                }
+
+                val f = MathUtils.constrainedMap(
+                        0.0f, 1.0f,
+                        initialDelay, initialDelay + AVAILABLE_ANIMATION_TIME,
+                        actualFraction
+                )
+                MOVE_INTERPOLATOR.getInterpolation(max(min(f, 1.0f), 0.0f))
+            }
+
+            // Was there an animation halt?
+            val moveAmount = if (currentAnimationNeededStop) {
+                // Only need to animate over the remaining space if the animation was aborted.
+                -animationCancelStopPosition
+            } else {
+                toRect.left.toFloat() - fromRect.left.toFloat()
+            }
+
+            for (i in 0 until NUM_DIGITS) {
+                glyphOffsets[i] = -moveAmount + (moveAmount * digitFractions[i])
+            }
+        }
+
+        invalidate()
+
+        if (fraction == 1.0f) {
+            // Reset
+            currentAnimationNeededStop = false
+        }
+
+        lastSeenAnimationProgress = fraction
+
+        // Ensure that the actual clock container is always in the "end" position.
+        this.setLeftTopRightBottom(toRect.left, toRect.top, toRect.right, toRect.bottom)
+    }
+
+    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
+    // This is an optimization to ensure we only recompute the patterns when the inputs change.
+    private object Patterns {
+        var sClockView12: String? = null
+        var sClockView24: String? = null
+        var sCacheKey: String? = null
+
+        fun update(context: Context) {
+            val locale = Locale.getDefault()
+            val res = context.resources
+            val clockView12Skel = res.getString(R.string.clock_12hr_format)
+            val clockView24Skel = res.getString(R.string.clock_24hr_format)
+            val key = locale.toString() + clockView12Skel + clockView24Skel
+            if (key == sCacheKey) return
+
+            val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
+            sClockView12 = clockView12
+
+            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
+            // format.  The following code removes the AM/PM indicator if we didn't want it.
+            if (!clockView12Skel.contains("a")) {
+                sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
+            }
+
+            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
+            sCacheKey = key
+        }
+    }
+
+    companion object {
+        private val TAG = AnimatableClockView::class.simpleName
+        const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
+        private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
+        private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
+        private const val DOZE_ANIM_DURATION: Long = 300
+        private const val APPEAR_ANIM_DURATION: Long = 350
+        private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
+        private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
+
+        // Constants for the animation
+        private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
+
+        // Calculate the positions of all of the digits...
+        // Offset each digit by, say, 0.1
+        // This means that each digit needs to move over a slice of "fractions", i.e. digit 0 should
+        // move from 0.0 - 0.7, digit 1 from 0.1 - 0.8, digit 2 from 0.2 - 0.9, and digit 3
+        // from 0.3 - 1.0.
+        private const val NUM_DIGITS = 4
+        private const val DIGITS_PER_LINE = 2
+
+        // How much of "fraction" to spend on canceling the animation, if needed
+        private const val ANIMATION_CANCELLATION_TIME = 0.4f
+
+        // Delays. Each digit's animation should have a slight delay, so we get a nice
+        // "stepping" effect. When moving right, the second digit of the hour should move first.
+        // When moving left, the first digit of the hour should move first. The lists encode
+        // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
+        // by delayMultiplier.
+        private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
+        private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
+
+        // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
+        // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
+        // before moving).
+        //
+        // The current specs dictate that each digit should have a 33ms gap between them. The
+        // overall time is 1s right now.
+        private const val MOVE_DIGIT_STEP = 0.033f
+
+        // Total available transition time for each digit, taking into account the step. If step is
+        // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
+        private val AVAILABLE_ANIMATION_TIME = 1.0f - MOVE_DIGIT_STEP * (NUM_DIGITS - 1)
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
new file mode 100644
index 0000000..59b4848
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -0,0 +1,229 @@
+/*
+ * 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.Context
+import android.database.ContentObserver
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Handler
+import android.provider.Settings
+import android.util.Log
+import androidx.annotation.OpenForTesting
+import com.android.internal.annotations.Keep
+import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+import com.android.systemui.plugins.ClockProviderPlugin
+import com.android.systemui.plugins.PluginListener
+import com.android.systemui.plugins.PluginManager
+import org.json.JSONObject
+
+private val TAG = ClockRegistry::class.simpleName
+private const val DEBUG = true
+
+/** ClockRegistry aggregates providers and plugins */
+open class ClockRegistry(
+    val context: Context,
+    val pluginManager: PluginManager,
+    val handler: Handler,
+    val isEnabled: Boolean,
+    userHandle: Int,
+    defaultClockProvider: ClockProvider,
+    val fallbackClockId: ClockId = DEFAULT_CLOCK_ID,
+) {
+    // Usually this would be a typealias, but a SAM provides better java interop
+    fun interface ClockChangeListener {
+        fun onClockChanged()
+    }
+
+    private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
+    private val clockChangeListeners = mutableListOf<ClockChangeListener>()
+    private val settingObserver = object : ContentObserver(handler) {
+        override fun onChange(selfChange: Boolean, uris: Collection<Uri>, flags: Int, userId: Int) =
+            clockChangeListeners.forEach { it.onClockChanged() }
+    }
+
+    private val pluginListener = object : PluginListener<ClockProviderPlugin> {
+        override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
+            connectClocks(plugin)
+
+        override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
+            disconnectClocks(plugin)
+    }
+
+    open var currentClockId: ClockId
+        get() {
+            return try {
+                val json = Settings.Secure.getString(
+                    context.contentResolver,
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+                )
+                if (json == null || json.isEmpty()) {
+                    return fallbackClockId
+                }
+                ClockSetting.deserialize(json).clockId
+            } catch (ex: Exception) {
+                Log.e(TAG, "Failed to parse clock setting", ex)
+                fallbackClockId
+            }
+        }
+        set(value) {
+            try {
+                val json = ClockSetting.serialize(ClockSetting(value, System.currentTimeMillis()))
+                Settings.Secure.putString(
+                    context.contentResolver,
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
+                )
+            } catch (ex: Exception) {
+                Log.e(TAG, "Failed to set clock setting", ex)
+            }
+        }
+
+    init {
+        connectClocks(defaultClockProvider)
+        if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
+            throw IllegalArgumentException(
+                "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
+            )
+        }
+
+        if (isEnabled) {
+            pluginManager.addPluginListener(
+                pluginListener,
+                ClockProviderPlugin::class.java,
+                /*allowMultiple=*/ true
+            )
+            context.contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                /*notifyForDescendants=*/ false,
+                settingObserver,
+                userHandle
+            )
+        }
+    }
+
+    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 (DEBUG) {
+                Log.i(TAG, "Added ${clock.clockId}")
+            }
+
+            if (currentId == id) {
+                if (DEBUG) {
+                    Log.i(TAG, "Current clock ($currentId) was connected")
+                }
+                clockChangeListeners.forEach { it.onClockChanged() }
+            }
+        }
+    }
+
+    private fun disconnectClocks(provider: ClockProvider) {
+        val currentId = currentClockId
+        for (clock in provider.getClocks()) {
+            availableClocks.remove(clock.clockId)
+            if (DEBUG) {
+                Log.i(TAG, "Removed ${clock.clockId}")
+            }
+
+            if (currentId == clock.clockId) {
+                Log.w(TAG, "Current clock ($currentId) was disconnected")
+                clockChangeListeners.forEach { it.onClockChanged() }
+            }
+        }
+    }
+
+    @OpenForTesting
+    open fun getClocks(): List<ClockMetadata> {
+        if (!isEnabled) {
+            return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
+        }
+        return availableClocks.map { (_, clock) -> clock.metadata }
+    }
+
+    fun getClockThumbnail(clockId: ClockId): Drawable? =
+        availableClocks[clockId]?.provider?.getClockThumbnail(clockId)
+
+    fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId)
+
+    fun registerClockChangeListener(listener: ClockChangeListener) =
+        clockChangeListeners.add(listener)
+
+    fun unregisterClockChangeListener(listener: ClockChangeListener) =
+        clockChangeListeners.remove(listener)
+
+    fun createCurrentClock(): ClockController {
+        val clockId = currentClockId
+        if (isEnabled && clockId.isNotEmpty()) {
+            val clock = createClock(clockId)
+            if (clock != null) {
+                if (DEBUG) {
+                    Log.i(TAG, "Rendering clock $clockId")
+                }
+                return clock
+            } else {
+                Log.e(TAG, "Clock $clockId not found; using default")
+            }
+        }
+
+        return createClock(DEFAULT_CLOCK_ID)!!
+    }
+
+    private fun createClock(clockId: ClockId): ClockController? =
+        availableClocks[clockId]?.provider?.createClock(clockId)
+
+    private data class ClockInfo(
+        val metadata: ClockMetadata,
+        val provider: ClockProvider
+    )
+
+    @Keep
+    data class ClockSetting(
+        val clockId: ClockId,
+        val _applied_timestamp: Long?
+    ) {
+        companion object {
+            private val KEY_CLOCK_ID = "clockId"
+            private val KEY_TIMESTAMP = "_applied_timestamp"
+
+            fun serialize(setting: ClockSetting): String {
+                return JSONObject()
+                    .put(KEY_CLOCK_ID, setting.clockId)
+                    .put(KEY_TIMESTAMP, setting._applied_timestamp)
+                    .toString()
+            }
+
+            fun deserialize(jsonStr: String): ClockSetting {
+                val json = JSONObject(jsonStr)
+                return ClockSetting(
+                    json.getString(KEY_CLOCK_ID),
+                    if (!json.isNull(KEY_TIMESTAMP)) json.getLong(KEY_TIMESTAMP) else null)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
new file mode 100644
index 0000000..8698844
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -0,0 +1,272 @@
+/*
+ * 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.Context
+import android.content.res.Resources
+import android.graphics.Color
+import android.graphics.Rect
+import android.icu.text.NumberFormat
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.customization.R
+import com.android.systemui.plugins.ClockAnimations
+import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.ClockEvents
+import com.android.systemui.plugins.ClockFaceController
+import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+
+private val TAG = DefaultClockController::class.simpleName
+
+/**
+ * Controls the default clock visuals.
+ *
+ * This serves as an adapter between the clock interface and the AnimatableClockView used by the
+ * existing lockscreen clock.
+ */
+class DefaultClockController(
+    ctx: Context,
+    private val layoutInflater: LayoutInflater,
+    private val resources: Resources,
+) : ClockController {
+    override val smallClock: DefaultClockFaceController
+    override val largeClock: LargeClockFaceController
+    private val clocks: List<AnimatableClockView>
+
+    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: DefaultClockEvents
+    override lateinit var animations: DefaultClockAnimations
+        private set
+
+    init {
+        val parent = FrameLayout(ctx)
+        smallClock =
+            DefaultClockFaceController(
+                layoutInflater.inflate(R.layout.clock_default_small, parent, false)
+                    as AnimatableClockView
+            )
+        largeClock =
+            LargeClockFaceController(
+                layoutInflater.inflate(R.layout.clock_default_large, parent, false)
+                    as AnimatableClockView
+            )
+        clocks = listOf(smallClock.view, largeClock.view)
+
+        events = DefaultClockEvents()
+        animations = DefaultClockAnimations(0f, 0f)
+        events.onLocaleChanged(Locale.getDefault())
+    }
+
+    override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+        largeClock.recomputePadding(null)
+        animations = DefaultClockAnimations(dozeFraction, foldFraction)
+        events.onColorPaletteChanged(resources)
+        events.onTimeZoneChanged(TimeZone.getDefault())
+        events.onTimeTick()
+    }
+
+    override fun setLogBuffer(logBuffer: LogBuffer) {
+        smallClock.view.tag = "smallClockView"
+        largeClock.view.tag = "largeClockView"
+        smallClock.view.logBuffer = logBuffer
+        largeClock.view.logBuffer = logBuffer
+    }
+
+    open inner class DefaultClockFaceController(
+        override val view: AnimatableClockView,
+    ) : ClockFaceController {
+
+        // MAGENTA is a placeholder, and will be assigned correctly in initialize
+        private var currentColor = Color.MAGENTA
+        private var isRegionDark = false
+        protected var targetRegion: Rect? = null
+
+        init {
+            view.setColors(currentColor, currentColor)
+        }
+
+        override val events =
+            object : ClockFaceEvents {
+                override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+                    this@DefaultClockFaceController.isRegionDark = isRegionDark
+                    updateColor()
+                }
+
+                override fun onTargetRegionChanged(targetRegion: Rect?) {
+                    this@DefaultClockFaceController.targetRegion = targetRegion
+                    recomputePadding(targetRegion)
+                }
+
+                override fun onFontSettingChanged(fontSizePx: Float) {
+                    view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
+                    recomputePadding(targetRegion)
+                }
+            }
+
+        open fun recomputePadding(targetRegion: Rect?) {}
+
+        fun updateColor() {
+            val color =
+                if (isRegionDark) {
+                    resources.getColor(android.R.color.system_accent1_100)
+                } else {
+                    resources.getColor(android.R.color.system_accent2_600)
+                }
+
+            if (currentColor == color) {
+                return
+            }
+
+            currentColor = color
+            view.setColors(DOZE_COLOR, color)
+            if (!animations.dozeState.isActive) {
+                view.animateAppearOnLockscreen()
+            }
+        }
+    }
+
+    inner class LargeClockFaceController(
+        view: AnimatableClockView,
+    ) : DefaultClockFaceController(view) {
+        override fun recomputePadding(targetRegion: Rect?) {
+            // We center the view within the targetRegion instead of within the parent
+            // view by computing the difference and adding that to the padding.
+            val parent = view.parent
+            val yDiff =
+                if (targetRegion != null && parent is View && parent.isLaidOut())
+                    targetRegion.centerY() - parent.height / 2f
+                else 0f
+            val lp = view.getLayoutParams() as FrameLayout.LayoutParams
+            lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
+            view.setLayoutParams(lp)
+        }
+
+        fun moveForSplitShade(fromRect: Rect, toRect: Rect, fraction: Float) {
+            view.moveForSplitShade(fromRect, toRect, fraction)
+        }
+    }
+
+    inner class DefaultClockEvents : 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 onColorPaletteChanged(resources: Resources) {
+            largeClock.updateColor()
+            smallClock.updateColor()
+        }
+
+        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() }
+        }
+    }
+
+    inner class DefaultClockAnimations(
+        dozeFraction: Float,
+        foldFraction: Float,
+    ) : ClockAnimations {
+        internal val dozeState = AnimationState(dozeFraction)
+        private val foldState = AnimationState(foldFraction)
+
+        init {
+            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 } }
+
+        override fun fold(fraction: Float) {
+            val (hasChanged, hasJumped) = foldState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateFoldAppear(!hasJumped) }
+            }
+        }
+
+        override fun doze(fraction: Float) {
+            val (hasChanged, hasJumped) = dozeState.update(fraction)
+            if (hasChanged) {
+                clocks.forEach { it.animateDoze(dozeState.isActive, !hasJumped) }
+            }
+        }
+
+        override fun onPositionUpdated(fromRect: Rect, toRect: Rect, fraction: Float) {
+            largeClock.moveForSplitShade(fromRect, toRect, fraction)
+        }
+
+        override val hasCustomPositionUpdatedAnimation: Boolean
+            get() = true
+    }
+
+    class AnimationState(
+        var fraction: Float,
+    ) {
+        var isActive: Boolean = fraction > 0.5f
+        fun update(newFraction: Float): Pair<Boolean, Boolean> {
+            if (newFraction == fraction) {
+                return Pair(isActive, false)
+            }
+            val wasActive = isActive
+            val hasJumped =
+                (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f)
+            isActive = newFraction > fraction
+            fraction = newFraction
+            return Pair(wasActive != isActive, hasJumped)
+        }
+    }
+
+    override fun dump(pw: PrintWriter) {
+        pw.print("smallClock=")
+        smallClock.view.dump(pw)
+
+        pw.print("largeClock=")
+        largeClock.view.dump(pw)
+    }
+
+    companion object {
+        @VisibleForTesting const val DOZE_COLOR = Color.WHITE
+        private const val FORMAT_NUMBER = 1234567890
+    }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
new file mode 100644
index 0000000..4c0504b
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.Context
+import android.content.res.Resources
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import com.android.systemui.customization.R
+import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.ClockId
+import com.android.systemui.plugins.ClockMetadata
+import com.android.systemui.plugins.ClockProvider
+
+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 constructor(
+    val ctx: Context,
+    val layoutInflater: LayoutInflater,
+    val resources: Resources
+) : ClockProvider {
+    override fun getClocks(): List<ClockMetadata> =
+        listOf(ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME))
+
+    override fun createClock(id: ClockId): ClockController {
+        if (id != DEFAULT_CLOCK_ID) {
+            throw IllegalArgumentException("$id is unsupported by $TAG")
+        }
+
+        return DefaultClockController(ctx, 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)
+    }
+}