Generalize some flag manager methods by flag type.
Bug: 209081785 Test: atest FeatureFlagsDebugTest FlagManagerTest Change-Id: Ie92667e33e7c086258cdd03e7b9ebccdefef5938
This commit is contained in:
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 + "\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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\"}")
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user