Generalize some flag manager methods by flag type.

Bug: 209081785
Test: atest FeatureFlagsDebugTest FlagManagerTest
Change-Id: Ie92667e33e7c086258cdd03e7b9ebccdefef5938
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
index 42fec77..ec619dd 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagManager.kt
@@ -26,8 +26,6 @@
 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 @@
         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 @@
                         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 @@
         } 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 @@
     }
 
     /** 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 @@
     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 @@
     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")
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt
new file mode 100644
index 0000000..e9ea19d
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/flags/FlagSerializer.kt
@@ -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")
diff --git a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
index 10ceee9..adfc872 100644
--- a/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
+++ b/packages/SystemUI/src-debug/com/android/systemui/flags/FlagsModule.kt
@@ -23,6 +23,7 @@
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
+import java.util.function.Supplier
 
 @Module(includes = [
     SettingsUtilModule::class
@@ -38,5 +39,9 @@
         fun provideFlagManager(context: Context, @Main handler: Handler): FlagManager {
             return FlagManager(context, handler)
         }
+
+        @JvmStatic
+        @Provides
+        fun providesFlagCollector(): Supplier<Map<Int, Flag<*>>>? = null
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
index d4b23c7..96a90df 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlags.kt
@@ -27,4 +27,10 @@
 
     /** 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
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
index b5916f1..89623f4 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsDebug.java
@@ -18,9 +18,11 @@
 
 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.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 @@
     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 @@
             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 @@
     }
 
     @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 @@
     /** 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 @@
     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 @@
 
                 Bundle extras =  getResultExtras(true);
                 if (extras != null) {
-                    extras.putParcelableArrayList(FIELD_FLAGS, pFlags);
+                    extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
                 }
             }
         }
@@ -213,26 +245,37 @@
                 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 @@
 
     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 + "\""));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
index d82b89b..348a8e2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/FeatureFlagsRelease.java
@@ -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 @@
 @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 @@
 
     @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 + "\"");
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
new file mode 100644
index 0000000..cb16bec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsDebugTest.kt
@@ -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()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.java
deleted file mode 100644
index b0fdcf4..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.java
+++ /dev/null
@@ -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;
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
new file mode 100644
index 0000000..b5e6602
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsReleaseTest.kt
@@ -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()
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
index aba656f..644bd21 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagManagerTest.kt
@@ -26,6 +26,7 @@
 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.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 @@
         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\"}")
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index eb54fe0..0f1b65c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -44,6 +44,11 @@
 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.
  *