DefaultClockProvider implementation using AnimatableClockView

Bug: 229771520
Test: Automated
Change-Id: Iba6c81f8b469ef2d0a5bc38dd89454d75bfe8a43
(cherry picked from commit c742a9cb68)
Merged-In: Iba6c81f8b469ef2d0a5bc38dd89454d75bfe8a43
This commit is contained in:
Hawkwood Glazier
2022-05-27 15:48:40 +00:00
parent f6e087e3ee
commit 9158a60bce
13 changed files with 490 additions and 72 deletions

View File

@@ -4390,6 +4390,9 @@
<!-- Name of a font family to use for medium body text. --> <!-- Name of a font family to use for medium body text. -->
<string name="config_bodyFontFamilyMedium" translatable="false">sans-serif-medium</string> <string name="config_bodyFontFamilyMedium" translatable="false">sans-serif-medium</string>
<!-- Name of the font family to use in the default lockscreen clock -->
<string name="config_clockFontFamily" translatable="false">monospace</string>
<!-- Size of icon shown beside a preference locked by admin --> <!-- Size of icon shown beside a preference locked by admin -->
<dimen name="config_restrictedIconSize">@dimen/restricted_icon_size_material</dimen> <dimen name="config_restrictedIconSize">@dimen/restricted_icon_size_material</dimen>

View File

@@ -22,6 +22,7 @@
** Should include all numeric glyphs in all supported locales. ** Should include all numeric glyphs in all supported locales.
** Recommended: font with variable width to support AOD => LS animations ** Recommended: font with variable width to support AOD => LS animations
--> -->
<!-- TODO: Remove when clock migration complete -->
<font-family xmlns:android="http://schemas.android.com/apk/res/android"> <font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font android:typeface="monospace"/> <font android:typeface="monospace"/>
</font-family> </font-family>

View File

@@ -666,6 +666,7 @@
<!-- With the large clock, move up slightly from the center --> <!-- With the large clock, move up slightly from the center -->
<dimen name="keyguard_large_clock_top_margin">-60dp</dimen> <dimen name="keyguard_large_clock_top_margin">-60dp</dimen>
<!-- TODO: Remove during migration -->
<!-- Default line spacing multiplier between hours and minutes of the keyguard clock --> <!-- 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> <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 --> <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock -->
@@ -890,6 +891,7 @@
<dimen name="burn_in_prevention_offset_y_clock">42dp</dimen> <dimen name="burn_in_prevention_offset_y_clock">42dp</dimen>
<!-- Clock maximum font size (dp is intentional, to prevent any further scaling) --> <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
<!-- TODO: Remove when clock migration complete -->
<dimen name="large_clock_text_size">150dp</dimen> <dimen name="large_clock_text_size">150dp</dimen>
<dimen name="clock_text_size">86dp</dimen> <dimen name="clock_text_size">86dp</dimen>

View File

@@ -56,6 +56,9 @@ android_library {
"dagger2", "dagger2",
"jsr330", "jsr330",
], ],
resource_dirs: [
"res",
],
java_version: "1.8", java_version: "1.8",
min_sdk_version: "current", min_sdk_version: "current",
plugins: ["dagger2-compiler"], plugins: ["dagger2-compiler"],

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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>

View File

@@ -331,9 +331,9 @@ class AnimatableClockView @JvmOverloads constructor(
) )
} }
fun refreshFormat() { fun refreshFormat() = refreshFormat(DateFormat.is24HourFormat(context))
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context) Patterns.update(context)
val use24HourFormat = DateFormat.is24HourFormat(context)
format = when { format = when {
isSingleLineInternal && use24HourFormat -> Patterns.sClockView24 isSingleLineInternal && use24HourFormat -> Patterns.sClockView24

View File

@@ -34,16 +34,23 @@ import javax.inject.Inject
private val TAG = ClockRegistry::class.simpleName private val TAG = ClockRegistry::class.simpleName
private val DEBUG = true private val DEBUG = true
const val DEFAULT_CLOCK_ID = "DEFAULT"
typealias ClockChangeListener = () -> Unit typealias ClockChangeListener = () -> Unit
/** ClockRegistry aggregates providers and plugins */ /** ClockRegistry aggregates providers and plugins */
open class ClockRegistry @Inject constructor( open class ClockRegistry(
val context: Context, val context: Context,
val pluginManager: PluginManager, 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 gson = Gson()
private val availableClocks = mutableMapOf<ClockId, ClockInfo>() private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
private val clockChangeListeners = mutableListOf<ClockChangeListener>() private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -53,60 +60,71 @@ open class ClockRegistry @Inject constructor(
} }
private val pluginListener = object : PluginListener<ClockProviderPlugin> { private val pluginListener = object : PluginListener<ClockProviderPlugin> {
override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) { override fun onPluginConnected(plugin: ClockProviderPlugin, context: Context) =
val currentId = currentClockId connectClocks(plugin)
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
}
availableClocks[id] = ClockInfo(clock, plugin) override fun onPluginDisconnected(plugin: ClockProviderPlugin) =
disconnectClocks(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() }
}
}
}
} }
open var currentClockId: ClockId open var currentClockId: ClockId
get() { get() {
val json = Settings.Secure.getString(context.contentResolver, val json = Settings.Secure.getString(
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE) context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
)
return gson.fromJson(json, ClockSetting::class.java).clockId return gson.fromJson(json, ClockSetting::class.java).clockId
} }
set(value) { set(value) {
val json = gson.toJson(ClockSetting(value, System.currentTimeMillis())) val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
Settings.Secure.putString(context.contentResolver, Settings.Secure.putString(
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json) context.contentResolver,
Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
)
} }
init { init {
connectClocks(defaultClockProvider)
pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java) pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java)
context.contentResolver.registerContentObserver( context.contentResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE), Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
false, false,
settingObserver, 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 } fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata }
@@ -148,4 +166,4 @@ open class ClockRegistry @Inject constructor(
val clockId: ClockId, val clockId: ClockId,
val _applied_timestamp: Long val _applied_timestamp: Long
) )
} }

View File

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

View File

@@ -15,20 +15,19 @@
*/ */
package com.android.systemui.shared.clocks package com.android.systemui.shared.clocks
import org.mockito.Mockito.`when` as whenever
import android.content.Context
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Handler import android.os.Handler
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.PluginListener
import com.android.systemui.shared.plugins.PluginManager
import com.android.systemui.plugins.Clock import com.android.systemui.plugins.Clock
import com.android.systemui.plugins.ClockId import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProviderPlugin import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.PluginListener
import com.android.systemui.shared.plugins.PluginManager
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
@@ -39,6 +38,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
@RunWith(AndroidTestingRunner::class) @RunWith(AndroidTestingRunner::class)
@@ -49,9 +49,11 @@ class ClockRegistryTest : SysuiTestCase() {
@Mock private lateinit var mockContext: Context @Mock private lateinit var mockContext: Context
@Mock private lateinit var mockPluginManager: PluginManager @Mock private lateinit var mockPluginManager: PluginManager
@Mock private lateinit var mockClock: Clock @Mock private lateinit var mockClock: Clock
@Mock private lateinit var mockDefaultClock: Clock
@Mock private lateinit var mockThumbnail: Drawable @Mock private lateinit var mockThumbnail: Drawable
@Mock private lateinit var mockHandler: Handler @Mock private lateinit var mockHandler: Handler
@Mock private lateinit var mockContentResolver: ContentResolver @Mock private lateinit var mockContentResolver: ContentResolver
private lateinit var fakeDefaultProvider: FakeClockPlugin
private lateinit var pluginListener: PluginListener<ClockProviderPlugin> private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry private lateinit var registry: ClockRegistry
@@ -83,42 +85,52 @@ class ClockRegistryTest : SysuiTestCase() {
name: String, name: String,
create: () -> Clock = ::failFactory, create: () -> Clock = ::failFactory,
getThumbnail: () -> Drawable? = ::failThumbnail getThumbnail: () -> Drawable? = ::failThumbnail
) { ): FakeClockPlugin {
metadata.add(ClockMetadata(id, name)) metadata.add(ClockMetadata(id, name))
createCallbacks[id] = create createCallbacks[id] = create
thumbnailCallbacks[id] = getThumbnail thumbnailCallbacks[id] = getThumbnail
return this
} }
} }
@Before @Before
fun setUp() { fun setUp() {
fakeDefaultProvider = FakeClockPlugin()
.addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail })
whenever(mockContext.contentResolver).thenReturn(mockContentResolver) whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>() val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>()
registry = object : ClockRegistry(mockContext, mockPluginManager, mockHandler) { registry = object : ClockRegistry(
mockContext,
mockPluginManager,
mockHandler,
fakeDefaultProvider
) {
override var currentClockId: ClockId override var currentClockId: ClockId
get() = settingValue get() = settingValue
set(value) { settingValue = value } set(value) { settingValue = value }
} }
verify(mockPluginManager).addPluginListener(captor.capture(),
eq(ClockProviderPlugin::class.java)) verify(mockPluginManager)
.addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java))
pluginListener = captor.value pluginListener = captor.value
} }
@Test @Test
fun pluginRegistration_CorrectState() { fun pluginRegistration_CorrectState() {
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
plugin1.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
plugin1.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
plugin2.addClock("clock_3", "clock 3") .addClock("clock_3", "clock 3")
plugin2.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginConnected(plugin2, mockContext)
val list = registry.getClocks() val list = registry.getClocks()
assertEquals(list, listOf( assertEquals(list, listOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1"), ClockMetadata("clock_1", "clock 1"),
ClockMetadata("clock_2", "clock 2"), ClockMetadata("clock_2", "clock 2"),
ClockMetadata("clock_3", "clock 3"), ClockMetadata("clock_3", "clock 3"),
@@ -126,20 +138,27 @@ class ClockRegistryTest : SysuiTestCase() {
)) ))
} }
@Test
fun noPlugins_createDefaultClock() {
val clock = registry.createCurrentClock()
assertEquals(clock, mockDefaultClock)
}
@Test @Test
fun clockIdConflict_ErrorWithoutCrash() { fun clockIdConflict_ErrorWithoutCrash() {
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
plugin1.addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail }) .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
plugin1.addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail }) .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
plugin2.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
plugin2.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginConnected(plugin2, mockContext)
val list = registry.getClocks() val list = registry.getClocks()
assertEquals(list, listOf( assertEquals(list, listOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1"), ClockMetadata("clock_1", "clock 1"),
ClockMetadata("clock_2", "clock 2") ClockMetadata("clock_2", "clock 2")
)) ))
@@ -153,13 +172,13 @@ class ClockRegistryTest : SysuiTestCase() {
@Test @Test
fun createCurrentClock_pluginConnected() { fun createCurrentClock_pluginConnected() {
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
plugin1.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
plugin1.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
settingValue = "clock_3" settingValue = "clock_3"
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
plugin2.addClock("clock_3", "clock 3", { mockClock }) .addClock("clock_3", "clock 3", { mockClock })
plugin2.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginConnected(plugin2, mockContext)
@@ -171,32 +190,32 @@ class ClockRegistryTest : SysuiTestCase() {
@Test @Test
fun createDefaultClock_pluginDisconnected() { fun createDefaultClock_pluginDisconnected() {
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
plugin1.addClock(DEFAULT_CLOCK_ID, "default", { mockClock }) .addClock("clock_1", "clock 1")
plugin1.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
settingValue = "clock_3" settingValue = "clock_3"
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
plugin2.addClock("clock_3", "clock 3") .addClock("clock_3", "clock 3")
plugin2.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginConnected(plugin2, mockContext)
pluginListener.onPluginDisconnected(plugin2) pluginListener.onPluginDisconnected(plugin2)
val clock = registry.createCurrentClock() val clock = registry.createCurrentClock()
assertEquals(clock, mockClock) assertEquals(clock, mockDefaultClock)
} }
@Test @Test
fun pluginRemoved_clockChanged() { fun pluginRemoved_clockChanged() {
val plugin1 = FakeClockPlugin() val plugin1 = FakeClockPlugin()
plugin1.addClock("clock_1", "clock 1") .addClock("clock_1", "clock 1")
plugin1.addClock("clock_2", "clock 2") .addClock("clock_2", "clock 2")
settingValue = "clock_3" settingValue = "clock_3"
val plugin2 = FakeClockPlugin() val plugin2 = FakeClockPlugin()
plugin2.addClock("clock_3", "clock 3", { mockClock }) .addClock("clock_3", "clock 3", { mockClock })
plugin2.addClock("clock_4", "clock 4") .addClock("clock_4", "clock 4")
pluginListener.onPluginConnected(plugin1, mockContext) pluginListener.onPluginConnected(plugin1, mockContext)
pluginListener.onPluginConnected(plugin2, mockContext) pluginListener.onPluginConnected(plugin2, mockContext)

View File

@@ -0,0 +1,77 @@
/*
* 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.drawable.Drawable
import android.testing.AndroidTestingRunner
import android.view.LayoutInflater
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@RunWith(AndroidTestingRunner::class)
@SmallTest
class DefaultClockProviderTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
@Mock private lateinit var mockClockView: AnimatableClockView
@Mock private lateinit var layoutInflater: LayoutInflater
@Mock private lateinit var mockClockThumbnail: Drawable
@Mock private lateinit var resources: Resources
private lateinit var provider: DefaultClockProvider
@Before
fun setUp() {
whenever(layoutInflater.inflate(R.layout.clock_default_small, null))
.thenReturn(mockClockView)
whenever(layoutInflater.inflate(R.layout.clock_default_large, null))
.thenReturn(mockClockView)
whenever(resources.getDrawable(R.drawable.clock_default_thumbnail, null))
.thenReturn(mockClockThumbnail)
provider = DefaultClockProvider(layoutInflater, resources)
}
@Test
fun providedClocks_matchesFactory() {
// All providers need to provide clocks & thumbnails for exposed clocks
for (metadata in provider.getClocks()) {
assertNotNull(provider.createClock(metadata.clockId))
assertNotNull(provider.getClockThumbnail(metadata.clockId))
}
}
@Test
fun defaultClock_alwaysProvided() {
// Default clock provider must always provide the default clock
val clock = provider.createClock(DEFAULT_CLOCK_ID)
assertNotNull(clock)
assertEquals(clock.smallClock, mockClockView)
assertEquals(clock.largeClock, mockClockView)
}
}