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