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;
+ }
+ }
+}