Add permissions to FeatureFlagService and override api.

This will allow flag flipper to change flags and prevent others
from doing so.

Bug: 279054964
Test: atest FrameworksServicesTests:com.android.server.flag
Change-Id: I6f2e2acecc202d3e24f94ef0801b98c2fd0aa456
diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java
index 8583ea6..68819c5 100644
--- a/core/java/android/flags/DynamicFlag.java
+++ b/core/java/android/flags/DynamicFlag.java
@@ -24,4 +24,8 @@
  * @hide
  */
 public interface DynamicFlag<T> extends Flag<T> {
+    @Override
+    default boolean isDynamic() {
+        return true;
+    }
 }
diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java
index 93e56b1..8d3112c 100644
--- a/core/java/android/flags/FeatureFlags.java
+++ b/core/java/android/flags/FeatureFlags.java
@@ -250,7 +250,13 @@
         return flag;
     }
 
-    protected void sync() {
+    /**
+     * Sync any known flags that have not yet been synced.
+     *
+     * This is called implicitly when any flag is read, and is not generally needed except in
+     * exceptional circumstances.
+     */
+    public void sync() {
         synchronized (FeatureFlags.class) {
             if (mDirtyFlags.isEmpty()) {
                 return;
diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java
index ae0a6ba..0ab3c8a 100644
--- a/core/java/android/flags/Flag.java
+++ b/core/java/android/flags/Flag.java
@@ -37,4 +37,9 @@
     /** The value of this flag if no override has been set. Null values are not supported. */
     @NonNull
     T getDefault();
+
+    /** Returns true if the value of this flag can change at runtime. */
+    default boolean isDynamic() {
+        return false;
+    }
 }
diff --git a/core/java/android/flags/IFeatureFlags.aidl b/core/java/android/flags/IFeatureFlags.aidl
index 1eef47f..3efcec9 100644
--- a/core/java/android/flags/IFeatureFlags.aidl
+++ b/core/java/android/flags/IFeatureFlags.aidl
@@ -62,4 +62,28 @@
      * {@link #registerCallback}.
      */
     void unregisterCallback(IFeatureFlagsCallback callback);
+
+    /**
+     * Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't
+     * cache them. See {@link #syncFlags}.
+     *
+     * You almost certainly don't want this method. This is intended for the Flag Flipper
+     * application that needs to query the state of system but doesn't want to affect it by
+     * doing so. All other clients should use {@link syncFlags}.
+     */
+    List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList);
+
+    /**
+     * Change a flags value in the system.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void overrideFlag(in SyncableFlag flag);
+
+    /**
+     * Restore a flag to its default value.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void resetFlag(in SyncableFlag flag);
 }
\ No newline at end of file
diff --git a/core/java/android/flags/SyncableFlag.java b/core/java/android/flags/SyncableFlag.java
index 0b2a4d9..449bcc3c 100644
--- a/core/java/android/flags/SyncableFlag.java
+++ b/core/java/android/flags/SyncableFlag.java
@@ -28,16 +28,28 @@
     private final String mName;
     private final String mValue;
     private final boolean mDynamic;
+    private final boolean mOverridden;
 
     public SyncableFlag(
             @NonNull String namespace,
             @NonNull String name,
             @NonNull String value,
             boolean dynamic) {
+        this(namespace, name, value, dynamic, false);
+    }
+
+    public SyncableFlag(
+            @NonNull String namespace,
+            @NonNull String name,
+            @NonNull String value,
+            boolean dynamic,
+            boolean overridden
+    ) {
         mNamespace = namespace;
         mName = name;
         mValue = value;
         mDynamic = dynamic;
+        mOverridden = overridden;
     }
 
     @NonNull
@@ -55,16 +67,23 @@
         return mValue;
     }
 
-    @NonNull
     public boolean isDynamic() {
         return mDynamic;
     }
 
+    public boolean isOverridden() {
+        return mOverridden;
+    }
+
     @NonNull
     public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() {
         public SyncableFlag createFromParcel(Parcel in) {
             return new SyncableFlag(
-                    in.readString(), in.readString(), in.readString(), in.readBoolean());
+                    in.readString(),
+                    in.readString(),
+                    in.readString(),
+                    in.readBoolean(),
+                    in.readBoolean());
         }
 
         public SyncableFlag[] newArray(int size) {
@@ -83,6 +102,7 @@
         dest.writeString(mName);
         dest.writeString(mValue);
         dest.writeBoolean(mDynamic);
+        dest.writeBoolean(mOverridden);
     }
 
     @Override
diff --git a/core/java/com/android/internal/flags/CoreFlags.java b/core/java/com/android/internal/flags/CoreFlags.java
new file mode 100644
index 0000000..f177ef8
--- /dev/null
+++ b/core/java/com/android/internal/flags/CoreFlags.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.internal.flags;
+
+import android.flags.BooleanFlag;
+import android.flags.DynamicBooleanFlag;
+import android.flags.FeatureFlags;
+import android.flags.FusedOffFlag;
+import android.flags.FusedOnFlag;
+import android.flags.SyncableFlag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Flags defined here are can be read by code in core.
+ *
+ * Flags not defined here will throw a security exception if third-party processes attempts to read
+ * them.
+ *
+ * DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that
+ * runs inside a third party process.
+ */
+public abstract class CoreFlags {
+    private static final List<SyncableFlag> sKnownFlags = new ArrayList<>();
+
+    public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false);
+    public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag");
+    public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag");
+    public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true);
+
+    /** Returns true if the passed in flag matches a flag in this class. */
+    public static boolean isCoreFlag(SyncableFlag flag) {
+        for (SyncableFlag knownFlag : sKnownFlags) {
+            if (knownFlag.getName().equals(flag.getName())
+                    && knownFlag.getNamespace().equals(flag.getNamespace())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<SyncableFlag> getCoreFlags() {
+        return sKnownFlags;
+    }
+
+    private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) {
+        BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false));
+
+        return f;
+    }
+
+    private static FusedOffFlag fusedOffFlag(String namespace, String name) {
+        FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "false", false));
+
+        return f;
+    }
+
+    private static FusedOnFlag fusedOnFlag(String namespace, String name) {
+        FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "true", false));
+
+        return f;
+    }
+
+    private static DynamicBooleanFlag dynamicBooleanFlag(
+            String namespace, String name, boolean defaultValue) {
+        DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true));
+
+        return f;
+    }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a01c7b6..10cf353 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7655,6 +7655,24 @@
     <permission android:name="android.permission.GET_ANY_PROVIDER_TYPE"
                 android:protectionLevel="signature" />
 
+
+    <!-- @hide Allows internal applications to read and synchronize non-core flags.
+         Apps without this permission can only read a subset of flags specifically intended
+         for use in "core", (i.e. third party apps). Apps with this permission can define their
+         own flags, and federate those values with other system-level apps.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.SYNC_FLAGS"
+        android:protectionLevel="signature" />
+
+    <!-- @hide Allows internal applications to override flags in the FeatureFlags service.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.WRITE_FLAGS"
+        android:protectionLevel="signature" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
index 70b65ce..bc5d8aa 100644
--- a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
+++ b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
@@ -39,6 +39,56 @@
     }
 
     @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) {
+        return mOverrides == null ? flagList : mOverrides;    }
+
+    @Override
+    public void overrideFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        mOverrides.add(syncableFlag);
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    private SyncableFlag findFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = null;
+        for (SyncableFlag sf : mOverrides) {
+            if (sf.getName().equals(syncableFlag.getName())
+                    && sf.getNamespace().equals(syncableFlag.getNamespace())) {
+                match = sf;
+                break;
+            }
+        }
+
+        return match;
+    }
+    @Override
     public void registerCallback(IFeatureFlagsCallback callback) {
         mCallbacks.add(callback);
     }
diff --git a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
index 8da5aae..0db3287 100644
--- a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
+++ b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
@@ -132,25 +132,8 @@
         // about changes coming in from adb, DeviceConfig, or other sources.
         // And also so that we can keep flags relatively consistent across processes.
 
-        // If we already have a value cached, just use that.
-        String value = null;
         DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
-        if (data != null) {
-            value = data.getValue();
-        } else {
-            // Put the value in the cache for future reference.
-            data = new DynamicFlagData(ns, name);
-            mDynamicFlags.setIfChanged(ns, name, data);
-        }
-        // If we're not in a release build, flags can be overridden locally on device.
-        if (!Build.IS_USER && value == null) {
-            value = mFlagStore.get(ns, name);
-        }
-        // If we still don't have a value, maybe DeviceConfig does?
-        // Fallback to sf.getValue() here as well.
-        if (value == null) {
-            value = DeviceConfig.getString(ns, name, sf.getValue());
-        }
+        String value = getFlagValue(ns, name, sf.getValue());
         // DeviceConfig listeners are per-namespace.
         if (!mDynamicFlags.containsNamespace(ns)) {
             DeviceConfig.addOnPropertiesChangedListener(
@@ -200,6 +183,30 @@
         }
     }
 
+    String getFlagValue(String namespace, String name, String defaultValue) {
+        // If we already have a value cached, just use that.
+        String value = null;
+        DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+        if (data != null) {
+            value = data.getValue();
+        } else {
+            // Put the value in the cache for future reference.
+            data = new DynamicFlagData(namespace, name);
+            mDynamicFlags.setIfChanged(namespace, name, data);
+        }
+        // If we're not in a release build, flags can be overridden locally on device.
+        if (!Build.IS_USER && value == null) {
+            value = mFlagStore.get(namespace, name);
+        }
+        // If we still don't have a value, maybe DeviceConfig does?
+        // Fallback to sf.getValue() here as well.
+        if (value == null) {
+            value = DeviceConfig.getString(namespace, name, defaultValue);
+        }
+
+        return value;
+    }
+
     private static class DynamicFlagData {
         private final String mNamespace;
         private final String mName;
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
index 86b464d..1fa8532 100644
--- a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
@@ -24,6 +24,9 @@
 import android.os.Build;
 import android.os.ParcelFileDescriptor;
 
+import com.android.internal.flags.CoreFlags;
+import com.android.server.flags.FeatureFlagsService.PermissionsChecker;
+
 import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -33,11 +36,16 @@
     private final FlagsShellCommand mShellCommand;
     private final FlagCache<String> mFlagCache = new FlagCache<>();
     private final DynamicFlagBinderDelegate mDynamicFlagDelegate;
+    private final PermissionsChecker mPermissionsChecker;
 
-    FeatureFlagsBinder(FlagOverrideStore flagStore, FlagsShellCommand shellCommand) {
+    FeatureFlagsBinder(
+            FlagOverrideStore flagStore,
+            FlagsShellCommand shellCommand,
+            PermissionsChecker permissionsChecker) {
         mFlagStore = flagStore;
         mShellCommand = shellCommand;
         mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
+        mPermissionsChecker = permissionsChecker;
     }
 
     @Override
@@ -50,11 +58,29 @@
         mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
     }
 
+    // Note: The internals of this method should be kept in sync with queryFlags
+    // as they both should return identical results. The difference is that this method
+    // caches any values it receives and/or reads, whereas queryFlags does not.
+
     @Override
     public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
         int pid = getCallingPid();
         List<SyncableFlag> outputFlags = new ArrayList<>();
+
+        boolean hasFullSyncPrivileges = false;
+        SecurityException permissionFailureException = null;
+        try {
+            assertSyncPermission();
+            hasFullSyncPrivileges = true;
+        } catch (SecurityException e) {
+            permissionFailureException = e;
+        }
+
         for (SyncableFlag sf : incomingFlags) {
+            if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) {
+                throw permissionFailureException;
+            }
+
             String ns = sf.getNamespace();
             String name = sf.getName();
             SyncableFlag outFlag;
@@ -76,6 +102,58 @@
         return outputFlags;
     }
 
+    @Override
+    public void overrideFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue());
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.erase(flag.getNamespace(), flag.getName());
+    }
+
+    @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) {
+        assertSyncPermission();
+        List<SyncableFlag> outputFlags = new ArrayList<>();
+        for (SyncableFlag sf : incomingFlags) {
+            String ns = sf.getNamespace();
+            String name = sf.getName();
+            String value;
+            String storeValue = mFlagStore.get(ns, name);
+            boolean overridden  = storeValue != null;
+
+            if (sf.isDynamic()) {
+                value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue());
+            } else {
+                value = mFlagCache.getOrNull(ns, name);
+                if (value == null) {
+                    value = Build.IS_USER ? null : storeValue;
+                    if (value == null) {
+                        value = sf.getValue();
+                    }
+                }
+            }
+            outputFlags.add(new SyncableFlag(
+                    sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden));
+        }
+
+        return outputFlags;
+    }
+
+    private void assertSyncPermission() {
+        mPermissionsChecker.assertSyncPermission();
+        clearCallingIdentity();
+    }
+
+    private void assertWritePermission() {
+        mPermissionsChecker.assertWritePermission();
+        clearCallingIdentity();
+    }
+
+
     @SystemApi
     public int handleShellCommand(
             @NonNull ParcelFileDescriptor in,
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsService.java b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
index a9de173..93b9e9e 100644
--- a/services/flags/java/com/android/server/flags/FeatureFlagsService.java
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
@@ -15,10 +15,15 @@
  */
 package com.android.server.flags;
 
+import static android.Manifest.permission.SYNC_FLAGS;
+import static android.Manifest.permission.WRITE_FLAGS;
+
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.flags.FeatureFlags;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.SystemService;
 
 /**
@@ -53,10 +58,56 @@
     @Override
     public void onStart() {
         Slog.d(TAG, "Started Feature Flag Service");
-        FeatureFlagsBinder service = new FeatureFlagsBinder(mFlagStore, mShellCommand);
+        FeatureFlagsBinder service = new FeatureFlagsBinder(
+                mFlagStore, mShellCommand, new PermissionsChecker(getContext()));
         publishBinderService(
                 Context.FEATURE_FLAGS_SERVICE, service);
         publishLocalService(FeatureFlags.class, new FeatureFlags(service));
     }
 
+    @Override
+    public void onBootPhase(int phase) {
+        super.onBootPhase(phase);
+
+        if (phase == PHASE_SYSTEM_SERVICES_READY) {
+            // Immediately sync our core flags so that they get locked in. We don't want third-party
+            // apps to override them, and syncing immediately is the easiest way to prevent that.
+            FeatureFlags.getInstance().sync();
+        }
+    }
+
+    /**
+     * Delegate for checking flag permissions.
+     */
+    @VisibleForTesting
+    public static class PermissionsChecker {
+        private final Context mContext;
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public PermissionsChecker(Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Ensures that the caller has {@link SYNC_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertSyncPermission() {
+            if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException(
+                        "Non-core flag queried. Requires SYNC_FLAGS permission!");
+            }
+        }
+
+        /**
+         * Ensures that the caller has {@link WRITE_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertWritePermission() {
+            if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires WRITE_FLAGS permission!");
+            }
+        }
+    }
 }
diff --git a/services/flags/java/com/android/server/flags/FlagOverrideStore.java b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
index 9866b1c..b1ddc7e6 100644
--- a/services/flags/java/com/android/server/flags/FlagOverrideStore.java
+++ b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
@@ -53,7 +53,8 @@
     }
 
     /** Put a value in the store. */
-    void set(String namespace, String name, String value) {
+    @VisibleForTesting
+    public void set(String namespace, String name, String value) {
         mSettingsProxy.putString(getPropName(namespace, name), value);
         mCallback.onFlagChanged(namespace, name, value);
     }
@@ -65,7 +66,8 @@
     }
 
     /** Erase a value from the store. */
-    void erase(String namespace, String name) {
+    @VisibleForTesting
+    public void erase(String namespace, String name) {
         set(namespace, name, null);
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
index 8455b88..df4731f 100644
--- a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
@@ -21,6 +21,7 @@
 
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -60,13 +61,16 @@
     private IFeatureFlagsCallback mIFeatureFlagsCallback;
     @Mock
     private IBinder mIFeatureFlagsCallbackAsBinder;
+    @Mock
+    private FeatureFlagsService.PermissionsChecker mPermissionsChecker;
 
     private FeatureFlagsBinder mFeatureFlagsService;
 
     @Before
     public void setup() {
         when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
-        mFeatureFlagsService = new FeatureFlagsBinder(mFlagStore, mFlagCommand);
+        mFeatureFlagsService = new FeatureFlagsBinder(
+                mFlagStore, mFlagCommand, mPermissionsChecker);
     }
 
     @Test
@@ -80,6 +84,40 @@
     }
 
     @Test
+    public void testOverrideFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.overrideFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
+    public void testResetFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.resetFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
     public void testSyncFlags_noOverrides() {
         List<SyncableFlag> inputFlags = List.of(
                 new SyncableFlag(NS, "a", "false", false),
@@ -137,7 +175,6 @@
         }
     }
 
-
     @Test
     public void testSyncFlags_twoCallsWithDifferentDefaults() {
         List<SyncableFlag> inputFlagsFirst = List.of(
@@ -171,6 +208,83 @@
                 .that(found).isTrue();
     }
 
+    @Test
+    public void testQueryFlags_onlyOnce() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testQueryFlags_twoCallsWithDifferentDefaults() {
+        List<SyncableFlag> inputFlagsFirst = List.of(
+                new SyncableFlag(NS, "a", "false", false)
+        );
+        List<SyncableFlag> inputFlagsSecond = List.of(
+                new SyncableFlag(NS, "a", "true", false),
+                new SyncableFlag(NS, "b", "false", false)
+        );
+
+        List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst);
+        List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond);
+
+        assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+        assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+        // This test only cares that the "a" flag passed in the second time came out with the
+        // same value that was passed in (i.e. it wasn't cached).
+
+        boolean found = false;
+        for (SyncableFlag second : outputFlagsSecond) {
+            if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) {
+                found = true;
+                assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue());
+                break;
+            }
+        }
+
+        assertWithMessage(
+                "Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output")
+                .that(found).isTrue();
+    }
+
+    @Test
+    public void testOverrideFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.overrideFlag(f);
+
+        verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue());
+    }
+
+    @Test
+    public void testResetFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.resetFlag(f);
+
+        verify(mFlagStore).erase(f.getNamespace(), f.getName());
+    }
+
+
     private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) {
         return a.getNamespace().equals(b.getNamespace())
                 && a.getName().equals(b.getName())