Adds listener support to FakeFeatureFlags

Implements addListener/removeListener with extensive tests.
Does not attempt to simulate 'requestNoRestart' option.

Bug: 244205755
Test: atest FakeFeatureFlagsTest
Change-Id: Ida5911279853744b23c6b139848e99bebc7e6cb2
This commit is contained in:
Mark Renouf
2022-08-28 12:15:57 -04:00
parent 0ff178f973
commit 170ca42562
2 changed files with 171 additions and 22 deletions

View File

@@ -41,7 +41,7 @@ class FakeFeatureFlagsTest : SysuiTestCase() {
* specified. If not, an exception is thrown.
*/
@Test
fun throwsIfUnspecifiedFlagIsAccessed() {
fun accessingUnspecifiedFlags_throwsException() {
val flags: FeatureFlags = FakeFeatureFlags()
try {
assertThat(flags.isEnabled(Flags.TEAMFOOD)).isFalse()
@@ -88,7 +88,7 @@ class FakeFeatureFlagsTest : SysuiTestCase() {
}
@Test
fun specifiedFlagsReturnCorrectValues() {
fun specifiedFlags_returnCorrectValues() {
val flags = FakeFeatureFlags()
flags.set(unreleasedFlag, false)
flags.set(releasedFlag, false)
@@ -114,4 +114,125 @@ class FakeFeatureFlagsTest : SysuiTestCase() {
assertThat(flags.isEnabled(sysPropBooleanFlag)).isTrue()
assertThat(flags.getString(resourceStringFlag)).isEqualTo("Android")
}
@Test
fun listenerForBooleanFlag_calledOnlyWhenFlagChanged() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(unreleasedFlag, listener)
flags.set(unreleasedFlag, true)
flags.set(unreleasedFlag, true)
flags.set(unreleasedFlag, false)
flags.set(unreleasedFlag, false)
listener.verifyInOrder(unreleasedFlag.id, unreleasedFlag.id)
}
@Test
fun listenerForStringFlag_calledOnlyWhenFlagChanged() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(stringFlag, listener)
flags.set(stringFlag, "Test")
flags.set(stringFlag, "Test")
listener.verifyInOrder(stringFlag.id)
}
@Test
fun listenerForBooleanFlag_notCalledAfterRemoved() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(unreleasedFlag, listener)
flags.set(unreleasedFlag, true)
flags.removeListener(listener)
flags.set(unreleasedFlag, false)
listener.verifyInOrder(unreleasedFlag.id)
}
@Test
fun listenerForStringFlag_notCalledAfterRemoved() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(stringFlag, listener)
flags.set(stringFlag, "Test")
flags.removeListener(listener)
flags.set(stringFlag, "Other")
listener.verifyInOrder(stringFlag.id)
}
@Test
fun listenerForMultipleFlags_calledWhenFlagsChange() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(unreleasedFlag, listener)
flags.addListener(releasedFlag, listener)
flags.set(releasedFlag, true)
flags.set(unreleasedFlag, true)
listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
}
@Test
fun listenerForMultipleFlags_notCalledAfterRemoved() {
val flags = FakeFeatureFlags()
val listener = VerifyingListener()
flags.addListener(unreleasedFlag, listener)
flags.addListener(releasedFlag, listener)
flags.set(releasedFlag, true)
flags.set(unreleasedFlag, true)
flags.removeListener(listener)
flags.set(releasedFlag, false)
flags.set(unreleasedFlag, false)
listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
}
@Test
fun multipleListenersForSingleFlag_allAreCalledWhenChanged() {
val flags = FakeFeatureFlags()
val listener1 = VerifyingListener()
val listener2 = VerifyingListener()
flags.addListener(releasedFlag, listener1)
flags.addListener(releasedFlag, listener2)
flags.set(releasedFlag, true)
listener1.verifyInOrder(releasedFlag.id)
listener2.verifyInOrder(releasedFlag.id)
}
@Test
fun multipleListenersForSingleFlag_removedListenerNotCalledAfterRemoval() {
val flags = FakeFeatureFlags()
val listener1 = VerifyingListener()
val listener2 = VerifyingListener()
flags.addListener(releasedFlag, listener1)
flags.addListener(releasedFlag, listener2)
flags.set(releasedFlag, true)
flags.removeListener(listener2)
flags.set(releasedFlag, false)
listener1.verifyInOrder(releasedFlag.id, releasedFlag.id)
listener2.verifyInOrder(releasedFlag.id)
}
class VerifyingListener : FlagListenable.Listener {
var flagEventIds = mutableListOf<Int>()
override fun onFlagChanged(event: FlagListenable.FlagEvent) {
flagEventIds.add(event.flagId)
}
fun verifyInOrder(vararg eventIds: Int) {
assertThat(flagEventIds).containsExactlyElementsIn(eventIds.asList())
}
}
}

View File

@@ -16,14 +16,12 @@
package com.android.systemui.flags
import android.util.SparseArray
import android.util.SparseBooleanArray
import androidx.core.util.containsKey
class FakeFeatureFlags : FeatureFlags {
private val booleanFlags = SparseBooleanArray()
private val stringFlags = SparseArray<String>()
private val booleanFlags = mutableMapOf<Int, Boolean>()
private val stringFlags = mutableMapOf<Int, String>()
private val knownFlagNames = mutableMapOf<Int, String>()
private val flagListeners = mutableMapOf<Int, MutableSet<FlagListenable.Listener>>()
private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
init {
Flags.getFlagFields().forEach { field ->
@@ -33,27 +31,52 @@ class FakeFeatureFlags : FeatureFlags {
}
fun set(flag: BooleanFlag, value: Boolean) {
booleanFlags.put(flag.id, value)
if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
notifyFlagChanged(flag)
}
}
fun set(flag: DeviceConfigBooleanFlag, value: Boolean) {
booleanFlags.put(flag.id, value)
if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
notifyFlagChanged(flag)
}
}
fun set(flag: ResourceBooleanFlag, value: Boolean) {
booleanFlags.put(flag.id, value)
if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
notifyFlagChanged(flag)
}
}
fun set(flag: SysPropBooleanFlag, value: Boolean) {
booleanFlags.put(flag.id, value)
if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
notifyFlagChanged(flag)
}
}
fun set(flag: StringFlag, value: String) {
stringFlags.put(flag.id, value)
if (stringFlags.put(flag.id, value)?.let { value != it } == null) {
notifyFlagChanged(flag)
}
}
fun set(flag: ResourceStringFlag, value: String) {
stringFlags.put(flag.id, value)
if (stringFlags.put(flag.id, value)?.let { value != it } == null) {
notifyFlagChanged(flag)
}
}
private fun notifyFlagChanged(flag: Flag<*>) {
flagListeners[flag.id]?.let { listeners ->
listeners.forEach { listener ->
listener.onFlagChanged(
object : FlagListenable.FlagEvent {
override val flagId = flag.id
override fun requestNoRestart() {}
}
)
}
}
}
override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
@@ -70,25 +93,30 @@ class FakeFeatureFlags : FeatureFlags {
override fun getString(flag: ResourceStringFlag): String = requireStringValue(flag.id)
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {}
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
flagListeners.getOrPut(flag.id) { mutableSetOf() }.add(listener)
listenerFlagIds.getOrPut(listener) { mutableSetOf() }.add(flag.id)
}
override fun removeListener(listener: FlagListenable.Listener) {}
override fun removeListener(listener: FlagListenable.Listener) {
listenerFlagIds.remove(listener)?.let {
flagIds -> flagIds.forEach {
id -> flagListeners[id]?.remove(listener)
}
}
}
private fun flagName(flagId: Int): String {
return knownFlagNames[flagId] ?: "UNKNOWN(id=$flagId)"
}
private fun requireBooleanValue(flagId: Int): Boolean {
if (!booleanFlags.containsKey(flagId)) {
throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.")
}
return booleanFlags[flagId]
?: error("Flag ${flagName(flagId)} was accessed but not specified.")
}
private fun requireStringValue(flagId: Int): String {
if (!stringFlags.containsKey(flagId)) {
throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.")
}
return stringFlags[flagId]
?: error("Flag ${flagName(flagId)} was accessed but not specified.")
}
}