1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 package com.android.systemui.shared.clocks 17 18 import android.content.ComponentName 19 import android.content.ContentResolver 20 import android.content.Context 21 import android.graphics.drawable.Drawable 22 import android.testing.AndroidTestingRunner 23 import androidx.test.filters.SmallTest 24 import com.android.systemui.SysuiTestCase 25 import com.android.systemui.flags.FakeFeatureFlags 26 import com.android.systemui.flags.Flags.TRANSIT_CLOCK 27 import com.android.systemui.plugins.ClockController 28 import com.android.systemui.plugins.ClockId 29 import com.android.systemui.plugins.ClockMetadata 30 import com.android.systemui.plugins.ClockProviderPlugin 31 import com.android.systemui.plugins.ClockSettings 32 import com.android.systemui.plugins.PluginLifecycleManager 33 import com.android.systemui.plugins.PluginListener 34 import com.android.systemui.plugins.PluginManager 35 import com.android.systemui.util.mockito.argumentCaptor 36 import com.android.systemui.util.mockito.eq 37 import junit.framework.Assert.assertEquals 38 import junit.framework.Assert.fail 39 import kotlinx.coroutines.CoroutineDispatcher 40 import kotlinx.coroutines.test.StandardTestDispatcher 41 import kotlinx.coroutines.test.TestCoroutineScheduler 42 import kotlinx.coroutines.test.TestScope 43 import org.junit.Before 44 import org.junit.Rule 45 import org.junit.Test 46 import org.junit.runner.RunWith 47 import org.mockito.Mock 48 import org.mockito.Mockito.never 49 import org.mockito.Mockito.spy 50 import org.mockito.Mockito.times 51 import org.mockito.Mockito.verify 52 import org.mockito.Mockito.`when` as whenever 53 import org.mockito.junit.MockitoJUnit 54 55 @RunWith(AndroidTestingRunner::class) 56 @SmallTest 57 class ClockRegistryTest : SysuiTestCase() { 58 59 @JvmField @Rule val mockito = MockitoJUnit.rule() 60 private lateinit var scheduler: TestCoroutineScheduler 61 private lateinit var dispatcher: CoroutineDispatcher 62 private lateinit var scope: TestScope 63 64 @Mock private lateinit var mockContext: Context 65 @Mock private lateinit var mockPluginManager: PluginManager 66 @Mock private lateinit var mockClock: ClockController 67 @Mock private lateinit var mockDefaultClock: ClockController 68 @Mock private lateinit var mockThumbnail: Drawable 69 @Mock private lateinit var mockContentResolver: ContentResolver 70 private lateinit var fakeDefaultProvider: FakeClockPlugin 71 private lateinit var pluginListener: PluginListener<ClockProviderPlugin> 72 private lateinit var registry: ClockRegistry 73 private val featureFlags = FakeFeatureFlags() 74 75 companion object { 76 private fun failFactory(clockId: ClockId): ClockController { 77 fail("Unexpected call to createClock: $clockId") 78 return null!! 79 } 80 81 private fun failThumbnail(clockId: ClockId): Drawable? { 82 fail("Unexpected call to getThumbnail: $clockId") 83 return null 84 } 85 } 86 87 private class FakeLifecycle( 88 private val tag: String, 89 private val plugin: ClockProviderPlugin?, 90 ) : PluginLifecycleManager<ClockProviderPlugin> { 91 var onLoad: (() -> Unit)? = null 92 var onUnload: (() -> Unit)? = null 93 94 private var mIsLoaded: Boolean = true 95 override fun isLoaded() = mIsLoaded 96 override fun getPlugin(): ClockProviderPlugin? = if (isLoaded) plugin else null 97 98 var mComponentName = ComponentName("Package[$tag]", "Class[$tag]") 99 override fun toString() = "Manager[$tag]" 100 override fun getPackage(): String = mComponentName.getPackageName() 101 override fun getComponentName(): ComponentName = mComponentName 102 103 private var isDebug: Boolean = false 104 override fun getIsDebug(): Boolean = isDebug 105 override fun setIsDebug(value: Boolean) { isDebug = value } 106 107 override fun loadPlugin() { 108 if (!mIsLoaded) { 109 mIsLoaded = true 110 onLoad?.invoke() 111 } 112 } 113 114 override fun unloadPlugin() { 115 if (mIsLoaded) { 116 mIsLoaded = false 117 onUnload?.invoke() 118 } 119 } 120 } 121 122 private class FakeClockPlugin : ClockProviderPlugin { 123 private val metadata = mutableListOf<ClockMetadata>() 124 private val createCallbacks = mutableMapOf<ClockId, (ClockId) -> ClockController>() 125 private val thumbnailCallbacks = mutableMapOf<ClockId, (ClockId) -> Drawable?>() 126 127 override fun getClocks() = metadata 128 override fun createClock(settings: ClockSettings): ClockController = 129 createCallbacks[settings.clockId!!]!!(settings.clockId!!) 130 override fun getClockThumbnail(id: ClockId): Drawable? = thumbnailCallbacks[id]!!(id) 131 132 fun addClock( 133 id: ClockId, 134 name: String, 135 create: (ClockId) -> ClockController = ::failFactory, 136 getThumbnail: (ClockId) -> Drawable? = ::failThumbnail 137 ): FakeClockPlugin { 138 metadata.add(ClockMetadata(id, name)) 139 createCallbacks[id] = create 140 thumbnailCallbacks[id] = getThumbnail 141 return this 142 } 143 } 144 145 @Before 146 fun setUp() { 147 scheduler = TestCoroutineScheduler() 148 dispatcher = StandardTestDispatcher(scheduler) 149 scope = TestScope(dispatcher) 150 151 fakeDefaultProvider = FakeClockPlugin() 152 .addClock(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME, { mockDefaultClock }, { mockThumbnail }) 153 whenever(mockContext.contentResolver).thenReturn(mockContentResolver) 154 155 val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>() 156 registry = object : ClockRegistry( 157 mockContext, 158 mockPluginManager, 159 scope = scope.backgroundScope, 160 mainDispatcher = dispatcher, 161 bgDispatcher = dispatcher, 162 isEnabled = true, 163 handleAllUsers = true, 164 defaultClockProvider = fakeDefaultProvider, 165 keepAllLoaded = false, 166 subTag = "Test", 167 ) { 168 override fun querySettings() { } 169 override fun applySettings(value: ClockSettings?) { 170 settings = value 171 } 172 // Unit Test does not validate threading 173 override fun assertMainThread() {} 174 override fun assertNotMainThread() {} 175 } 176 registry.registerListeners() 177 178 verify(mockPluginManager) 179 .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true)) 180 pluginListener = captor.value 181 } 182 183 @Test 184 fun pluginRegistration_CorrectState() { 185 val plugin1 = FakeClockPlugin() 186 .addClock("clock_1", "clock 1") 187 .addClock("clock_2", "clock 2") 188 val lifecycle1 = FakeLifecycle("1", plugin1) 189 190 val plugin2 = FakeClockPlugin() 191 .addClock("clock_3", "clock 3") 192 .addClock("clock_4", "clock 4") 193 val lifecycle2 = FakeLifecycle("2", plugin2) 194 195 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 196 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 197 val list = registry.getClocks() 198 assertEquals( 199 list.toSet(), 200 setOf( 201 ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), 202 ClockMetadata("clock_1", "clock 1"), 203 ClockMetadata("clock_2", "clock 2"), 204 ClockMetadata("clock_3", "clock 3"), 205 ClockMetadata("clock_4", "clock 4") 206 ) 207 ) 208 } 209 210 @Test 211 fun noPlugins_createDefaultClock() { 212 val clock = registry.createCurrentClock() 213 assertEquals(clock, mockDefaultClock) 214 } 215 216 @Test 217 fun clockIdConflict_ErrorWithoutCrash_unloadDuplicate() { 218 val plugin1 = FakeClockPlugin() 219 .addClock("clock_1", "clock 1", { mockClock }, { mockThumbnail }) 220 .addClock("clock_2", "clock 2", { mockClock }, { mockThumbnail }) 221 val lifecycle1 = spy(FakeLifecycle("1", plugin1)) 222 223 val plugin2 = FakeClockPlugin() 224 .addClock("clock_1", "clock 1") 225 .addClock("clock_2", "clock 2") 226 val lifecycle2 = spy(FakeLifecycle("2", plugin2)) 227 228 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 229 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 230 val list = registry.getClocks() 231 assertEquals( 232 list.toSet(), 233 setOf( 234 ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), 235 ClockMetadata("clock_1", "clock 1"), 236 ClockMetadata("clock_2", "clock 2") 237 ) 238 ) 239 240 assertEquals(registry.createExampleClock("clock_1"), mockClock) 241 assertEquals(registry.createExampleClock("clock_2"), mockClock) 242 assertEquals(registry.getClockThumbnail("clock_1"), mockThumbnail) 243 assertEquals(registry.getClockThumbnail("clock_2"), mockThumbnail) 244 verify(lifecycle1, never()).unloadPlugin() 245 verify(lifecycle2, times(2)).unloadPlugin() 246 } 247 248 @Test 249 fun createCurrentClock_pluginConnected() { 250 val plugin1 = FakeClockPlugin() 251 .addClock("clock_1", "clock 1") 252 .addClock("clock_2", "clock 2") 253 val lifecycle1 = spy(FakeLifecycle("1", plugin1)) 254 255 val plugin2 = FakeClockPlugin() 256 .addClock("clock_3", "clock 3", { mockClock }) 257 .addClock("clock_4", "clock 4") 258 val lifecycle2 = spy(FakeLifecycle("2", plugin2)) 259 260 registry.applySettings(ClockSettings("clock_3", null)) 261 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 262 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 263 264 val clock = registry.createCurrentClock() 265 assertEquals(mockClock, clock) 266 } 267 268 @Test 269 fun activeClockId_changeAfterPluginConnected() { 270 val plugin1 = FakeClockPlugin() 271 .addClock("clock_1", "clock 1") 272 .addClock("clock_2", "clock 2") 273 val lifecycle1 = spy(FakeLifecycle("1", plugin1)) 274 275 val plugin2 = FakeClockPlugin() 276 .addClock("clock_3", "clock 3", { mockClock }) 277 .addClock("clock_4", "clock 4") 278 val lifecycle2 = spy(FakeLifecycle("2", plugin2)) 279 280 registry.applySettings(ClockSettings("clock_3", null)) 281 282 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 283 assertEquals(DEFAULT_CLOCK_ID, registry.activeClockId) 284 285 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 286 assertEquals("clock_3", registry.activeClockId) 287 } 288 289 @Test 290 fun createDefaultClock_pluginDisconnected() { 291 val plugin1 = FakeClockPlugin() 292 .addClock("clock_1", "clock 1") 293 .addClock("clock_2", "clock 2") 294 val lifecycle1 = spy(FakeLifecycle("1", plugin1)) 295 296 val plugin2 = FakeClockPlugin() 297 .addClock("clock_3", "clock 3") 298 .addClock("clock_4", "clock 4") 299 val lifecycle2 = spy(FakeLifecycle("2", plugin2)) 300 301 registry.applySettings(ClockSettings("clock_3", null)) 302 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 303 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 304 pluginListener.onPluginUnloaded(plugin2, lifecycle2) 305 306 val clock = registry.createCurrentClock() 307 assertEquals(clock, mockDefaultClock) 308 } 309 310 @Test 311 fun pluginRemoved_clockAndListChanged() { 312 val plugin1 = FakeClockPlugin() 313 .addClock("clock_1", "clock 1") 314 .addClock("clock_2", "clock 2") 315 val lifecycle1 = spy(FakeLifecycle("1", plugin1)) 316 317 val plugin2 = FakeClockPlugin() 318 .addClock("clock_3", "clock 3", { mockClock }) 319 .addClock("clock_4", "clock 4") 320 val lifecycle2 = spy(FakeLifecycle("2", plugin2)) 321 322 var changeCallCount = 0 323 var listChangeCallCount = 0 324 registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { 325 override fun onCurrentClockChanged() { changeCallCount++ } 326 override fun onAvailableClocksChanged() { listChangeCallCount++ } 327 }) 328 329 registry.applySettings(ClockSettings("clock_3", null)) 330 scheduler.runCurrent() 331 assertEquals(1, changeCallCount) 332 assertEquals(0, listChangeCallCount) 333 334 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 335 scheduler.runCurrent() 336 assertEquals(1, changeCallCount) 337 assertEquals(1, listChangeCallCount) 338 339 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 340 scheduler.runCurrent() 341 assertEquals(2, changeCallCount) 342 assertEquals(2, listChangeCallCount) 343 344 pluginListener.onPluginUnloaded(plugin1, lifecycle1) 345 scheduler.runCurrent() 346 assertEquals(2, changeCallCount) 347 assertEquals(2, listChangeCallCount) 348 349 pluginListener.onPluginUnloaded(plugin2, lifecycle2) 350 scheduler.runCurrent() 351 assertEquals(3, changeCallCount) 352 assertEquals(2, listChangeCallCount) 353 354 pluginListener.onPluginDetached(lifecycle1) 355 scheduler.runCurrent() 356 assertEquals(3, changeCallCount) 357 assertEquals(3, listChangeCallCount) 358 359 pluginListener.onPluginDetached(lifecycle2) 360 scheduler.runCurrent() 361 assertEquals(3, changeCallCount) 362 assertEquals(4, listChangeCallCount) 363 } 364 365 @Test 366 fun unknownPluginAttached_clockAndListUnchanged_loadRequested() { 367 val lifecycle = FakeLifecycle("", null).apply { 368 mComponentName = ComponentName("some.other.package", "SomeClass") 369 } 370 371 var changeCallCount = 0 372 var listChangeCallCount = 0 373 registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { 374 override fun onCurrentClockChanged() { changeCallCount++ } 375 override fun onAvailableClocksChanged() { listChangeCallCount++ } 376 }) 377 378 assertEquals(true, pluginListener.onPluginAttached(lifecycle)) 379 scheduler.runCurrent() 380 assertEquals(0, changeCallCount) 381 assertEquals(0, listChangeCallCount) 382 } 383 384 @Test 385 fun knownPluginAttached_clockAndListChanged_notLoaded() { 386 val lifecycle1 = FakeLifecycle("Metro", null).apply { 387 mComponentName = ComponentName("com.android.systemui.clocks.metro", "MetroClock") 388 } 389 val lifecycle2 = FakeLifecycle("BigNum", null).apply { 390 mComponentName = ComponentName("com.android.systemui.clocks.bignum", "BigNumClock") 391 } 392 393 var changeCallCount = 0 394 var listChangeCallCount = 0 395 registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { 396 override fun onCurrentClockChanged() { changeCallCount++ } 397 override fun onAvailableClocksChanged() { listChangeCallCount++ } 398 }) 399 400 registry.applySettings(ClockSettings("DIGITAL_CLOCK_CALLIGRAPHY", null)) 401 scheduler.runCurrent() 402 assertEquals(1, changeCallCount) 403 assertEquals(0, listChangeCallCount) 404 405 assertEquals(false, pluginListener.onPluginAttached(lifecycle1)) 406 scheduler.runCurrent() 407 assertEquals(1, changeCallCount) 408 assertEquals(1, listChangeCallCount) 409 410 assertEquals(false, pluginListener.onPluginAttached(lifecycle2)) 411 scheduler.runCurrent() 412 assertEquals(1, changeCallCount) 413 assertEquals(2, listChangeCallCount) 414 } 415 416 @Test 417 fun pluginAddRemove_concurrentModification() { 418 val plugin1 = FakeClockPlugin().addClock("clock_1", "clock 1") 419 val lifecycle1 = FakeLifecycle("1", plugin1) 420 val plugin2 = FakeClockPlugin().addClock("clock_2", "clock 2") 421 val lifecycle2 = FakeLifecycle("2", plugin2) 422 val plugin3 = FakeClockPlugin().addClock("clock_3", "clock 3") 423 val lifecycle3 = FakeLifecycle("3", plugin3) 424 val plugin4 = FakeClockPlugin().addClock("clock_4", "clock 4") 425 val lifecycle4 = FakeLifecycle("4", plugin4) 426 427 // Set the current clock to the final clock to load 428 registry.applySettings(ClockSettings("clock_4", null)) 429 scheduler.runCurrent() 430 431 // When ClockRegistry attempts to unload a plugin, we at that point decide to load and 432 // unload other plugins. This causes ClockRegistry to modify the list of available clock 433 // plugins while it is being iterated over. In production this happens as a result of a 434 // thread race, instead of synchronously like it does here. 435 lifecycle2.onUnload = { 436 pluginListener.onPluginDetached(lifecycle1) 437 pluginListener.onPluginLoaded(plugin4, mockContext, lifecycle4) 438 } 439 440 // Load initial plugins 441 pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) 442 pluginListener.onPluginLoaded(plugin2, mockContext, lifecycle2) 443 pluginListener.onPluginLoaded(plugin3, mockContext, lifecycle3) 444 445 // Repeatedly verify the loaded providers to get final state 446 registry.verifyLoadedProviders() 447 scheduler.runCurrent() 448 registry.verifyLoadedProviders() 449 scheduler.runCurrent() 450 451 // Verify all plugins were correctly loaded into the registry 452 assertEquals(registry.getClocks().toSet(), setOf( 453 ClockMetadata("DEFAULT", "Default Clock"), 454 ClockMetadata("clock_2", "clock 2"), 455 ClockMetadata("clock_3", "clock 3"), 456 ClockMetadata("clock_4", "clock 4") 457 )) 458 } 459 460 @Test 461 fun jsonDeserialization_gotExpectedObject() { 462 val expected = ClockSettings("ID", null).apply { 463 metadata.put("appliedTimestamp", 500) 464 } 465 val actual = ClockSettings.deserialize("""{ 466 "clockId":"ID", 467 "metadata": { 468 "appliedTimestamp":500 469 } 470 }""") 471 assertEquals(expected, actual) 472 } 473 474 @Test 475 fun jsonDeserialization_noTimestamp_gotExpectedObject() { 476 val expected = ClockSettings("ID", null) 477 val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}") 478 assertEquals(expected, actual) 479 } 480 481 @Test 482 fun jsonDeserialization_nullTimestamp_gotExpectedObject() { 483 val expected = ClockSettings("ID", null) 484 val actual = ClockSettings.deserialize("""{ 485 "clockId":"ID", 486 "metadata":null 487 }""") 488 assertEquals(expected, actual) 489 } 490 491 @Test 492 fun jsonDeserialization_noId_deserializedEmpty() { 493 val expected = ClockSettings(null, null).apply { 494 metadata.put("appliedTimestamp", 500) 495 } 496 val actual = ClockSettings.deserialize("{\"metadata\":{\"appliedTimestamp\":500}}") 497 assertEquals(expected, actual) 498 } 499 500 @Test 501 fun jsonSerialization_gotExpectedString() { 502 val expected = "{\"clockId\":\"ID\",\"metadata\":{\"appliedTimestamp\":500}}" 503 val actual = ClockSettings.serialize(ClockSettings("ID", null).apply { 504 metadata.put("appliedTimestamp", 500) 505 }) 506 assertEquals(expected, actual) 507 } 508 509 @Test 510 fun jsonSerialization_noTimestamp_gotExpectedString() { 511 val expected = "{\"clockId\":\"ID\",\"metadata\":{}}" 512 val actual = ClockSettings.serialize(ClockSettings("ID", null)) 513 assertEquals(expected, actual) 514 } 515 516 @Test 517 fun testTransitClockEnabled_hasTransitClock() { 518 testTransitClockFlag(true) 519 } 520 521 @Test 522 fun testTransitClockDisabled_noTransitClock() { 523 testTransitClockFlag(false) 524 } 525 526 private fun testTransitClockFlag(flag: Boolean) { 527 featureFlags.set(TRANSIT_CLOCK, flag) 528 registry.isTransitClockEnabled = featureFlags.isEnabled(TRANSIT_CLOCK) 529 val plugin = FakeClockPlugin() 530 .addClock("clock_1", "clock 1") 531 .addClock("DIGITAL_CLOCK_METRO", "metro clock") 532 val lifecycle = FakeLifecycle("metro", plugin) 533 pluginListener.onPluginLoaded(plugin, mockContext, lifecycle) 534 535 val list = registry.getClocks() 536 if (flag) { 537 assertEquals( 538 setOf( 539 ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), 540 ClockMetadata("clock_1", "clock 1"), 541 ClockMetadata("DIGITAL_CLOCK_METRO", "metro clock") 542 ), 543 list.toSet() 544 ) 545 } else { 546 assertEquals( 547 setOf( 548 ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), 549 ClockMetadata("clock_1", "clock 1") 550 ), 551 list.toSet() 552 ) 553 } 554 } 555 } 556