diff options
| -rw-r--r-- | packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt | 33 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt | 57 |
2 files changed, 70 insertions, 20 deletions
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index e73afe74c03d..a7e95b58a6e4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -33,6 +33,7 @@ import com.android.systemui.plugins.PluginLifecycleManager import com.android.systemui.plugins.PluginListener import com.android.systemui.plugins.PluginManager import com.android.systemui.util.Assert +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -41,6 +42,18 @@ import kotlinx.coroutines.launch private const val DEBUG = true private val KEY_TIMESTAMP = "appliedTimestamp" +private fun <TKey, TVal> ConcurrentHashMap<TKey, TVal>.concurrentGetOrPut( + key: TKey, + value: TVal, + onNew: () -> Unit +): TVal { + val result = this.putIfAbsent(key, value) + if (result == null) { + onNew() + } + return result ?: value +} + /** ClockRegistry aggregates providers and plugins */ open class ClockRegistry( val context: Context, @@ -64,7 +77,7 @@ open class ClockRegistry( fun onAvailableClocksChanged() {} } - private val availableClocks = mutableMapOf<ClockId, ClockInfo>() + private val availableClocks = ConcurrentHashMap<ClockId, ClockInfo>() private val clockChangeListeners = mutableListOf<ClockChangeListener>() private val settingObserver = object : ContentObserver(null) { @@ -92,18 +105,12 @@ open class ClockRegistry( var isClockListChanged = false for (clock in plugin.getClocks()) { val id = clock.clockId - var isNew = false val info = - availableClocks.getOrPut(id) { - isNew = true - ClockInfo(clock, plugin, manager) + availableClocks.concurrentGetOrPut(id, ClockInfo(clock, plugin, manager)) { + isClockListChanged = true + onConnected(id) } - if (isNew) { - isClockListChanged = true - onConnected(id) - } - if (manager != info.manager) { Log.e( TAG, @@ -254,10 +261,8 @@ open class ClockRegistry( return } - android.util.Log.e("HAWK", "triggerOnCurrentClockChanged") scope.launch(mainDispatcher) { assertMainThread() - android.util.Log.e("HAWK", "isClockChanged") isClockChanged.set(false) clockChangeListeners.forEach { it.onCurrentClockChanged() } } @@ -270,10 +275,8 @@ open class ClockRegistry( return } - android.util.Log.e("HAWK", "triggerOnAvailableClocksChanged") scope.launch(mainDispatcher) { assertMainThread() - android.util.Log.e("HAWK", "isClockListChanged") isClockListChanged.set(false) clockChangeListeners.forEach { it.onAvailableClocksChanged() } } @@ -356,7 +359,7 @@ open class ClockRegistry( } private var isVerifying = AtomicBoolean(false) - private fun verifyLoadedProviders() { + fun verifyLoadedProviders() { val shouldSchedule = isVerifying.compareAndSet(false, true) if (!shouldSchedule) { return diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 374aae1f2948..78f5bf20e6f9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -122,7 +122,7 @@ class ClockRegistryTest : SysuiTestCase() { isEnabled = true, handleAllUsers = true, defaultClockProvider = fakeDefaultProvider, - keepAllLoaded = true, + keepAllLoaded = false, subTag = "Test", ) { override fun querySettings() { } @@ -154,8 +154,8 @@ class ClockRegistryTest : SysuiTestCase() { pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle) val list = registry.getClocks() assertEquals( - list, - listOf( + list.toSet(), + setOf( ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), ClockMetadata("clock_1", "clock 1"), ClockMetadata("clock_2", "clock 2"), @@ -187,8 +187,8 @@ class ClockRegistryTest : SysuiTestCase() { pluginListener.onPluginLoaded(plugin2, mockContext, mockPluginLifecycle2) val list = registry.getClocks() assertEquals( - list, - listOf( + list.toSet(), + setOf( ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME), ClockMetadata("clock_1", "clock 1"), ClockMetadata("clock_2", "clock 2") @@ -293,6 +293,53 @@ class ClockRegistryTest : SysuiTestCase() { assertEquals(4, 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() { |