Added DeviceConfig.dump() method

That will allow it to include more information, like the property
change listeners. Example:

$ adb shell dumpsys device_config | grep listeners | head -5
245 listeners for 185 namespaces:
   accessibility: 2 listeners
   activity_manager: 5 listeners

Bug: 364399200
Test: m framework-configinfrastructure.stubs.source.module_lib-update-current-api
Test: atest ConfigInfrastructureServiceUnitTests:DeviceConfigTest
Flag: android.provider.flags.dump_improvements

Change-Id: Ifab219bccededb07de558dced9d6203efe568042
diff --git a/framework/Android.bp b/framework/Android.bp
index e160e7c..4b52710 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -65,6 +65,7 @@
     min_sdk_version: "34",
     apex_available: [
         "com.android.configinfrastructure",
+        "//apex_available:platform", // Used by DeviceConfigService
     ],
     visibility: [
         "//visibility:public",
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index d8f17f9..cbe9399 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -7,6 +7,7 @@
     method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void clearLocalOverride(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.MONITOR_DEVICE_CONFIG_ACCESS) public static void clearMonitorCallback(@NonNull android.content.ContentResolver);
     method @RequiresPermission(anyOf={android.Manifest.permission.WRITE_DEVICE_CONFIG, android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG}) public static boolean deleteProperty(@NonNull String, @NonNull String);
+    method @FlaggedApi("android.provider.flags.dump_improvements") @RequiresPermission(android.Manifest.permission.DUMP) public static void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String, @Nullable String[]);
     method @NonNull public static java.util.Set<java.lang.String> getAdbWritableFlags();
     method @NonNull public static java.util.Set<android.provider.DeviceConfig.Properties> getAllProperties();
     method public static boolean getBoolean(@NonNull String, @NonNull String, boolean);
diff --git a/framework/flags.aconfig b/framework/flags.aconfig
index f4d93fd..0d5346f 100644
--- a/framework/flags.aconfig
+++ b/framework/flags.aconfig
@@ -9,3 +9,11 @@
   is_fixed_read_only: true
   is_exported: true
 }
+
+flag {
+    name: "dump_improvements"
+    namespace: "core_experiments_team_internal"
+    description: "Added more information on `dumpsys device_config`"
+    bug: "364399200"
+  is_exported: true
+}
diff --git a/framework/java/android/provider/DeviceConfig.java b/framework/java/android/provider/DeviceConfig.java
index 6351404..7b97151 100644
--- a/framework/java/android/provider/DeviceConfig.java
+++ b/framework/java/android/provider/DeviceConfig.java
@@ -20,9 +20,11 @@
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
 import static android.Manifest.permission.READ_WRITE_SYNC_DISABLED_MODE_CONFIG;
+import static android.Manifest.permission.DUMP;
 
 import android.Manifest;
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -32,13 +34,17 @@
 import android.content.ContentResolver;
 import android.database.ContentObserver;
 import android.net.Uri;
-import com.android.modules.utils.build.SdkLevel;
+import android.provider.flags.Flags;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.modules.utils.build.SdkLevel;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -46,12 +52,15 @@
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.concurrent.Executor;
 
 import android.util.Log;
@@ -59,7 +68,9 @@
 import android.provider.aidl.IDeviceConfigManager;
 import android.provider.DeviceConfigServiceManager;
 import android.provider.DeviceConfigInitializer;
+import android.os.Binder;
 import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
 
 /**
  * Device level configuration parameters which can be tuned by a separate configuration service.
@@ -1021,7 +1032,7 @@
      */
     @SystemApi
     public static final int SYNC_DISABLED_MODE_UNTIL_REBOOT = 2;
-    
+
     private static final Object sLock = new Object();
     @GuardedBy("sLock")
     private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
@@ -1524,6 +1535,56 @@
         }
     }
 
+    // NOTE: this API is only used by the framework code, but using MODULE_LIBRARIES causes a
+    // build-time error on CtsDeviceConfigTestCases, so it's using PRIVILEGED_APPS.
+    /**
+     * Dumps internal state into the given {@code fd} or {@code pw}.
+     *
+     * @param fd file descriptor that will output the dump state. Typically used for binary dumps.
+     * @param pw print writer that will output the dump state. Typically used for formatted text.
+     * @param prefix prefix added to each line
+     * @param args (optional) arguments passed by {@code dumpsys}.
+     *
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+    @FlaggedApi(Flags.FLAG_DUMP_IMPROVEMENTS)
+    @RequiresPermission(DUMP)
+    public static void dump(@NonNull ParcelFileDescriptor fd, @NonNull PrintWriter pw,
+            @NonNull String dumpPrefix, @Nullable String[] args) {
+        Comparator<OnPropertiesChangedListener> comparator = (o1, o2) -> o1.toString()
+                .compareTo(o2.toString());
+        TreeMap<String, Set<OnPropertiesChangedListener>> listenersByNamespace  =
+                new TreeMap<>();
+        ArraySet<OnPropertiesChangedListener> uniqueListeners = new ArraySet<>();
+        int listenersSize;
+        synchronized (sLock) {
+            listenersSize = sListeners.size();
+            for (int i = 0; i < listenersSize; i++) {
+                var namespace = sListeners.valueAt(i).first;
+                var listener = sListeners.keyAt(i);
+                var listeners = listenersByNamespace.get(namespace);
+                if (listeners == null) {
+                    // Life would be so much easier if Android provided a MultiMap implementation...
+                    listeners = new TreeSet<>(comparator);
+                    listenersByNamespace.put(namespace, listeners);
+                }
+                listeners.add(listener);
+                uniqueListeners.add(listener);
+            }
+        }
+        pw.printf("%s%d listeners for %d namespaces:\n", dumpPrefix, uniqueListeners.size(),
+                listenersByNamespace.size());
+        for (var entry : listenersByNamespace.entrySet()) {
+            var namespace = entry.getKey();
+            var listeners = entry.getValue();
+            pw.printf("%s%s: %d listeners\n", dumpPrefix, namespace, listeners.size());
+            for (var listener : listeners) {
+                pw.printf("%s%s%s\n", dumpPrefix, dumpPrefix, listener);
+            }
+        }
+    }
+
     /**
      * Remove a listener for property changes. The listener will receive no further notification of
      * property changes.
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
index 8838912..13f927b 100644
--- a/service/javatests/Android.bp
+++ b/service/javatests/Android.bp
@@ -39,6 +39,7 @@
         "androidx.test.rules",
         "androidx.test.runner",
         "androidx.annotation_annotation",
+        "configinfra_framework_flags_java_lib",
         "modules-utils-build",
         "service-configinfrastructure.impl",
         "frameworks-base-testutils",
diff --git a/service/javatests/src/com/android/server/deviceconfig/DeviceConfigTest.java b/service/javatests/src/com/android/server/deviceconfig/DeviceConfigTest.java
new file mode 100644
index 0000000..a32e131
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/DeviceConfigTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 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.server.deviceconfig;
+
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.flags.Flags;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
+import android.provider.DeviceConfig.Properties;
+import android.util.Log;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public final class DeviceConfigTest {
+
+    private static final String TAG = DeviceConfigTest.class.getSimpleName();
+
+    private static final String NAMESPACE_A = "A Space has no name";
+    private static final String NAMESPACE_B = "B Space has no name";
+
+    private static final String DUMP_PREFIX = "..";
+
+    @Rule public final Expect expect = Expect.create();
+    @Rule public final CheckFlagsRule checkFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DUMP_IMPROVEMENTS)
+    public void testDump_empty() throws Exception {
+        String dump = dump();
+
+        expect.withMessage("dump()").that(dump).isEqualTo(DUMP_PREFIX
+                + "0 listeners for 0 namespaces:\n");
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DUMP_IMPROVEMENTS)
+    public void testDump_withListeners() throws Exception {
+        var listener1 = new TestOnPropertiesChangedListener();
+        var listener2 = new TestOnPropertiesChangedListener();
+        var listener3 = new TestOnPropertiesChangedListener();
+
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_A, Runnable::run, listener1);
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_A, Runnable::run, listener2);
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_A, Runnable::run, listener3);
+        // Next call will remove listener1 from NAMESPACE_A
+        DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_B, Runnable::run, listener1);
+
+        try {
+            String dump = dump();
+
+            expect.withMessage("dump()").that(dump).isEqualTo(DUMP_PREFIX
+                    + "3 listeners for 2 namespaces:\n"
+                    + DUMP_PREFIX + NAMESPACE_A + ": 2 listeners\n"
+                    + DUMP_PREFIX + DUMP_PREFIX + listener2 + "\n"
+                    + DUMP_PREFIX + DUMP_PREFIX + listener3 + "\n"
+                    + DUMP_PREFIX + NAMESPACE_B + ": 1 listeners\n"
+                    + DUMP_PREFIX + DUMP_PREFIX + listener1 + "\n"
+                    );
+        } finally {
+            DeviceConfig.removeOnPropertiesChangedListener(listener1);
+            DeviceConfig.removeOnPropertiesChangedListener(listener2);
+            DeviceConfig.removeOnPropertiesChangedListener(listener3);
+        }
+    }
+
+    private String dump(String...args) throws IOException {
+        try (StringWriter sw = new StringWriter()) {
+            PrintWriter pw = new PrintWriter(sw);
+
+            DeviceConfig.dump(/* fd= */ null, pw, DUMP_PREFIX, args);
+
+            pw.flush();
+            String dump = sw.toString();
+
+            Log.v(TAG, "dump() output\n" + dump);
+
+            return dump;
+        }
+    }
+
+    private static final class TestOnPropertiesChangedListener
+            implements OnPropertiesChangedListener {
+
+        private static int sNextId;
+
+        private final int mId = ++sNextId;
+
+        @Override
+        public void onPropertiesChanged(Properties properties) {
+            throw new UnsupportedOperationException("Not used in any test (yet?)");
+        }
+
+        @Override
+        public String toString() {
+            return "TestOnPropertiesChangedListener#" + mId;
+        }
+    }
+}