Generalize some flag manager methods by flag type.

Bug: 209081785
Test: atest FeatureFlagsDebugTest FlagManagerTest
Change-Id: Ie92667e33e7c086258cdd03e7b9ebccdefef5938
This commit is contained in:
Jeff DeCew
2021-12-03 20:12:54 +00:00
parent 595956489d
commit 3b6176a6bd
11 changed files with 784 additions and 202 deletions

View File

@@ -26,8 +26,6 @@ import android.os.Bundle
import android.os.Handler
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(
@@ -40,11 +38,9 @@ class FlagManager constructor(
const val ACTION_SET_FLAG = "com.android.systemui.action.SET_FLAG"
const val ACTION_GET_FLAGS = "com.android.systemui.action.GET_FLAGS"
const val FLAGS_PERMISSION = "com.android.systemui.permission.FLAGS"
const val FIELD_ID = "id"
const val FIELD_VALUE = "value"
const val FIELD_TYPE = "type"
const val FIELD_FLAGS = "flags"
const val TYPE_BOOLEAN = "boolean"
const val EXTRA_ID = "id"
const val EXTRA_VALUE = "value"
const val EXTRA_FLAGS = "flags"
private const val SETTINGS_PREFIX = "systemui/flags"
}
@@ -74,7 +70,7 @@ class FlagManager constructor(
override fun onReceive(context: Context, intent: Intent) {
val extras: Bundle? = getResultExtras(false)
val listOfFlags: java.util.ArrayList<ParcelableFlag<*>>? =
extras?.getParcelableArrayList(FIELD_FLAGS)
extras?.getParcelableArrayList(EXTRA_FLAGS)
if (listOfFlags != null) {
completer.set(listOfFlags)
} else {
@@ -86,9 +82,19 @@ class FlagManager constructor(
} as ListenableFuture<Collection<Flag<*>>>
}
/**
* Returns the stored value or null if not set.
* This API is used by TheFlippinApp.
*/
fun isEnabled(id: Int): Boolean? = readFlagValue(id, BooleanFlagSerializer)
/**
* Sets the value of a boolean flag.
* This API is used by TheFlippinApp.
*/
fun setFlagValue(id: Int, enabled: Boolean) {
val intent = createIntent(id)
intent.putExtra(FIELD_VALUE, enabled)
intent.putExtra(EXTRA_VALUE, enabled)
context.sendBroadcast(intent)
}
@@ -100,20 +106,9 @@ class FlagManager constructor(
}
/** Returns the stored value or null if not set. */
fun isEnabled(id: Int): Boolean? {
val data = settings.getString(keyToSettingsPrefix(id))
if (data == null || data?.isEmpty()) {
return null
}
val json: JSONObject
try {
json = JSONObject(data)
return if (!assertType(json, TYPE_BOOLEAN)) {
null
} else json.getBoolean(FIELD_VALUE)
} catch (e: JSONException) {
throw InvalidFlagStorageException()
}
fun <T> readFlagValue(id: Int, serializer: FlagSerializer<T>): T? {
val data = settings.getString(idToSettingsKey(id))
return serializer.fromSettingsData(data)
}
override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
@@ -141,21 +136,13 @@ class FlagManager constructor(
private fun createIntent(id: Int): Intent {
val intent = Intent(ACTION_SET_FLAG)
intent.setPackage(RECEIVING_PACKAGE)
intent.putExtra(FIELD_ID, id)
intent.putExtra(EXTRA_ID, id)
return intent
}
fun keyToSettingsPrefix(key: Int): String {
return "$SETTINGS_PREFIX/$key"
}
private fun assertType(json: JSONObject, type: String): Boolean {
return try {
json.getString(FIELD_TYPE) == TYPE_BOOLEAN
} catch (e: JSONException) {
false
}
fun idToSettingsKey(id: Int): String {
return "$SETTINGS_PREFIX/$id"
}
inner class SettingsObserver : ContentObserver(handler) {
@@ -200,7 +187,5 @@ class FlagManager constructor(
private data class PerFlagListener(val id: Int, val listener: FlagListenable.Listener)
}
class InvalidFlagStorageException : Exception("Data found but is invalid")
class NoFlagResultsException : Exception(
"SystemUI failed to communicate its flags back successfully")

View File

@@ -0,0 +1,80 @@
/*
* 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.util.Log
import org.json.JSONException
import org.json.JSONObject
private const val FIELD_VALUE = "value"
private const val FIELD_TYPE = "type"
private const val TYPE_BOOLEAN = "boolean"
private const val TYPE_STRING = "string"
private const val TAG = "FlagSerializer"
abstract class FlagSerializer<T>(
private val type: String,
private val setter: (JSONObject, String, T) -> Unit,
private val getter: (JSONObject, String) -> T
) {
fun toSettingsData(value: T): String? {
return try {
JSONObject()
.put(FIELD_TYPE, type)
.also { setter(it, FIELD_VALUE, value) }
.toString()
} catch (e: JSONException) {
Log.w(TAG, "write error", e)
null
}
}
/**
* @throws InvalidFlagStorageException
*/
fun fromSettingsData(data: String?): T? {
if (data == null || data.isEmpty()) {
return null
}
try {
val json = JSONObject(data)
return if (json.getString(FIELD_TYPE) == type) {
getter(json, FIELD_VALUE)
} else {
null
}
} catch (e: JSONException) {
Log.w(TAG, "read error", e)
throw InvalidFlagStorageException()
}
}
}
object BooleanFlagSerializer : FlagSerializer<Boolean>(
TYPE_BOOLEAN,
JSONObject::put,
JSONObject::getBoolean
)
object StringFlagSerializer : FlagSerializer<String>(
TYPE_STRING,
JSONObject::put,
JSONObject::getString
)
class InvalidFlagStorageException : Exception("Data found but is invalid")

View File

@@ -23,6 +23,7 @@ import com.android.systemui.util.settings.SettingsUtilModule
import dagger.Binds
import dagger.Module
import dagger.Provides
import java.util.function.Supplier
@Module(includes = [
SettingsUtilModule::class
@@ -38,5 +39,9 @@ abstract class FlagsModule {
fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager {
return FlagManager(context, handler)
}
@JvmStatic
@Provides
fun providesFlagCollector(): Supplier<Map<Int, Flag<*>>>? = null
}
}

View File

@@ -27,4 +27,10 @@ interface FeatureFlags : FlagListenable {
/** Returns a boolean value for the given flag. */
fun isEnabled(flag: ResourceBooleanFlag): Boolean
/** Returns a string value for the given flag. */
fun getString(flag: StringFlag): String
/** Returns a string value for the given flag. */
fun getString(flag: ResourceStringFlag): String
}

View File

@@ -18,9 +18,11 @@ package com.android.systemui.flags;
import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS;
import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG;
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 static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
import static com.android.systemui.flags.FlagManager.EXTRA_ID;
import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;
import static java.util.Objects.requireNonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -39,14 +41,13 @@ import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.util.settings.SecureSettings;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.function.Supplier;
import javax.inject.Inject;
@@ -66,7 +67,9 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
private final FlagManager mFlagManager;
private final SecureSettings mSecureSettings;
private final Resources mResources;
private final Map<Integer, Boolean> mBooleanFlagCache = new HashMap<>();
private final Supplier<Map<Integer, Flag<?>>> mFlagsCollector;
private final Map<Integer, Boolean> mBooleanFlagCache = new TreeMap<>();
private final Map<Integer, String> mStringFlagCache = new TreeMap<>();
@Inject
public FeatureFlagsDebug(
@@ -74,10 +77,12 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
Context context,
SecureSettings secureSettings,
@Main Resources resources,
DumpManager dumpManager) {
DumpManager dumpManager,
@Nullable Supplier<Map<Integer, Flag<?>>> flagsCollector) {
mFlagManager = flagManager;
mSecureSettings = secureSettings;
mResources = resources;
mFlagsCollector = flagsCollector != null ? flagsCollector : Flags::collectFlags;
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_SET_FLAG);
filter.addAction(ACTION_GET_FLAGS);
@@ -88,58 +93,85 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
}
@Override
public boolean isEnabled(BooleanFlag flag) {
public boolean isEnabled(@NonNull BooleanFlag flag) {
int id = flag.getId();
if (!mBooleanFlagCache.containsKey(id)) {
mBooleanFlagCache.put(id, isEnabled(id, flag.getDefault()));
mBooleanFlagCache.put(id,
readFlagValue(id, flag.getDefault(), BooleanFlagSerializer.INSTANCE));
}
return mBooleanFlagCache.get(id);
}
@Override
public boolean isEnabled(ResourceBooleanFlag flag) {
public boolean isEnabled(@NonNull ResourceBooleanFlag flag) {
int id = flag.getId();
if (!mBooleanFlagCache.containsKey(id)) {
mBooleanFlagCache.put(
id, isEnabled(id, mResources.getBoolean(flag.getResourceId())));
mBooleanFlagCache.put(id,
readFlagValue(id, mResources.getBoolean(flag.getResourceId()),
BooleanFlagSerializer.INSTANCE));
}
return mBooleanFlagCache.get(id);
}
/** Return a flag's value. */
private boolean isEnabled(int id, boolean defaultValue) {
Boolean result = isEnabledInternal(id);
@NonNull
@Override
public String getString(@NonNull StringFlag flag) {
int id = flag.getId();
if (!mStringFlagCache.containsKey(id)) {
mStringFlagCache.put(id,
readFlagValue(id, flag.getDefault(), StringFlagSerializer.INSTANCE));
}
return mStringFlagCache.get(id);
}
@NonNull
@Override
public String getString(@NonNull ResourceStringFlag flag) {
int id = flag.getId();
if (!mStringFlagCache.containsKey(id)) {
mStringFlagCache.put(id,
readFlagValue(id, mResources.getString(flag.getResourceId()),
StringFlagSerializer.INSTANCE));
}
return mStringFlagCache.get(id);
}
@NonNull
private <T> T readFlagValue(int id, @NonNull T defaultValue, FlagSerializer<T> serializer) {
requireNonNull(defaultValue, "defaultValue");
T result = readFlagValueInternal(id, serializer);
return result == null ? defaultValue : result;
}
/** Returns the stored value or null if not set. */
private Boolean isEnabledInternal(int id) {
@Nullable
private <T> T readFlagValueInternal(int id, FlagSerializer<T> serializer) {
try {
return mFlagManager.isEnabled(id);
return mFlagManager.readFlagValue(id, serializer);
} catch (Exception e) {
eraseInternal(id);
}
return null;
}
/** Set whether a given {@link BooleanFlag} is enabled or not. */
public void setEnabled(int id, boolean value) {
Boolean currentValue = isEnabledInternal(id);
if (currentValue != null && currentValue == value) {
private <T> void setFlagValue(int id, @NonNull T value, FlagSerializer<T> serializer) {
requireNonNull(value, "Cannot set a null value");
T currentValue = readFlagValueInternal(id, serializer);
if (Objects.equals(currentValue, value)) {
Log.i(TAG, "Flag id " + id + " is already " + value);
return;
}
JSONObject json = new JSONObject();
try {
json.put(FlagManager.FIELD_TYPE, FlagManager.TYPE_BOOLEAN);
json.put(FIELD_VALUE, value);
mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), json.toString());
} catch (JSONException e) {
return; // ignore
final String data = serializer.toSettingsData(value);
if (data == null) {
Log.w(TAG, "Failed to set id " + id + " to " + value);
return;
}
mSecureSettings.putString(mFlagManager.idToSettingsKey(id), data);
Log.i(TAG, "Set id " + id + " to " + value);
removeFromCache(id);
mFlagManager.dispatchListenersAndMaybeRestart(id);
@@ -155,7 +187,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
/** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
private void eraseInternal(int id) {
// We can't actually "erase" things from sysprops, but we can set them to empty!
mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), "");
mSecureSettings.putString(mFlagManager.idToSettingsKey(id), "");
Log.i(TAG, "Erase id " + id);
}
@@ -182,14 +214,14 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
String action = intent == null ? null : intent.getAction();
if (action == null) {
return;
}
if (ACTION_SET_FLAG.equals(action)) {
handleSetFlag(intent.getExtras());
} else if (ACTION_GET_FLAGS.equals(action)) {
Map<Integer, Flag<?>> knownFlagMap = Flags.collectFlags();
Map<Integer, Flag<?>> knownFlagMap = mFlagsCollector.get();
ArrayList<Flag<?>> flags = new ArrayList<>(knownFlagMap.values());
// Convert all flags to parcelable flags.
@@ -203,7 +235,7 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
Bundle extras = getResultExtras(true);
if (extras != null) {
extras.putParcelableArrayList(FIELD_FLAGS, pFlags);
extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
}
}
}
@@ -213,26 +245,37 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
Log.w(TAG, "No extras");
return;
}
int id = extras.getInt(FIELD_ID);
int id = extras.getInt(EXTRA_ID);
if (id <= 0) {
Log.w(TAG, "ID not set or less than or equal to 0: " + id);
return;
}
Map<Integer, Flag<?>> flagMap = Flags.collectFlags();
Map<Integer, Flag<?>> flagMap = mFlagsCollector.get();
if (!flagMap.containsKey(id)) {
Log.w(TAG, "Tried to set unknown id: " + id);
return;
}
Flag<?> flag = flagMap.get(id);
if (!extras.containsKey(FIELD_VALUE)) {
if (!extras.containsKey(EXTRA_VALUE)) {
eraseFlag(id);
return;
}
if (flag instanceof BooleanFlag) {
setEnabled(id, extras.getBoolean(FIELD_VALUE));
Object value = extras.get(EXTRA_VALUE);
if (flag instanceof BooleanFlag && value instanceof Boolean) {
setFlagValue(id, (Boolean) value, BooleanFlagSerializer.INSTANCE);
} else if (flag instanceof ResourceBooleanFlag && value instanceof Boolean) {
setFlagValue(id, (Boolean) value, BooleanFlagSerializer.INSTANCE);
} else if (flag instanceof StringFlag && value instanceof String) {
setFlagValue(id, (String) value, StringFlagSerializer.INSTANCE);
} else if (flag instanceof ResourceStringFlag && value instanceof String) {
setFlagValue(id, (String) value, StringFlagSerializer.INSTANCE);
} else {
Log.w(TAG,
"Unable to set " + id + " of type " + flag.getClass() + " to value of type "
+ (value == null ? null : value.getClass()));
}
}
@@ -258,18 +301,16 @@ public class FeatureFlagsDebug implements FeatureFlags, Dumpable {
private void removeFromCache(int id) {
mBooleanFlagCache.remove(id);
mStringFlagCache.remove(id);
}
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("can override: true");
ArrayList<String> flagStrings = new ArrayList<>(mBooleanFlagCache.size());
for (Map.Entry<Integer, Boolean> entry : mBooleanFlagCache.entrySet()) {
flagStrings.add(" sysui_flag_" + entry.getKey() + ": " + entry.getValue());
}
flagStrings.sort(String.CASE_INSENSITIVE_ORDER);
for (String flagString : flagStrings) {
pw.println(flagString);
}
pw.println("booleans: " + mBooleanFlagCache.size());
mBooleanFlagCache.forEach((key, value) -> pw.println(" sysui_flag_" + key + ": " + value));
pw.println("Strings: " + mStringFlagCache.size());
mStringFlagCache.forEach((key, value) -> pw.println(" sysui_flag_" + key
+ ": [length=" + value.length() + "] \"" + value + "\""));
}
}

View File

@@ -16,7 +16,10 @@
package com.android.systemui.flags;
import static java.util.Objects.requireNonNull;
import android.content.res.Resources;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import androidx.annotation.NonNull;
@@ -40,7 +43,8 @@ import javax.inject.Inject;
@SysUISingleton
public class FeatureFlagsRelease implements FeatureFlags, Dumpable {
private final Resources mResources;
SparseBooleanArray mFlagCache = new SparseBooleanArray();
SparseBooleanArray mBooleanCache = new SparseBooleanArray();
SparseArray<String> mStringCache = new SparseArray<>();
@Inject
public FeatureFlagsRelease(@Main Resources resources, DumpManager dumpManager) {
mResources = resources;
@@ -60,25 +64,57 @@ public class FeatureFlagsRelease implements FeatureFlags, Dumpable {
@Override
public boolean isEnabled(ResourceBooleanFlag flag) {
int cacheIndex = mFlagCache.indexOfKey(flag.getId());
int cacheIndex = mBooleanCache.indexOfKey(flag.getId());
if (cacheIndex < 0) {
return isEnabled(flag.getId(), mResources.getBoolean(flag.getResourceId()));
}
return mFlagCache.valueAt(cacheIndex);
return mBooleanCache.valueAt(cacheIndex);
}
private boolean isEnabled(int key, boolean defaultValue) {
mFlagCache.append(key, defaultValue);
mBooleanCache.append(key, defaultValue);
return defaultValue;
}
@NonNull
@Override
public String getString(@NonNull StringFlag flag) {
return getString(flag.getId(), flag.getDefault());
}
@NonNull
@Override
public String getString(@NonNull ResourceStringFlag flag) {
int cacheIndex = mStringCache.indexOfKey(flag.getId());
if (cacheIndex < 0) {
return getString(flag.getId(),
requireNonNull(mResources.getString(flag.getResourceId())));
}
return mStringCache.valueAt(cacheIndex);
}
private String getString(int key, String defaultValue) {
mStringCache.append(key, defaultValue);
return defaultValue;
}
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
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));
int numBooleans = mBooleanCache.size();
pw.println("booleans: " + numBooleans);
for (int i = 0; i < numBooleans; i++) {
pw.println(" sysui_flag_" + mBooleanCache.keyAt(i) + ": " + mBooleanCache.valueAt(i));
}
int numStrings = mStringCache.size();
pw.println("Strings: " + numStrings);
for (int i = 0; i < numStrings; i++) {
final int id = mStringCache.keyAt(i);
final String value = mStringCache.valueAt(i);
final int length = value.length();
pw.println(" sysui_flag_" + id + ": [length=" + length + "] \"" + value + "\"");
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* 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.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.content.res.Resources
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
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.nullable
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import java.io.PrintWriter
import java.io.Serializable
import java.io.StringWriter
import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
/**
* 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 FeatureFlagsDebugTest : SysuiTestCase() {
private lateinit var mFeatureFlagsDebug: FeatureFlagsDebug
@Mock private lateinit var mFlagManager: FlagManager
@Mock private lateinit var mMockContext: Context
@Mock private lateinit var mSecureSettings: SecureSettings
@Mock private lateinit var mResources: Resources
@Mock private lateinit var mDumpManager: DumpManager
private val mFlagMap = mutableMapOf<Int, Flag<*>>()
private lateinit var mBroadcastReceiver: BroadcastReceiver
private lateinit var mClearCacheAction: Consumer<Int>
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mFeatureFlagsDebug = FeatureFlagsDebug(
mFlagManager,
mMockContext,
mSecureSettings,
mResources,
mDumpManager,
{ mFlagMap }
)
verify(mFlagManager).restartAction = any()
mBroadcastReceiver = withArgCaptor {
verify(mMockContext).registerReceiver(capture(), any(), nullable(), nullable())
}
mClearCacheAction = withArgCaptor {
verify(mFlagManager).clearCacheAction = capture()
}
whenever(mFlagManager.idToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
}
@Test
fun testReadBooleanFlag() {
whenever(mFlagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
whenever(mFlagManager.readFlagValue<Boolean>(eq(4), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(1, false))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(2, true))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(3, false))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(BooleanFlag(4, true))).isFalse()
}
@Test
fun testReadResourceBooleanFlag() {
whenever(mResources.getBoolean(1001)).thenReturn(false)
whenever(mResources.getBoolean(1002)).thenReturn(true)
whenever(mResources.getBoolean(1003)).thenReturn(false)
whenever(mResources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() }
whenever(mResources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() }
whenever(mFlagManager.readFlagValue<Boolean>(eq(3), any())).thenReturn(true)
whenever(mFlagManager.readFlagValue<Boolean>(eq(5), any())).thenReturn(false)
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(1, 1001))).isFalse()
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(2, 1002))).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(3, 1003))).isTrue()
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(4, 1004))
}
// Test that resource is loaded (and validated) even when the setting is set.
// This prevents developers from not noticing when they reference an invalid resource.
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.isEnabled(ResourceBooleanFlag(5, 1005))
}
}
@Test
fun testReadStringFlag() {
whenever(mFlagManager.readFlagValue<String>(eq(3), any())).thenReturn("foo")
whenever(mFlagManager.readFlagValue<String>(eq(4), any())).thenReturn("bar")
assertThat(mFeatureFlagsDebug.getString(StringFlag(1, "biz"))).isEqualTo("biz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(2, "baz"))).isEqualTo("baz")
assertThat(mFeatureFlagsDebug.getString(StringFlag(3, "buz"))).isEqualTo("foo")
assertThat(mFeatureFlagsDebug.getString(StringFlag(4, "buz"))).isEqualTo("bar")
}
@Test
fun testReadResourceStringFlag() {
whenever(mResources.getString(1001)).thenReturn("")
whenever(mResources.getString(1002)).thenReturn("resource2")
whenever(mResources.getString(1003)).thenReturn("resource3")
whenever(mResources.getString(1004)).thenReturn(null)
whenever(mResources.getString(1005)).thenAnswer { throw NameNotFoundException() }
whenever(mResources.getString(1006)).thenAnswer { throw NameNotFoundException() }
whenever(mFlagManager.readFlagValue<String>(eq(3), any())).thenReturn("override3")
whenever(mFlagManager.readFlagValue<String>(eq(4), any())).thenReturn("override4")
whenever(mFlagManager.readFlagValue<String>(eq(6), any())).thenReturn("override6")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(2, 1002))).isEqualTo("resource2")
assertThat(mFeatureFlagsDebug.getString(ResourceStringFlag(3, 1003))).isEqualTo("override3")
Assert.assertThrows(NullPointerException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(4, 1004))
}
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(5, 1005))
}
// Test that resource is loaded (and validated) even when the setting is set.
// This prevents developers from not noticing when they reference an invalid resource.
Assert.assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsDebug.getString(ResourceStringFlag(6, 1005))
}
}
@Test
fun testBroadcastReceiverIgnoresInvalidData() {
addFlag(BooleanFlag(1, false))
addFlag(ResourceBooleanFlag(2, 1002))
addFlag(StringFlag(3, "flag3"))
addFlag(ResourceStringFlag(4, 1004))
mBroadcastReceiver.onReceive(mMockContext, null)
mBroadcastReceiver.onReceive(mMockContext, Intent())
mBroadcastReceiver.onReceive(mMockContext, Intent("invalid action"))
mBroadcastReceiver.onReceive(mMockContext, Intent(FlagManager.ACTION_SET_FLAG))
setByBroadcast(0, false) // unknown id does nothing
setByBroadcast(1, "string") // wrong type does nothing
setByBroadcast(2, 123) // wrong type does nothing
setByBroadcast(3, false) // wrong type does nothing
setByBroadcast(4, 123) // wrong type does nothing
verifyNoMoreInteractions(mFlagManager, mSecureSettings)
}
@Test
fun testIntentWithIdButNoValueKeyClears() {
addFlag(BooleanFlag(1, false))
// trying to erase an id not in the map does noting
mBroadcastReceiver.onReceive(
mMockContext,
Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 0)
)
verifyNoMoreInteractions(mFlagManager, mSecureSettings)
// valid id with no value puts empty string in the setting
mBroadcastReceiver.onReceive(
mMockContext,
Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_ID, 1)
)
verifyPutData(1, "", numReads = 0)
}
@Test
fun testSetBooleanFlag() {
addFlag(BooleanFlag(1, false))
addFlag(BooleanFlag(2, false))
addFlag(ResourceBooleanFlag(3, 1003))
addFlag(ResourceBooleanFlag(4, 1004))
setByBroadcast(1, false)
verifyPutData(1, "{\"type\":\"boolean\",\"value\":false}")
setByBroadcast(2, true)
verifyPutData(2, "{\"type\":\"boolean\",\"value\":true}")
setByBroadcast(3, false)
verifyPutData(3, "{\"type\":\"boolean\",\"value\":false}")
setByBroadcast(4, true)
verifyPutData(4, "{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun testSetStringFlag() {
addFlag(StringFlag(1, "flag1"))
addFlag(ResourceStringFlag(2, 1002))
setByBroadcast(1, "override1")
verifyPutData(1, "{\"type\":\"string\",\"value\":\"override1\"}")
setByBroadcast(2, "override2")
verifyPutData(2, "{\"type\":\"string\",\"value\":\"override2\"}")
}
@Test
fun testSetFlagClearsCache() {
val flag1 = addFlag(StringFlag(1, "flag1"))
whenever(mFlagManager.readFlagValue<String>(eq(1), any())).thenReturn("original")
// gets the flag & cache it
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
verify(mFlagManager).readFlagValue(eq(1), eq(StringFlagSerializer))
// hit the cache
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("original")
verifyNoMoreInteractions(mFlagManager)
// set the flag
setByBroadcast(1, "new")
verifyPutData(1, "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
whenever(mFlagManager.readFlagValue<String>(eq(1), any())).thenReturn("new")
assertThat(mFeatureFlagsDebug.getString(flag1)).isEqualTo("new")
verify(mFlagManager, times(3)).readFlagValue(eq(1), eq(StringFlagSerializer))
}
private fun verifyPutData(id: Int, data: String, numReads: Int = 1) {
inOrder(mFlagManager, mSecureSettings).apply {
verify(mFlagManager, times(numReads)).readFlagValue(eq(id), any<FlagSerializer<*>>())
verify(mFlagManager).idToSettingsKey(eq(id))
verify(mSecureSettings).putString(eq("key-$id"), eq(data))
verify(mFlagManager).dispatchListenersAndMaybeRestart(eq(id))
}.verifyNoMoreInteractions()
verifyNoMoreInteractions(mFlagManager, mSecureSettings)
}
private fun setByBroadcast(id: Int, value: Serializable?) {
val intent = Intent(FlagManager.ACTION_SET_FLAG)
intent.putExtra(FlagManager.EXTRA_ID, id)
intent.putExtra(FlagManager.EXTRA_VALUE, value)
mBroadcastReceiver.onReceive(mMockContext, intent)
}
private fun <F : Flag<*>> addFlag(flag: F): F {
val old = mFlagMap.put(flag.id, flag)
check(old == null) { "Flag ${flag.id} already registered" }
return flag
}
@Test
fun testDump() {
val flag1 = BooleanFlag(1, true)
val flag2 = ResourceBooleanFlag(2, 1002)
val flag3 = BooleanFlag(3, false)
val flag4 = StringFlag(4, "")
val flag5 = StringFlag(5, "flag5default")
val flag6 = ResourceStringFlag(6, 1006)
val flag7 = ResourceStringFlag(7, 1007)
whenever(mResources.getBoolean(1002)).thenReturn(true)
whenever(mResources.getString(1006)).thenReturn("resource1006")
whenever(mResources.getString(1007)).thenReturn("resource1007")
whenever(mFlagManager.readFlagValue(eq(7), eq(StringFlagSerializer)))
.thenReturn("override7")
// WHEN the flags have been accessed
assertThat(mFeatureFlagsDebug.isEnabled(flag1)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(flag2)).isTrue()
assertThat(mFeatureFlagsDebug.isEnabled(flag3)).isFalse()
assertThat(mFeatureFlagsDebug.getString(flag4)).isEmpty()
assertThat(mFeatureFlagsDebug.getString(flag5)).isEqualTo("flag5default")
assertThat(mFeatureFlagsDebug.getString(flag6)).isEqualTo("resource1006")
assertThat(mFeatureFlagsDebug.getString(flag7)).isEqualTo("override7")
// THEN the dump contains the flags and the default values
val dump = dumpToString()
assertThat(dump).contains(" sysui_flag_1: true\n")
assertThat(dump).contains(" sysui_flag_2: true\n")
assertThat(dump).contains(" sysui_flag_3: false\n")
assertThat(dump).contains(" sysui_flag_4: [length=0] \"\"\n")
assertThat(dump).contains(" sysui_flag_5: [length=12] \"flag5default\"\n")
assertThat(dump).contains(" sysui_flag_6: [length=12] \"resource1006\"\n")
assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n")
}
private fun dumpToString(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
mFeatureFlagsDebug.dump(mock(), pw, emptyArray<String>())
pw.flush()
return sw.toString()
}
}

View File

@@ -1,111 +0,0 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.content.res.Resources;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 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
public class FeatureFlagsReleaseTest extends SysuiTestCase {
FeatureFlagsRelease mFeatureFlagsRelease;
@Mock private Resources mResources;
@Mock private DumpManager mDumpManager;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFeatureFlagsRelease = new FeatureFlagsRelease(mResources, mDumpManager);
}
@After
public void onFinished() {
// The dump manager should be registered with even for the release version, but that's it.
verify(mDumpManager).registerDumpable(anyString(), any());
verifyNoMoreInteractions(mDumpManager);
}
@Test
public void testBooleanResourceFlag() {
int flagId = 213;
int flagResourceId = 3;
ResourceBooleanFlag flag = new ResourceBooleanFlag(flagId, flagResourceId);
when(mResources.getBoolean(flagResourceId)).thenReturn(true);
assertThat(mFeatureFlagsRelease.isEnabled(flag)).isTrue();
}
@Test
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(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_" + flagIdA + ": true\n");
assertThat(dump).contains(" sysui_flag_" + flagIdB + ": true\n");
assertThat(dump).contains(" sysui_flag_" + flagIdC + ": false\n");
}
private String dumpToString() {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
mFeatureFlagsRelease.dump(mock(FileDescriptor.class), pw, new String[0]);
pw.flush();
String dump = sw.toString();
return dump;
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.pm.PackageManager.NameNotFoundException
import android.content.res.Resources
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import java.io.PrintWriter
import java.io.StringWriter
import org.mockito.Mockito.`when` as whenever
/**
* 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 FeatureFlagsReleaseTest : SysuiTestCase() {
private lateinit var mFeatureFlagsRelease: FeatureFlagsRelease
@Mock private lateinit var mResources: Resources
@Mock private lateinit var mDumpManager: DumpManager
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
mFeatureFlagsRelease = FeatureFlagsRelease(mResources, mDumpManager)
}
@After
fun onFinished() {
// The dump manager should be registered with even for the release version, but that's it.
verify(mDumpManager).registerDumpable(any(), any())
verifyNoMoreInteractions(mDumpManager)
}
@Test
fun testBooleanResourceFlag() {
val flagId = 213
val flagResourceId = 3
val flag = ResourceBooleanFlag(flagId, flagResourceId)
whenever(mResources.getBoolean(flagResourceId)).thenReturn(true)
assertThat(mFeatureFlagsRelease.isEnabled(flag)).isTrue()
}
@Test
fun testReadResourceStringFlag() {
whenever(mResources.getString(1001)).thenReturn("")
whenever(mResources.getString(1002)).thenReturn("res2")
whenever(mResources.getString(1003)).thenReturn(null)
whenever(mResources.getString(1004)).thenAnswer { throw NameNotFoundException() }
assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(1, 1001))).isEqualTo("")
assertThat(mFeatureFlagsRelease.getString(ResourceStringFlag(2, 1002))).isEqualTo("res2")
assertThrows(NullPointerException::class.java) {
mFeatureFlagsRelease.getString(ResourceStringFlag(3, 1003))
}
assertThrows(NameNotFoundException::class.java) {
mFeatureFlagsRelease.getString(ResourceStringFlag(4, 1004))
}
}
@Test
fun testDump() {
val flag1 = BooleanFlag(1, true)
val flag2 = ResourceBooleanFlag(2, 1002)
val flag3 = BooleanFlag(3, false)
val flag4 = StringFlag(4, "")
val flag5 = StringFlag(5, "flag5default")
val flag6 = ResourceStringFlag(6, 1006)
whenever(mResources.getBoolean(1002)).thenReturn(true)
whenever(mResources.getString(1006)).thenReturn("resource1006")
whenever(mResources.getString(1007)).thenReturn("resource1007")
// WHEN the flags have been accessed
assertThat(mFeatureFlagsRelease.isEnabled(flag1)).isTrue()
assertThat(mFeatureFlagsRelease.isEnabled(flag2)).isTrue()
assertThat(mFeatureFlagsRelease.isEnabled(flag3)).isFalse()
assertThat(mFeatureFlagsRelease.getString(flag4)).isEmpty()
assertThat(mFeatureFlagsRelease.getString(flag5)).isEqualTo("flag5default")
assertThat(mFeatureFlagsRelease.getString(flag6)).isEqualTo("resource1006")
// THEN the dump contains the flags and the default values
val dump = dumpToString()
assertThat(dump).contains(" sysui_flag_1: true\n")
assertThat(dump).contains(" sysui_flag_2: true\n")
assertThat(dump).contains(" sysui_flag_3: false\n")
assertThat(dump).contains(" sysui_flag_4: [length=0] \"\"\n")
assertThat(dump).contains(" sysui_flag_5: [length=12] \"flag5default\"\n")
assertThat(dump).contains(" sysui_flag_6: [length=12] \"resource1006\"\n")
}
private fun dumpToString(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
mFeatureFlagsRelease.dump(mock(), pw, emptyArray())
pw.flush()
return sw.toString()
}
}

View File

@@ -26,6 +26,7 @@ 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.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -34,6 +35,7 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
/**
* NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow
@@ -211,4 +213,90 @@ class FlagManagerTest : SysuiTestCase() {
verify(restartAction).accept(eq(false))
verifyNoMoreInteractions(restartAction)
}
@Test
fun testReadBooleanFlag() {
// test that null string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
// test that empty string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
// test false
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":false}")
assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isFalse()
// test true
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":true}")
assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isTrue()
// Reading a value of a different type should just return null
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
assertThat(mFlagManager.readFlagValue(1, BooleanFlagSerializer)).isNull()
// Reading a value that isn't json should throw an exception
assertThrows(InvalidFlagStorageException::class.java) {
whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
mFlagManager.readFlagValue(1, BooleanFlagSerializer)
}
}
@Test
fun testSerializeBooleanFlag() {
// test false
assertThat(BooleanFlagSerializer.toSettingsData(false))
.isEqualTo("{\"type\":\"boolean\",\"value\":false}")
// test true
assertThat(BooleanFlagSerializer.toSettingsData(true))
.isEqualTo("{\"type\":\"boolean\",\"value\":true}")
}
@Test
fun testReadStringFlag() {
// test that null string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn(null)
assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
// test that empty string returns null
whenever(mFlagSettingsHelper.getString(any())).thenReturn("")
assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
// test json with the empty string value returns empty string
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"\"}")
assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("")
// test string with value is returned
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"string\",\"value\":\"foo\"}")
assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isEqualTo("foo")
// Reading a value of a different type should just return null
whenever(mFlagSettingsHelper.getString(any()))
.thenReturn("{\"type\":\"boolean\",\"value\":false}")
assertThat(mFlagManager.readFlagValue(1, StringFlagSerializer)).isNull()
// Reading a value that isn't json should throw an exception
assertThrows(InvalidFlagStorageException::class.java) {
whenever(mFlagSettingsHelper.getString(any())).thenReturn("1")
mFlagManager.readFlagValue(1, StringFlagSerializer)
}
}
@Test
fun testSerializeStringFlag() {
// test empty string
assertThat(StringFlagSerializer.toSettingsData(""))
.isEqualTo("{\"type\":\"string\",\"value\":\"\"}")
// test string "foo"
assertThat(StringFlagSerializer.toSettingsData("foo"))
.isEqualTo("{\"type\":\"string\",\"value\":\"foo\"}")
}
}

View File

@@ -43,6 +43,11 @@ fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
inline fun <reified T> any(): T = any(T::class.java)
/**
* Kotlin type-inferred version of Mockito.nullable()
*/
inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
/**
* Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
* when null is returned.