FeatureFlags Listeners:
* Convert FlagReader interface to FlagListenable. * Move Flag reading to FeatureFlags interface. * Remove isEnabled(id, boolean) from all interfaces; make private in implementations where used. Bug: 209081785 Test: atest FlagManagerTest Change-Id: I8c90a67def242b5d2fa55d5effab8270ecafabb4
This commit is contained in:
@@ -18,28 +18,24 @@ package com.android.systemui.flags
|
||||
/**
|
||||
* Plugin for loading flag values
|
||||
*/
|
||||
interface FlagReader {
|
||||
/** Returns a boolean value for the given flag. */
|
||||
fun isEnabled(flag: BooleanFlag): Boolean {
|
||||
return flag.default
|
||||
}
|
||||
|
||||
fun isEnabled(flag: ResourceBooleanFlag): Boolean
|
||||
|
||||
/** Returns a boolean value for the given flag. */
|
||||
fun isEnabled(id: Int, def: Boolean): Boolean {
|
||||
return def
|
||||
}
|
||||
|
||||
/** Add a listener to be alerted when any flag changes. */
|
||||
fun addListener(listener: Listener) {}
|
||||
interface FlagListenable {
|
||||
/** Add a listener to be alerted when the given flag changes. */
|
||||
fun addListener(flag: Flag<*>, listener: Listener)
|
||||
|
||||
/** Remove a listener to be alerted when any flag changes. */
|
||||
fun removeListener(listener: Listener) {}
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
/** A simple listener to be alerted when a flag changes. */
|
||||
fun interface Listener {
|
||||
/** */
|
||||
fun onFlagChanged(id: Int)
|
||||
/** Called when the flag changes */
|
||||
fun onFlagChanged(event: FlagEvent)
|
||||
}
|
||||
|
||||
/** An event representing the change */
|
||||
interface FlagEvent {
|
||||
/** the id of the flag which changed */
|
||||
val flagId: Int
|
||||
/** if all listeners alerted invoke this method, the restart will be skipped */
|
||||
fun requestNoRestart()
|
||||
}
|
||||
}
|
||||
@@ -24,16 +24,17 @@ import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.function.Consumer
|
||||
|
||||
class FlagManager constructor(
|
||||
private val context: Context,
|
||||
private val settings: FlagSettingsHelper,
|
||||
private val handler: Handler
|
||||
) : FlagReader {
|
||||
) : FlagListenable {
|
||||
companion object {
|
||||
const val RECEIVING_PACKAGE = "com.android.systemui"
|
||||
const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
|
||||
@@ -47,7 +48,19 @@ class FlagManager constructor(
|
||||
private const val SETTINGS_PREFIX = "systemui/flags"
|
||||
}
|
||||
|
||||
private val listeners: MutableSet<FlagReader.Listener> = mutableSetOf()
|
||||
constructor(context: Context, handler: Handler) : this(
|
||||
context,
|
||||
FlagSettingsHelper(context.contentResolver),
|
||||
handler
|
||||
)
|
||||
|
||||
/**
|
||||
* An action called on restart which takes as an argument whether the listeners requested
|
||||
* that the restart be suppressed
|
||||
*/
|
||||
var restartAction: Consumer<Boolean>? = null
|
||||
var clearCacheAction: Consumer<Int>? = null
|
||||
private val listeners: MutableSet<PerFlagListener> = mutableSetOf()
|
||||
private val settingsObserver: ContentObserver = SettingsObserver()
|
||||
|
||||
fun getFlagsFuture(): ListenableFuture<Collection<Flag<*>>> {
|
||||
@@ -86,14 +99,9 @@ class FlagManager constructor(
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
override fun isEnabled(id: Int, def: Boolean): Boolean {
|
||||
return isEnabled(id) ?: def
|
||||
}
|
||||
|
||||
/** Returns the stored value or null if not set. */
|
||||
fun isEnabled(id: Int): Boolean? {
|
||||
val data: String? = Settings.Secure.getString(
|
||||
context.contentResolver, keyToSettingsPrefix(id))
|
||||
val data = settings.getString(keyToSettingsPrefix(id))
|
||||
if (data == null || data?.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
@@ -108,27 +116,24 @@ class FlagManager constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEnabled(flag: ResourceBooleanFlag): Boolean {
|
||||
throw RuntimeException("Not implemented in FlagManager")
|
||||
}
|
||||
|
||||
override fun addListener(listener: FlagReader.Listener) {
|
||||
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
|
||||
synchronized(listeners) {
|
||||
val registerNeeded = listeners.isEmpty()
|
||||
listeners.add(listener)
|
||||
listeners.add(PerFlagListener(flag.id, listener))
|
||||
if (registerNeeded) {
|
||||
context.contentResolver.registerContentObserver(
|
||||
Settings.Secure.getUriFor(SETTINGS_PREFIX), true, settingsObserver)
|
||||
settings.registerContentObserver(SETTINGS_PREFIX, true, settingsObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeListener(listener: FlagReader.Listener) {
|
||||
override fun removeListener(listener: FlagListenable.Listener) {
|
||||
synchronized(listeners) {
|
||||
val isRegistered = !listeners.isEmpty()
|
||||
listeners.remove(listener)
|
||||
if (isRegistered && listeners.isEmpty()) {
|
||||
context.contentResolver.unregisterContentObserver(settingsObserver)
|
||||
if (listeners.isEmpty()) {
|
||||
return
|
||||
}
|
||||
listeners.removeIf { it.listener == listener }
|
||||
if (listeners.isEmpty()) {
|
||||
settings.unregisterContentObserver(settingsObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +147,7 @@ class FlagManager constructor(
|
||||
}
|
||||
|
||||
fun keyToSettingsPrefix(key: Int): String {
|
||||
return SETTINGS_PREFIX + "/" + key
|
||||
return "$SETTINGS_PREFIX/$key"
|
||||
}
|
||||
|
||||
private fun assertType(json: JSONObject, type: String): Boolean {
|
||||
@@ -160,14 +165,39 @@ class FlagManager constructor(
|
||||
}
|
||||
val parts = uri.pathSegments
|
||||
val idStr = parts[parts.size - 1]
|
||||
try {
|
||||
val id = idStr.toInt()
|
||||
listeners.forEach { l -> l.onFlagChanged(id) }
|
||||
} catch (e: NumberFormatException) {
|
||||
// no-op
|
||||
}
|
||||
val id = try { idStr.toInt() } catch (e: NumberFormatException) { return }
|
||||
clearCacheAction?.accept(id)
|
||||
dispatchListenersAndMaybeRestart(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun dispatchListenersAndMaybeRestart(id: Int) {
|
||||
val filteredListeners: List<FlagListenable.Listener> = synchronized(listeners) {
|
||||
listeners.mapNotNull { if (it.id == id) it.listener else null }
|
||||
}
|
||||
// If there are no listeners, there's nothing to dispatch to, and nothing to suppress it.
|
||||
if (filteredListeners.isEmpty()) {
|
||||
restartAction?.accept(false)
|
||||
return
|
||||
}
|
||||
// Dispatch to every listener and save whether each one called requestNoRestart.
|
||||
val suppressRestartList: List<Boolean> = filteredListeners.map { listener ->
|
||||
var didRequestNoRestart = false
|
||||
val event = object : FlagListenable.FlagEvent {
|
||||
override val flagId = id
|
||||
override fun requestNoRestart() {
|
||||
didRequestNoRestart = true
|
||||
}
|
||||
}
|
||||
listener.onFlagChanged(event)
|
||||
didRequestNoRestart
|
||||
}
|
||||
// Suppress restart only if ALL listeners request it.
|
||||
val suppressRestart = suppressRestartList.all { it }
|
||||
restartAction?.accept(suppressRestart)
|
||||
}
|
||||
|
||||
private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
|
||||
}
|
||||
|
||||
class InvalidFlagStorageException : Exception("Data found but is invalid")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.flags
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.provider.Settings
|
||||
|
||||
class FlagSettingsHelper(private val contentResolver: ContentResolver) {
|
||||
|
||||
fun getString(key: String): String? = Settings.Secure.getString(contentResolver, key)
|
||||
|
||||
fun registerContentObserver(
|
||||
name: String,
|
||||
notifyForDescendants: Boolean,
|
||||
observer: ContentObserver
|
||||
) {
|
||||
contentResolver.registerContentObserver(
|
||||
Settings.Secure.getUriFor(name),
|
||||
notifyForDescendants,
|
||||
observer
|
||||
)
|
||||
}
|
||||
|
||||
fun unregisterContentObserver(observer: ContentObserver) {
|
||||
contentResolver.unregisterContentObserver(observer)
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.systemui.flags;
|
||||
package com.android.systemui.flags
|
||||
|
||||
/**
|
||||
* Class to manage simple DeviceConfig-based feature flags.
|
||||
*
|
||||
* See {@link Flags} for instructions on defining new flags.
|
||||
* See [Flags] for instructions on defining new flags.
|
||||
*/
|
||||
public interface FeatureFlags extends FlagReader {
|
||||
}
|
||||
interface FeatureFlags : FlagListenable {
|
||||
/** Returns a boolean value for the given flag. */
|
||||
fun isEnabled(flag: BooleanFlag): Boolean
|
||||
|
||||
/** Returns a boolean value for the given flag. */
|
||||
fun isEnabled(flag: ResourceBooleanFlag): Boolean
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import static com.android.systemui.flags.FlagManager.FIELD_FLAGS;
|
||||
import static com.android.systemui.flags.FlagManager.FIELD_ID;
|
||||
import static com.android.systemui.flags.FlagManager.FIELD_VALUE;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -32,6 +31,7 @@ import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.systemui.Dumpable;
|
||||
import com.android.systemui.dagger.SysUISingleton;
|
||||
@@ -81,6 +81,8 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_SET_FLAG);
|
||||
filter.addAction(ACTION_GET_FLAGS);
|
||||
flagManager.setRestartAction(this::restartSystemUI);
|
||||
flagManager.setClearCacheAction(this::removeFromCache);
|
||||
context.registerReceiver(mReceiver, filter, null, null);
|
||||
dumpManager.registerDumpable(TAG, this);
|
||||
}
|
||||
@@ -106,9 +108,8 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
return mBooleanFlagCache.get(id);
|
||||
}
|
||||
|
||||
/** Return a {@link BooleanFlag}'s value. */
|
||||
@Override
|
||||
public boolean isEnabled(int id, boolean defaultValue) {
|
||||
/** Return a flag's value. */
|
||||
private boolean isEnabled(int id, boolean defaultValue) {
|
||||
Boolean result = isEnabledInternal(id);
|
||||
return result == null ? defaultValue : result;
|
||||
}
|
||||
@@ -136,17 +137,19 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
json.put(FlagManager.FIELD_TYPE, FlagManager.TYPE_BOOLEAN);
|
||||
json.put(FIELD_VALUE, value);
|
||||
mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), json.toString());
|
||||
Log.i(TAG, "Set id " + id + " to " + value);
|
||||
restartSystemUI();
|
||||
} catch (JSONException e) {
|
||||
// no-op
|
||||
return; // ignore
|
||||
}
|
||||
Log.i(TAG, "Set id " + id + " to " + value);
|
||||
removeFromCache(id);
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(id);
|
||||
}
|
||||
|
||||
/** Erase a flag's overridden value if there is one. */
|
||||
public void eraseFlag(int id) {
|
||||
eraseInternal(id);
|
||||
restartSystemUI();
|
||||
removeFromCache(id);
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(id);
|
||||
}
|
||||
|
||||
/** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
|
||||
@@ -157,16 +160,20 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener run) {
|
||||
mFlagManager.addListener(run);
|
||||
public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {
|
||||
mFlagManager.addListener(flag, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener run) {
|
||||
mFlagManager.removeListener(run);
|
||||
public void removeListener(@NonNull Listener listener) {
|
||||
mFlagManager.removeListener(listener);
|
||||
}
|
||||
|
||||
private void restartSystemUI() {
|
||||
private void restartSystemUI(boolean requestSuppress) {
|
||||
if (requestSuppress) {
|
||||
Log.i(TAG, "SystemUI Restart Suppressed");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "Restarting SystemUI");
|
||||
// SysUI starts back when up exited. Is there a better way to do this?
|
||||
System.exit(0);
|
||||
@@ -202,6 +209,10 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
}
|
||||
|
||||
private void handleSetFlag(Bundle extras) {
|
||||
if (extras == null) {
|
||||
Log.w(TAG, "No extras");
|
||||
return;
|
||||
}
|
||||
int id = extras.getInt(FIELD_ID);
|
||||
if (id <= 0) {
|
||||
Log.w(TAG, "ID not set or less than or equal to 0: " + id);
|
||||
@@ -245,6 +256,10 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
|
||||
}
|
||||
};
|
||||
|
||||
private void removeFromCache(int id) {
|
||||
mBooleanFlagCache.remove(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
|
||||
pw.println("can override: true");
|
||||
|
||||
@@ -48,10 +48,10 @@ public class FeatureFlagsRelease implements FeatureFlags, Dumpable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(Listener run) {}
|
||||
public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {}
|
||||
|
||||
@Override
|
||||
public void removeListener(Listener run) {}
|
||||
public void removeListener(@NonNull Listener listener) {}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(BooleanFlag flag) {
|
||||
@@ -68,8 +68,7 @@ public class FeatureFlagsRelease implements FeatureFlags, Dumpable {
|
||||
return mFlagCache.valueAt(cacheIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int key, boolean defaultValue) {
|
||||
private boolean isEnabled(int key, boolean defaultValue) {
|
||||
mFlagCache.append(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
@@ -79,8 +78,7 @@ public class FeatureFlagsRelease implements FeatureFlags, Dumpable {
|
||||
pw.println("can override: false");
|
||||
int size = mFlagCache.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
pw.println(" sysui_flag_" + mFlagCache.keyAt(i)
|
||||
+ ": " + mFlagCache.valueAt(i));
|
||||
pw.println(" sysui_flag_" + mFlagCache.keyAt(i) + ": " + mFlagCache.valueAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,21 +81,23 @@ public class FeatureFlagsReleaseTest extends SysuiTestCase {
|
||||
public void testDump() {
|
||||
int flagIdA = 213;
|
||||
int flagIdB = 18;
|
||||
int flagIdC = 1;
|
||||
int flagResourceId = 3;
|
||||
BooleanFlag flagA = new BooleanFlag(flagIdA, true);
|
||||
ResourceBooleanFlag flagB = new ResourceBooleanFlag(flagIdB, flagResourceId);
|
||||
BooleanFlag flagC = new BooleanFlag(flagIdC, false);
|
||||
when(mResources.getBoolean(flagResourceId)).thenReturn(true);
|
||||
|
||||
// WHEN the flags have been accessed
|
||||
assertThat(mFeatureFlagsRelease.isEnabled(1, false)).isFalse();
|
||||
assertThat(mFeatureFlagsRelease.isEnabled(flagA)).isTrue();
|
||||
assertThat(mFeatureFlagsRelease.isEnabled(flagB)).isTrue();
|
||||
assertThat(mFeatureFlagsRelease.isEnabled(flagC)).isFalse();
|
||||
|
||||
// THEN the dump contains the flags and the default values
|
||||
String dump = dumpToString();
|
||||
assertThat(dump).contains(" sysui_flag_1: false\n");
|
||||
assertThat(dump).contains(" sysui_flag_" + flagIdA + ": true\n");
|
||||
assertThat(dump).contains(" sysui_flag_" + flagIdB + ": true\n");
|
||||
assertThat(dump).contains(" sysui_flag_" + flagIdC + ": false\n");
|
||||
}
|
||||
|
||||
private String dumpToString() {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.systemui.flags
|
||||
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.util.mockito.any
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.android.systemui.util.mockito.withArgCaptor
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow
|
||||
* overriding, and should never return any value other than the one provided as the default.
|
||||
*/
|
||||
@SmallTest
|
||||
class FlagManagerTest : SysuiTestCase() {
|
||||
private lateinit var mFlagManager: FlagManager
|
||||
|
||||
@Mock private lateinit var mMockContext: Context
|
||||
@Mock private lateinit var mFlagSettingsHelper: FlagSettingsHelper
|
||||
@Mock private lateinit var mHandler: Handler
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
mFlagManager = FlagManager(mMockContext, mFlagSettingsHelper, mHandler)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testContentObserverAddedAndRemoved() {
|
||||
val listener1 = mock<FlagListenable.Listener>()
|
||||
val listener2 = mock<FlagListenable.Listener>()
|
||||
|
||||
// no interactions before adding listener
|
||||
verifyNoMoreInteractions(mFlagSettingsHelper)
|
||||
|
||||
// adding the first listener registers the observer
|
||||
mFlagManager.addListener(BooleanFlag(1, true), listener1)
|
||||
val observer = withArgCaptor<ContentObserver> {
|
||||
verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
|
||||
}
|
||||
verifyNoMoreInteractions(mFlagSettingsHelper)
|
||||
|
||||
// adding another listener does nothing
|
||||
mFlagManager.addListener(BooleanFlag(2, true), listener2)
|
||||
verifyNoMoreInteractions(mFlagSettingsHelper)
|
||||
|
||||
// removing the original listener does nothing with second one still present
|
||||
mFlagManager.removeListener(listener1)
|
||||
verifyNoMoreInteractions(mFlagSettingsHelper)
|
||||
|
||||
// removing the final listener unregisters the observer
|
||||
mFlagManager.removeListener(listener2)
|
||||
verify(mFlagSettingsHelper).unregisterContentObserver(eq(observer))
|
||||
verifyNoMoreInteractions(mFlagSettingsHelper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testObserverClearsCache() {
|
||||
val listener = mock<FlagListenable.Listener>()
|
||||
val clearCacheAction = mock<Consumer<Int>>()
|
||||
mFlagManager.clearCacheAction = clearCacheAction
|
||||
mFlagManager.addListener(BooleanFlag(1, true), listener)
|
||||
val observer = withArgCaptor<ContentObserver> {
|
||||
verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
|
||||
}
|
||||
observer.onChange(false, flagUri(1))
|
||||
verify(clearCacheAction).accept(eq(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testObserverInvokesListeners() {
|
||||
val listener1 = mock<FlagListenable.Listener>()
|
||||
val listener10 = mock<FlagListenable.Listener>()
|
||||
mFlagManager.addListener(BooleanFlag(1, true), listener1)
|
||||
mFlagManager.addListener(BooleanFlag(10, true), listener10)
|
||||
val observer = withArgCaptor<ContentObserver> {
|
||||
verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture())
|
||||
}
|
||||
observer.onChange(false, flagUri(1))
|
||||
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener1).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent1.flagId).isEqualTo(1)
|
||||
verifyNoMoreInteractions(listener1, listener10)
|
||||
|
||||
observer.onChange(false, flagUri(10))
|
||||
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener10).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent10.flagId).isEqualTo(10)
|
||||
verifyNoMoreInteractions(listener1, listener10)
|
||||
}
|
||||
|
||||
fun flagUri(id: Int): Uri = Uri.parse("content://settings/system/systemui/flags/$id")
|
||||
|
||||
@Test
|
||||
fun testOnlySpecificFlagListenerIsInvoked() {
|
||||
val listener1 = mock<FlagListenable.Listener>()
|
||||
val listener10 = mock<FlagListenable.Listener>()
|
||||
mFlagManager.addListener(BooleanFlag(1, true), listener1)
|
||||
mFlagManager.addListener(BooleanFlag(10, true), listener10)
|
||||
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener1).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent1.flagId).isEqualTo(1)
|
||||
verifyNoMoreInteractions(listener1, listener10)
|
||||
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(10)
|
||||
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener10).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent10.flagId).isEqualTo(10)
|
||||
verifyNoMoreInteractions(listener1, listener10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSameListenerCanBeUsedForMultipleFlags() {
|
||||
val listener = mock<FlagListenable.Listener>()
|
||||
mFlagManager.addListener(BooleanFlag(1, true), listener)
|
||||
mFlagManager.addListener(BooleanFlag(10, true), listener)
|
||||
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent1.flagId).isEqualTo(1)
|
||||
verifyNoMoreInteractions(listener)
|
||||
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(10)
|
||||
val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> {
|
||||
verify(listener, times(2)).onFlagChanged(capture())
|
||||
}
|
||||
assertThat(flagEvent10.flagId).isEqualTo(10)
|
||||
verifyNoMoreInteractions(listener)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestartWithNoListeners() {
|
||||
val restartAction = mock<Consumer<Boolean>>()
|
||||
mFlagManager.restartAction = restartAction
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
verify(restartAction).accept(eq(false))
|
||||
verifyNoMoreInteractions(restartAction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListenerCanSuppressRestart() {
|
||||
val restartAction = mock<Consumer<Boolean>>()
|
||||
mFlagManager.restartAction = restartAction
|
||||
mFlagManager.addListener(BooleanFlag(1, true)) { event ->
|
||||
event.requestNoRestart()
|
||||
}
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
verify(restartAction).accept(eq(true))
|
||||
verifyNoMoreInteractions(restartAction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListenerOnlySuppressesRestartForOwnFlag() {
|
||||
val restartAction = mock<Consumer<Boolean>>()
|
||||
mFlagManager.restartAction = restartAction
|
||||
mFlagManager.addListener(BooleanFlag(10, true)) { event ->
|
||||
event.requestNoRestart()
|
||||
}
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
verify(restartAction).accept(eq(false))
|
||||
verifyNoMoreInteractions(restartAction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestartWhenNotAllListenersRequestSuppress() {
|
||||
val restartAction = mock<Consumer<Boolean>>()
|
||||
mFlagManager.restartAction = restartAction
|
||||
mFlagManager.addListener(BooleanFlag(10, true)) { event ->
|
||||
event.requestNoRestart()
|
||||
}
|
||||
mFlagManager.addListener(BooleanFlag(10, true)) {
|
||||
// do not request
|
||||
}
|
||||
mFlagManager.dispatchListenersAndMaybeRestart(1)
|
||||
verify(restartAction).accept(eq(false))
|
||||
verifyNoMoreInteractions(restartAction)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user