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
+ }
+}