blob: 9495fdd50a7e370b7bb33961270ea2f1ea9ba07d [file] [log] [blame]
/*
* 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.ContentResolver
import android.content.Context
import android.graphics.drawable.Drawable
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags.TRANSIT_CLOCK
import com.android.systemui.plugins.ClockController
import com.android.systemui.plugins.ClockId
import com.android.systemui.plugins.ClockMetadata
import com.android.systemui.plugins.ClockProviderPlugin
import com.android.systemui.plugins.ClockSettings
import com.android.systemui.plugins.PluginListener
import com.android.systemui.plugins.PluginLifecycleManager
import com.android.systemui.plugins.PluginManager
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import junit.framework.Assert.assertEquals
import junit.framework.Assert.fail
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
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.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@RunWith(AndroidTestingRunner::class)
@SmallTest
class ClockRegistryTest : SysuiTestCase() {
@JvmField @Rule val mockito = MockitoJUnit.rule()
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var scope: TestScope
@Mock private lateinit var mockContext: Context
@Mock private lateinit var mockPluginManager: PluginManager
@Mock private lateinit var mockClock: ClockController
@Mock private lateinit var mockDefaultClock: ClockController
@Mock private lateinit var mockThumbnail: Drawable
@Mock private lateinit var mockContentResolver: ContentResolver
@Mock private lateinit var mockPluginLifecycle: PluginLifecycleManager<ClockProviderPlugin>
private lateinit var fakeDefaultProvider: FakeClockPlugin
private lateinit var pluginListener: PluginListener<ClockProviderPlugin>
private lateinit var registry: ClockRegistry
private val featureFlags = FakeFeatureFlags()
companion object {
private fun failFactory(clockId: ClockId): ClockController {
fail("Unexpected call to createClock: $clockId")
return null!!
}
private fun failThumbnail(clockId: ClockId): Drawable? {
fail("Unexpected call to getThumbnail: $clockId")
return null
}
}
private class FakeClockPlugin : ClockProviderPlugin {
private val metadata = mutableListOf<ClockMetadata>()
private val createCallbacks = mutableMapOf<ClockId, (ClockId) -> ClockController>()
private val thumbnailCallbacks = mutableMapOf<ClockId, (ClockId) -> Drawable?>()
override fun getClocks() = metadata
override fun createClock(settings: ClockSettings): ClockController =
createCallbacks[settings.clockId!!]!!(settings.clockId!!)
override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!(id)
fun addClock(
id: ClockId,
name: String,
create: (ClockId) -> ClockController = ::failFactory,
getThumbnail: (ClockId) -> Drawable? = ::failThumbnail
): FakeClockPlugin {
metadata.add(ClockMetadata(id, name))
createCallbacks[id] = create
thumbnailCallbacks[id] = getThumbnail
return this
}
}
@Before
fun setUp() {
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
scope = TestScope(dispatcher)
fakeDefaultProvider = FakeClockPlugin()
.addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail })
whenever(mockContext.contentResolver).thenReturn(mockContentResolver)
val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>()
registry = object : ClockRegistry(
mockContext,
mockPluginManager,
scope = scope.backgroundScope,
mainDispatcher = dispatcher,
bgDispatcher = dispatcher,
isEnabled = true,
handleAllUsers = true,
defaultClockProvider = fakeDefaultProvider,
keepAllLoaded = false,
subTag = "Test",
) {
override fun querySettings() { }
override fun applySettings(value: ClockSettings?) {
settings = value
}
// Unit Test does not validate threading
override fun assertMainThread() {}
override fun assertNotMainThread() {}
}
registry.registerListeners()
verify(mockPluginManager)
.addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
pluginListener = captor.value
}
@Test
fun pluginRegistration_CorrectState() {
val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4")
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
val list = registry.getClocks()
assertEquals(
list.toSet(),
setOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1"),
ClockMetadata("clock_2", "clock 2"),
ClockMetadata("clock_3", "clock 3"),
ClockMetadata("clock_4", "clock 4")
)
)
}
@Test
fun noPlugins_createDefaultClock() {
val clock = registry.createCurrentClock()
assertEquals(clock, mockDefaultClock)
}
@Test
fun clockIdConflict_ErrorWithoutCrash_unloadDuplicate() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail })
.addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail })
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin2 = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2)
val list = registry.getClocks()
assertEquals(
list.toSet(),
setOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1"),
ClockMetadata("clock_2", "clock 2")
)
)
assertEquals(registry.createExampleClock("clock_1"), mockClock)
assertEquals(registry.createExampleClock("clock_2"), mockClock)
assertEquals(registry.getClockThumbnail("clock_1"), mockThumbnail)
assertEquals(registry.getClockThumbnail("clock_2"), mockThumbnail)
verify(mockPluginLifecycle1, never()).unloadPlugin()
verify(mockPluginLifecycle2, times(2)).unloadPlugin()
}
@Test
fun createCurrentClock_pluginConnected() {
val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
val clock = registry.createCurrentClock()
assertEquals(mockClock, clock)
}
@Test
fun createDefaultClock_pluginDisconnected() {
val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3")
.addClock("clock_4", "clock 4")
registry.applySettings(ClockSettings("clock_3", null))
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle)
pluginListener.onPluginUnloaded(plugin2, mockPluginLifecycle)
val clock = registry.createCurrentClock()
assertEquals(clock, mockDefaultClock)
}
@Test
fun pluginRemoved_clockAndListChanged() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin1 = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("clock_2", "clock 2")
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin2 = FakeClockPlugin()
.addClock("clock_3", "clock 3", { mockClock })
.addClock("clock_4", "clock 4")
var changeCallCount = 0
var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() { changeCallCount++ }
override fun onAvailableClocksChanged() { listChangeCallCount++ }
})
registry.applySettings(ClockSettings("clock_3", null))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(0, listChangeCallCount)
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1)
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(1, listChangeCallCount)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2)
scheduler.runCurrent()
assertEquals(2, changeCallCount)
assertEquals(2, listChangeCallCount)
pluginListener.onPluginUnloaded(plugin1, mockPluginLifecycle1)
scheduler.runCurrent()
assertEquals(2, changeCallCount)
assertEquals(2, listChangeCallCount)
pluginListener.onPluginUnloaded(plugin2, mockPluginLifecycle2)
scheduler.runCurrent()
assertEquals(3, changeCallCount)
assertEquals(2, listChangeCallCount)
pluginListener.onPluginDetached(mockPluginLifecycle1)
scheduler.runCurrent()
assertEquals(3, changeCallCount)
assertEquals(3, listChangeCallCount)
pluginListener.onPluginDetached(mockPluginLifecycle2)
scheduler.runCurrent()
assertEquals(3, changeCallCount)
assertEquals(4, listChangeCallCount)
}
@Test
fun unknownPluginAttached_clockAndListUnchanged_loadRequested() {
val mockPluginLifecycle = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle.getPackage()).thenReturn("some.other.package")
var changeCallCount = 0
var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() { changeCallCount++ }
override fun onAvailableClocksChanged() { listChangeCallCount++ }
})
assertEquals(true, pluginListener.onPluginAttached(mockPluginLifecycle))
scheduler.runCurrent()
assertEquals(0, changeCallCount)
assertEquals(0, listChangeCallCount)
}
@Test
fun knownPluginAttached_clockAndListChanged_notLoaded() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle1.getPackage()).thenReturn("com.android.systemui.falcon.one")
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
whenever(mockPluginLifecycle2.getPackage()).thenReturn("com.android.systemui.falcon.two")
var changeCallCount = 0
var listChangeCallCount = 0
registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener {
override fun onCurrentClockChanged() { changeCallCount++ }
override fun onAvailableClocksChanged() { listChangeCallCount++ }
})
registry.applySettings(ClockSettings("DIGITAL_CLOCK_CALLIGRAPHY", null))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(0, listChangeCallCount)
assertEquals(false, pluginListener.onPluginAttached(mockPluginLifecycle1))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(1, listChangeCallCount)
assertEquals(false, pluginListener.onPluginAttached(mockPluginLifecycle2))
scheduler.runCurrent()
assertEquals(1, changeCallCount)
assertEquals(2, listChangeCallCount)
}
@Test
fun pluginAddRemove_concurrentModification() {
val mockPluginLifecycle1 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val mockPluginLifecycle2 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val mockPluginLifecycle3 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val mockPluginLifecycle4 = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin1 = FakeClockPlugin().addClock("clock_1", "clock 1")
val plugin2 = FakeClockPlugin().addClock("clock_2", "clock 2")
val plugin3 = FakeClockPlugin().addClock("clock_3", "clock 3")
val plugin4 = FakeClockPlugin().addClock("clock_4", "clock 4")
whenever(mockPluginLifecycle1.isLoaded).thenReturn(true)
whenever(mockPluginLifecycle2.isLoaded).thenReturn(true)
whenever(mockPluginLifecycle3.isLoaded).thenReturn(true)
whenever(mockPluginLifecycle4.isLoaded).thenReturn(true)
// Set the current clock to the final clock to load
registry.applySettings(ClockSettings("clock_4", null))
scheduler.runCurrent()
// When ClockRegistry attempts to unload a plugin, we at that point decide to load and
// unload other plugins. This causes ClockRegistry to modify the list of available clock
// plugins while it is being iterated over. In production this happens as a result of a
// thread race, instead of synchronously like it does here.
whenever(mockPluginLifecycle2.unloadPlugin()).then {
pluginListener.onPluginDetached(mockPluginLifecycle1)
pluginListener.onPluginLoaded(plugin4, mockContext, mockPluginLifecycle4)
}
// Load initial plugins
pluginListener.onPluginLoaded(plugin1, mockContext, mockPluginLifecycle1)
pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2)
pluginListener.onPluginLoaded(plugin3, mockContext, mockPluginLifecycle3)
// Repeatedly verify the loaded providers to get final state
registry.verifyLoadedProviders()
scheduler.runCurrent()
registry.verifyLoadedProviders()
scheduler.runCurrent()
// Verify all plugins were correctly loaded into the registry
assertEquals(registry.getClocks().toSet(), setOf(
ClockMetadata("DEFAULT", "Default Clock"),
ClockMetadata("clock_2", "clock 2"),
ClockMetadata("clock_3", "clock 3"),
ClockMetadata("clock_4", "clock 4")
))
}
@Test
fun jsonDeserialization_gotExpectedObject() {
val expected = ClockSettings("ID", null).apply {
metadata.put("appliedTimestamp", 500)
}
val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"metadata": {
"appliedTimestamp":500
}
}""")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_noTimestamp_gotExpectedObject() {
val expected = ClockSettings("ID", null)
val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_nullTimestamp_gotExpectedObject() {
val expected = ClockSettings("ID", null)
val actual = ClockSettings.deserialize("""{
"clockId":"ID",
"metadata":null
}""")
assertEquals(expected, actual)
}
@Test
fun jsonDeserialization_noId_deserializedEmpty() {
val expected = ClockSettings(null, null).apply {
metadata.put("appliedTimestamp", 500)
}
val actual = ClockSettings.deserialize("{\"metadata\":{\"appliedTimestamp\":500}}")
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_gotExpectedString() {
val expected = "{\"clockId\":\"ID\",\"metadata\":{\"appliedTimestamp\":500}}"
val actual = ClockSettings.serialize(ClockSettings("ID", null).apply {
metadata.put("appliedTimestamp", 500)
})
assertEquals(expected, actual)
}
@Test
fun jsonSerialization_noTimestamp_gotExpectedString() {
val expected = "{\"clockId\":\"ID\",\"metadata\":{}}"
val actual = ClockSettings.serialize(ClockSettings("ID", null))
assertEquals(expected, actual)
}
@Test
fun testTransitClockEnabled_hasTransitClock() {
testTransitClockFlag(true)
}
@Test
fun testTransitClockDisabled_noTransitClock() {
testTransitClockFlag(false)
}
private fun testTransitClockFlag(flag: Boolean) {
featureFlags.set(TRANSIT_CLOCK, flag)
registry.isTransitClockEnabled = featureFlags.isEnabled(TRANSIT_CLOCK)
val mockPluginLifecycle = mock<PluginLifecycleManager<ClockProviderPlugin>>()
val plugin = FakeClockPlugin()
.addClock("clock_1", "clock 1")
.addClock("DIGITAL_CLOCK_METRO", "metro clock")
pluginListener.onPluginLoaded(plugin, mockContext, mockPluginLifecycle)
val list = registry.getClocks()
if (flag) {
assertEquals(
setOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1"),
ClockMetadata("DIGITAL_CLOCK_METRO", "metro clock")
),
list.toSet()
)
} else {
assertEquals(
setOf(
ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
ClockMetadata("clock_1", "clock 1")
),
list.toSet()
)
}
}
}