Enable local DeviceConfig overriding from adb.

Test: new unit test
Bug: 298392357
Change-Id: I4b3736f6742c20e2c6ae39b1536c8c44707c9c4c
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 346462d..92ebe09 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -31,6 +31,7 @@
         "unsupportedappusage",
     ],
     static_libs: [
+        "device_config_service_flags_java",
         "junit",
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibDisplayUtils",
@@ -56,7 +57,10 @@
     ],
     static_libs: [
         "androidx.test.rules",
+        "device_config_service_flags_java",
+        "flag-junit",
         "mockito-target-minus-junit4",
+        "platform-test-annotations",
         "SettingsLibDeviceStateRotationLock",
         "SettingsLibDisplayUtils",
         "platform-test-annotations",
@@ -79,3 +83,16 @@
     manifest: "test/AndroidManifest.xml",
     test_config: "test/AndroidTest.xml",
 }
+
+aconfig_declarations {
+    name: "device_config_service_flags",
+    package: "com.android.providers.settings",
+    srcs: [
+        "src/com/android/providers/settings/device_config_service.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "device_config_service_flags_java",
+    aconfig_declarations: "device_config_service_flags",
+}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index ffaebf4..b57f6ca 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -19,6 +19,7 @@
 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_NONE;
 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_PERSISTENT;
 import static android.provider.Settings.Config.SYNC_DISABLED_MODE_UNTIL_REBOOT;
+import static com.android.providers.settings.Flags.supportOverrides;
 
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
@@ -42,10 +43,8 @@
 
 import java.io.File;
 import java.io.FileDescriptor;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.InputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.lang.reflect.Field;
@@ -69,6 +68,11 @@
         "/system_ext/etc/aconfig_flags.textproto",
         "/vendor/etc/aconfig_flags.textproto");
 
+    private static final List<String> PRIVATE_NAMESPACES = List.of(
+            "device_config_overrides",
+            "staged",
+            "token_staged");
+
     final SettingsProvider mProvider;
 
     public DeviceConfigService(SettingsProvider provider) {
@@ -171,6 +175,8 @@
         enum CommandVerb {
             GET,
             PUT,
+            OVERRIDE,
+            CLEAR_OVERRIDE,
             DELETE,
             LIST,
             LIST_NAMESPACES,
@@ -244,6 +250,10 @@
                 verb = CommandVerb.GET;
             } else if ("put".equalsIgnoreCase(cmd)) {
                 verb = CommandVerb.PUT;
+            } else if (supportOverrides() && "override".equalsIgnoreCase(cmd)) {
+                verb = CommandVerb.OVERRIDE;
+            } else if (supportOverrides() && "clear_override".equalsIgnoreCase(cmd)) {
+                verb = CommandVerb.CLEAR_OVERRIDE;
             } else if ("delete".equalsIgnoreCase(cmd)) {
                 verb = CommandVerb.DELETE;
             } else if ("list".equalsIgnoreCase(cmd)) {
@@ -330,7 +340,7 @@
                         publicOnly = true;
                     }
                 } else if (namespace == null) {
-                    // GET, PUT, DELETE, LIST 1st arg
+                    // GET, PUT, OVERRIDE, DELETE, LIST 1st arg
                     namespace = arg;
                     if (verb == CommandVerb.LIST) {
                         if (peekNextArg() == null) {
@@ -342,9 +352,12 @@
                         }
                     }
                 } else if (key == null) {
-                    // GET, PUT, DELETE 2nd arg
+                    // GET, PUT, OVERRIDE, DELETE 2nd arg
                     key = arg;
-                    if ((verb == CommandVerb.GET || verb == CommandVerb.DELETE)) {
+                    boolean validVerb = verb == CommandVerb.GET
+                            || verb == CommandVerb.DELETE
+                            || verb == CommandVerb.CLEAR_OVERRIDE;
+                    if (validVerb) {
                         // GET, DELETE only have 2 args
                         if (peekNextArg() == null) {
                             isValid = true;
@@ -355,9 +368,11 @@
                         }
                     }
                 } else if (value == null) {
-                    // PUT 3rd arg (required)
+                    // PUT, OVERRIDE 3rd arg (required)
                     value = arg;
-                    if (verb == CommandVerb.PUT && peekNextArg() == null) {
+                    boolean validVerb = verb == CommandVerb.PUT
+                            || verb == CommandVerb.OVERRIDE;
+                    if (validVerb && peekNextArg() == null) {
                         isValid = true;
                     }
                 } else if ("default".equalsIgnoreCase(arg)) {
@@ -387,22 +402,80 @@
                 case PUT:
                     DeviceConfig.setProperty(namespace, key, value, makeDefault);
                     break;
+                case OVERRIDE:
+                    if (supportOverrides()) {
+                        DeviceConfig.setLocalOverride(namespace, key, value);
+                    }
+                    break;
+                case CLEAR_OVERRIDE:
+                    if (supportOverrides()) {
+                        DeviceConfig.clearLocalOverride(namespace, key);
+                    }
+                    break;
                 case DELETE:
                     pout.println(delete(iprovider, namespace, key)
                             ? "Successfully deleted " + key + " from " + namespace
                             : "Failed to delete " + key + " from " + namespace);
                     break;
                 case LIST:
-                    if (namespace != null) {
-                        DeviceConfig.Properties properties = DeviceConfig.getProperties(namespace);
-                        List<String> keys = new ArrayList<>(properties.getKeyset());
-                        Collections.sort(keys);
-                        for (String name : keys) {
-                            pout.println(name + "=" + properties.getString(name, null));
+                    if (supportOverrides()) {
+                        pout.println("Server overrides:");
+
+                        Map<String, Map<String, String>> underlyingValues =
+                                DeviceConfig.getUnderlyingValuesForOverriddenFlags();
+
+                        if (namespace != null) {
+                            DeviceConfig.Properties properties =
+                                    DeviceConfig.getProperties(namespace);
+                            List<String> keys = new ArrayList<>(properties.getKeyset());
+                            Collections.sort(keys);
+                            for (String name : keys) {
+                                String valueReadFromDeviceConfig = properties.getString(name, null);
+                                String underlyingValue = underlyingValues.get(namespace).get(name);
+                                String printValue = underlyingValue != null
+                                        ? underlyingValue
+                                        : valueReadFromDeviceConfig;
+                                pout.println(name + "=" + printValue);
+                            }
+                        } else {
+                            for (String line : listAll(iprovider)) {
+                                boolean isPrivateNamespace = false;
+                                for (String privateNamespace : PRIVATE_NAMESPACES) {
+                                    if (line.startsWith(privateNamespace)) {
+                                        isPrivateNamespace = true;
+                                    }
+                                }
+                                if (!isPrivateNamespace) {
+                                    pout.println(line);
+                                }
+                            }
+                        }
+
+                        pout.println("");
+                        pout.println("Local overrides (these take precedence):");
+                        for (String overrideNamespace : underlyingValues.keySet()) {
+                            Map<String, String> flagToValue =
+                                    underlyingValues.get(overrideNamespace);
+                            for (String flag : flagToValue.keySet()) {
+                                String flagText = overrideNamespace + "/" + flag;
+                                String valueText =
+                                        DeviceConfig.getProperty(overrideNamespace, flag);
+                                pout.println(flagText + "=" + valueText);
+                            }
                         }
                     } else {
-                        for (String line : listAll(iprovider)) {
-                            pout.println(line);
+                        if (namespace != null) {
+                            DeviceConfig.Properties properties =
+                                    DeviceConfig.getProperties(namespace);
+                            List<String> keys = new ArrayList<>(properties.getKeyset());
+                            Collections.sort(keys);
+                            for (String name : keys) {
+                                pout.println(name + "=" + properties.getString(name, null));
+                            }
+                        } else {
+                            for (String line : listAll(iprovider)) {
+                                pout.println(line);
+                            }
                         }
                     }
                     break;
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
new file mode 100644
index 0000000..bb8bb00
--- /dev/null
+++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.providers.settings"
+
+flag {
+    name: "support_overrides"
+    namespace: "core_experiments_team_internal"
+    description: "When enabled, allows setting and displaying local overrides via adb."
+    bug: "b/298392357"
+}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
index 753378b..8dd51b2 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
@@ -22,21 +22,30 @@
 
 import android.content.ContentResolver;
 import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.common.io.CharStreams;
+
 import libcore.io.Streams;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 
 /**
  * Tests for {@link DeviceConfigService}.
@@ -49,6 +58,10 @@
 
     private ContentResolver mContentResolver;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         mContentResolver = InstrumentationRegistry.getContext().getContentResolver();
@@ -59,6 +72,39 @@
         deleteFromContentProvider(mContentResolver, sNamespace, sKey);
     }
 
+    /**
+     * Test that setting overrides are properly disabled when the flag is off.
+     */
+    @Test
+    @RequiresFlagsDisabled("com.android.providers.settings.support_overrides")
+    public void testOverrideDisabled() throws IOException {
+        final String newValue = "value2";
+
+        executeShellCommand("device_config put " + sNamespace + " " + sKey + " " + sValue);
+        executeShellCommand("device_config override " + sNamespace + " " + sKey + " " + newValue);
+        String result = readShellCommandOutput("device_config get " + sNamespace + " " + sKey);
+        assertEquals(sValue + "\n", result);
+    }
+
+    /**
+     * Test that overrides are readable and can be cleared.
+     */
+    @Test
+    @RequiresFlagsEnabled("com.android.providers.settings.support_overrides")
+    public void testOverride() throws IOException {
+        final String newValue = "value2";
+
+        executeShellCommand("device_config put " + sNamespace + " " + sKey + " " + sValue);
+        executeShellCommand("device_config override " + sNamespace + " " + sKey + " " + newValue);
+
+        String result = readShellCommandOutput("device_config get " + sNamespace + " " + sKey);
+        assertEquals(newValue + "\n", result);
+
+        executeShellCommand("device_config clear_override " + sNamespace + " " + sKey);
+        result = readShellCommandOutput("device_config get " + sNamespace + " " + sKey);
+        assertEquals(sValue + "\n", result);
+    }
+
     @Test
     public void testPut() throws Exception {
         final String newNamespace = "namespace2";
@@ -165,6 +211,12 @@
         Streams.readFully(is);
     }
 
+    private static String readShellCommandOutput(String command) throws IOException {
+        InputStream is = new FileInputStream(InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().executeShellCommand(command).getFileDescriptor());
+        return CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8));
+    }
+
     private static void putWithContentProvider(ContentResolver resolver, String namespace,
             String key, String value) {
         putWithContentProvider(resolver, namespace, key, value, false);