Merge "Guard off thread list mutations of the available clock map" into udc-dev

This commit is contained in:
Hawkwood Glazier
2023-03-31 23:08:24 +00:00
committed by Android (Google) Code Review
2 changed files with 70 additions and 20 deletions

View File

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

View File

@@ -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() {