Adds DeviceConfig.getProperties method for atomic reading.

This fetches multiple flags atomically, either the entire contents of
the namespace, or just a list of flags specified by the caller. This
update also enables the local Settings class to fetch and cache the entire namespace
whenever any flags from that namespace are read so that we only incur the cost of the
IPC once.

Test: atest FrameworksCoreTests:DeviceConfigTest
      atest FrameworksCoreTests:SettingsProviderTest
      atest SettingsProviderTest:DeviceConfigServiceTest
Bug: 136135417
Change-Id: I0be7c4b51c37590f5001e53b074b06246851a198
diff --git a/api/system-current.txt b/api/system-current.txt
index fc48845..0f81f8de 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5875,6 +5875,7 @@
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static float getFloat(@NonNull String, @NonNull String, float);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static int getInt(@NonNull String, @NonNull String, int);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static long getLong(@NonNull String, @NonNull String, long);
+    method @NonNull @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static android.provider.DeviceConfig.Properties getProperties(@NonNull String, @NonNull java.lang.String...);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getProperty(@NonNull String, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(@NonNull String, @NonNull String, @Nullable String);
     method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener);
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index fd1381a..e456c8a 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -403,9 +403,37 @@
     @TestApi
     @RequiresPermission(READ_DEVICE_CONFIG)
     public static String getProperty(@NonNull String namespace, @NonNull String name) {
+        // Fetch all properties for the namespace at once and cache them in the local process, so we
+        // incur the cost of the IPC less often. Lookups happen much more frequently than updates,
+        // and we want to optimize the former.
+        return getProperties(namespace, name).getString(name, null);
+    }
+
+    /**
+     * Look up the values of multiple properties for a particular namespace. The lookup is atomic,
+     * such that the values of these properties cannot change between the time when the first is
+     * fetched and the time when the last is fetched.
+     *
+     * TODO: reference setProperties when it is added.
+     *
+     * @param namespace The namespace containing the properties to look up.
+     * @param names     The names of properties to look up, or empty to fetch all properties for the
+     *                  given namespace.
+     * @return {@link Properties} object containing the requested properties. This reflects the
+     *     state of these properties at the time of the lookup, and is not updated to reflect any
+     *     future changes. The keyset of this Properties object will contain only the intersection
+     *     of properties already set and properties requested via the names parameter. Properties
+     *     that are already set but were not requested will not be contained here. Properties that
+     *     are not set, but were requested will not be contained here either.
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @RequiresPermission(READ_DEVICE_CONFIG)
+    public static Properties getProperties(@NonNull String namespace, @NonNull String ... names) {
         ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
-        String compositeName = createCompositeName(namespace, name);
-        return Settings.Config.getString(contentResolver, compositeName);
+        return new Properties(namespace,
+                Settings.Config.getStrings(contentResolver, namespace, Arrays.asList(names)));
     }
 
     /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e4e8bf7..457dcc0 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -84,8 +84,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -2248,7 +2250,7 @@
         private static final String NAME_EQ_PLACEHOLDER = "name=?";
 
         // Must synchronize on 'this' to access mValues and mValuesVersion.
-        private final HashMap<String, String> mValues = new HashMap<>();
+        private final ArrayMap<String, String> mValues = new ArrayMap<>();
 
         private final Uri mUri;
         @UnsupportedAppUsage
@@ -2258,15 +2260,22 @@
         // for the fast path of retrieving settings.
         private final String mCallGetCommand;
         private final String mCallSetCommand;
+        private final String mCallListCommand;
 
         @GuardedBy("this")
         private GenerationTracker mGenerationTracker;
 
         public NameValueCache(Uri uri, String getCommand, String setCommand,
                 ContentProviderHolder providerHolder) {
+            this(uri, getCommand, setCommand, null, providerHolder);
+        }
+
+        NameValueCache(Uri uri, String getCommand, String setCommand, String listCommand,
+                ContentProviderHolder providerHolder) {
             mUri = uri;
             mCallGetCommand = getCommand;
             mCallSetCommand = setCommand;
+            mCallListCommand = listCommand;
             mProviderHolder = providerHolder;
         }
 
@@ -2448,8 +2457,8 @@
 
                 String value = c.moveToNext() ? c.getString(0) : null;
                 synchronized (NameValueCache.this) {
-                    if(mGenerationTracker != null &&
-                            currentGeneration == mGenerationTracker.getCurrentGeneration()) {
+                    if (mGenerationTracker != null
+                            && currentGeneration == mGenerationTracker.getCurrentGeneration()) {
                         mValues.put(name, value);
                     }
                 }
@@ -2466,6 +2475,141 @@
             }
         }
 
+        public ArrayMap<String, String> getStringsForPrefix(ContentResolver cr, String prefix,
+                List<String> names) {
+            ArrayMap<String, String> keyValues = new ArrayMap<>();
+            int currentGeneration = -1;
+
+            synchronized (NameValueCache.this) {
+                if (mGenerationTracker != null) {
+                    if (mGenerationTracker.isGenerationChanged()) {
+                        if (DEBUG) {
+                            Log.i(TAG, "Generation changed for type:" + mUri.getPath()
+                                    + " in package:" + cr.getPackageName());
+                        }
+                        mValues.clear();
+                    } else {
+                        boolean prefixCached = false;
+                        int size = mValues.size();
+                        for (int i = 0; i < size; ++i) {
+                            if (mValues.keyAt(i).startsWith(prefix + "/")) {
+                                prefixCached = true;
+                                break;
+                            }
+                        }
+                        if (prefixCached) {
+                            if (!names.isEmpty()) {
+                                for (String name : names) {
+                                    if (mValues.containsKey(name)) {
+                                        keyValues.put(name, mValues.get(name));
+                                    }
+                                }
+                            } else {
+                                for (int i = 0; i < size; ++i) {
+                                    String key = mValues.keyAt(i);
+                                    if (key.startsWith(prefix + "/")) {
+                                        keyValues.put(key, mValues.get(key));
+                                    }
+                                }
+                            }
+                            return keyValues;
+                        }
+                    }
+                    if (mGenerationTracker != null) {
+                        currentGeneration = mGenerationTracker.getCurrentGeneration();
+                    }
+                }
+            }
+
+            if (mCallListCommand == null) {
+                // No list command specified, return empty map
+                return keyValues;
+            }
+            IContentProvider cp = mProviderHolder.getProvider(cr);
+
+            try {
+                Bundle args = new Bundle();
+                args.putString(Settings.CALL_METHOD_PREFIX_KEY, prefix);
+                boolean needsGenerationTracker = false;
+                synchronized (NameValueCache.this) {
+                    if (mGenerationTracker == null) {
+                        needsGenerationTracker = true;
+                        args.putString(CALL_METHOD_TRACK_GENERATION_KEY, null);
+                        if (DEBUG) {
+                            Log.i(TAG, "Requested generation tracker for type: "
+                                    + mUri.getPath() + " in package:" + cr.getPackageName());
+                        }
+                    }
+                }
+
+                // Fetch all flags for the namespace at once for caching purposes
+                Bundle b = cp.call(cr.getPackageName(), mProviderHolder.mUri.getAuthority(),
+                        mCallListCommand, null, args);
+                if (b == null) {
+                    // Invalid response, return an empty map
+                    return keyValues;
+                }
+
+                // All flags for the namespace
+                Map<String, String> flagsToValues =
+                        (HashMap) b.getSerializable(Settings.NameValueTable.VALUE);
+                // Only the flags requested by the caller
+                if (!names.isEmpty()) {
+                    for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
+                        if (names.contains(flag.getKey())) {
+                            keyValues.put(flag.getKey(), flag.getValue());
+                        }
+                    }
+                } else {
+                    keyValues.putAll(flagsToValues);
+                }
+
+                synchronized (NameValueCache.this) {
+                    if (needsGenerationTracker) {
+                        MemoryIntArray array = b.getParcelable(
+                                CALL_METHOD_TRACK_GENERATION_KEY);
+                        final int index = b.getInt(
+                                CALL_METHOD_GENERATION_INDEX_KEY, -1);
+                        if (array != null && index >= 0) {
+                            final int generation = b.getInt(
+                                    CALL_METHOD_GENERATION_KEY, 0);
+                            if (DEBUG) {
+                                Log.i(TAG, "Received generation tracker for type:"
+                                        + mUri.getPath() + " in package:"
+                                        + cr.getPackageName() + " with index:" + index);
+                            }
+                            if (mGenerationTracker != null) {
+                                mGenerationTracker.destroy();
+                            }
+                            mGenerationTracker = new GenerationTracker(array, index,
+                                    generation, () -> {
+                                synchronized (NameValueCache.this) {
+                                    Log.e(TAG, "Error accessing generation tracker"
+                                            + " - removing");
+                                    if (mGenerationTracker != null) {
+                                        GenerationTracker generationTracker =
+                                                mGenerationTracker;
+                                        mGenerationTracker = null;
+                                        generationTracker.destroy();
+                                        mValues.clear();
+                                    }
+                                }
+                            });
+                        }
+                    }
+                    if (mGenerationTracker != null && currentGeneration
+                            == mGenerationTracker.getCurrentGeneration()) {
+                        // cache the complete list of flags for the namespace
+                        mValues.putAll(flagsToValues);
+                    }
+                }
+                return keyValues;
+            } catch (RemoteException e) {
+                // Not supported by the remote side, return an empty map
+                return keyValues;
+            }
+        }
+
         public void clearGenerationTrackerForTest() {
             synchronized (NameValueCache.this) {
                 if (mGenerationTracker != null) {
@@ -13499,6 +13643,7 @@
                 DeviceConfig.CONTENT_URI,
                 CALL_METHOD_GET_CONFIG,
                 CALL_METHOD_PUT_CONFIG,
+                CALL_METHOD_LIST_CONFIG,
                 sProviderHolder);
 
         /**
@@ -13515,6 +13660,37 @@
         }
 
         /**
+         * Look up a list of names in the database, based on a common prefix.
+         *
+         * @param resolver to access the database with
+         * @param prefix to apply to all of the names which will be fetched
+         * @param names to look up in the table
+         * @return a non null, but possibly empty, map from name to value for any of the names that
+         *         were found during lookup.
+         *
+         * @hide
+         */
+        @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
+        static Map<String, String> getStrings(@NonNull ContentResolver resolver,
+                @NonNull String prefix, @NonNull List<String> names) {
+            List<String> concatenatedNames = new ArrayList<>(names.size());
+            for (String name : names) {
+                concatenatedNames.add(prefix + "/" + name);
+            }
+
+            ArrayMap<String, String> rawKeyValues = sNameValueCache.getStringsForPrefix(
+                    resolver, prefix, concatenatedNames);
+            int size = rawKeyValues.size();
+            int substringLength = prefix.length() + 1;
+            ArrayMap<String, String> keyValues = new ArrayMap<>(size);
+            for (int i = 0; i < size; ++i) {
+                keyValues.put(rawKeyValues.keyAt(i).substring(substringLength),
+                        rawKeyValues.valueAt(i));
+            }
+            return keyValues;
+        }
+
+        /**
          * Store a name/value pair into the database.
          * <p>
          * Also the method takes an argument whether to make the value the default for this setting.
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 23fabce..77b7f2a 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -17,6 +17,7 @@
 package android.provider;
 
 import static android.provider.DeviceConfig.OnPropertiesChangedListener;
+import static android.provider.DeviceConfig.Properties;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -42,27 +43,33 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DeviceConfigTest {
-    // TODO(b/109919982): Migrate tests to CTS
-    private static final String sNamespace = "namespace1";
-    private static final String sKey = "key1";
-    private static final String sValue = "value1";
     private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
+    private static final String DEFAULT_VALUE = "test_default_value";
+    private static final String NAMESPACE = "namespace1";
+    private static final String KEY = "key1";
+    private static final String KEY2 = "key2";
+    private static final String KEY3 = "key3";
+    private static final String VALUE = "value1";
+    private static final String VALUE2 = "value2";
+    private static final String VALUE3 = "value3";
 
     @After
     public void cleanUp() {
-        deleteViaContentProvider(sNamespace, sKey);
+        deleteViaContentProvider(NAMESPACE, KEY);
+        deleteViaContentProvider(NAMESPACE, KEY2);
+        deleteViaContentProvider(NAMESPACE, KEY3);
     }
 
     @Test
     public void getProperty_empty() {
-        String result = DeviceConfig.getProperty(sNamespace, sKey);
+        String result = DeviceConfig.getProperty(NAMESPACE, KEY);
         assertThat(result).isNull();
     }
 
     @Test
     public void getProperty_nullNamespace() {
         try {
-            DeviceConfig.getProperty(null, sKey);
+            DeviceConfig.getProperty(null, KEY);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -72,7 +79,7 @@
     @Test
     public void getProperty_nullName() {
         try {
-            DeviceConfig.getProperty(sNamespace, null);
+            DeviceConfig.getProperty(NAMESPACE, null);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -82,13 +89,13 @@
     @Test
     public void getString_empty() {
         final String default_value = "default_value";
-        final String result = DeviceConfig.getString(sNamespace, sKey, default_value);
+        final String result = DeviceConfig.getString(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(default_value);
     }
 
     @Test
     public void getString_nullDefault() {
-        final String result = DeviceConfig.getString(sNamespace, sKey, null);
+        final String result = DeviceConfig.getString(NAMESPACE, KEY, null);
         assertThat(result).isNull();
     }
 
@@ -96,16 +103,16 @@
     public void getString_nonEmpty() {
         final String value = "new_value";
         final String default_value = "default";
-        DeviceConfig.setProperty(sNamespace, sKey, value, false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, value, false);
 
-        final String result = DeviceConfig.getString(sNamespace, sKey, default_value);
+        final String result = DeviceConfig.getString(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(value);
     }
 
     @Test
     public void getString_nullNamespace() {
         try {
-            DeviceConfig.getString(null, sKey, "default_value");
+            DeviceConfig.getString(null, KEY, "default_value");
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -115,7 +122,7 @@
     @Test
     public void getString_nullName() {
         try {
-            DeviceConfig.getString(sNamespace, null, "default_value");
+            DeviceConfig.getString(NAMESPACE, null, "default_value");
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -125,7 +132,7 @@
     @Test
     public void getBoolean_empty() {
         final boolean default_value = true;
-        final boolean result = DeviceConfig.getBoolean(sNamespace, sKey, default_value);
+        final boolean result = DeviceConfig.getBoolean(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(default_value);
     }
 
@@ -133,18 +140,18 @@
     public void getBoolean_valid() {
         final boolean value = true;
         final boolean default_value = false;
-        DeviceConfig.setProperty(sNamespace, sKey, String.valueOf(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, String.valueOf(value), false);
 
-        final boolean result = DeviceConfig.getBoolean(sNamespace, sKey, default_value);
+        final boolean result = DeviceConfig.getBoolean(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(value);
     }
 
     @Test
     public void getBoolean_invalid() {
         final boolean default_value = true;
-        DeviceConfig.setProperty(sNamespace, sKey, "not_a_boolean", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, "not_a_boolean", false);
 
-        final boolean result = DeviceConfig.getBoolean(sNamespace, sKey, default_value);
+        final boolean result = DeviceConfig.getBoolean(NAMESPACE, KEY, default_value);
         // Anything non-null other than case insensitive "true" parses to false.
         assertThat(result).isFalse();
     }
@@ -152,7 +159,7 @@
     @Test
     public void getBoolean_nullNamespace() {
         try {
-            DeviceConfig.getBoolean(null, sKey, false);
+            DeviceConfig.getBoolean(null, KEY, false);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -162,7 +169,7 @@
     @Test
     public void getBoolean_nullName() {
         try {
-            DeviceConfig.getBoolean(sNamespace, null, false);
+            DeviceConfig.getBoolean(NAMESPACE, null, false);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -172,7 +179,7 @@
     @Test
     public void getInt_empty() {
         final int default_value = 999;
-        final int result = DeviceConfig.getInt(sNamespace, sKey, default_value);
+        final int result = DeviceConfig.getInt(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(default_value);
     }
 
@@ -180,18 +187,18 @@
     public void getInt_valid() {
         final int value = 123;
         final int default_value = 999;
-        DeviceConfig.setProperty(sNamespace, sKey, String.valueOf(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, String.valueOf(value), false);
 
-        final int result = DeviceConfig.getInt(sNamespace, sKey, default_value);
+        final int result = DeviceConfig.getInt(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(value);
     }
 
     @Test
     public void getInt_invalid() {
         final int default_value = 999;
-        DeviceConfig.setProperty(sNamespace, sKey, "not_an_int", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, "not_an_int", false);
 
-        final int result = DeviceConfig.getInt(sNamespace, sKey, default_value);
+        final int result = DeviceConfig.getInt(NAMESPACE, KEY, default_value);
         // Failure to parse results in using the default value
         assertThat(result).isEqualTo(default_value);
     }
@@ -199,7 +206,7 @@
     @Test
     public void getInt_nullNamespace() {
         try {
-            DeviceConfig.getInt(null, sKey, 0);
+            DeviceConfig.getInt(null, KEY, 0);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -209,7 +216,7 @@
     @Test
     public void getInt_nullName() {
         try {
-            DeviceConfig.getInt(sNamespace, null, 0);
+            DeviceConfig.getInt(NAMESPACE, null, 0);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -219,7 +226,7 @@
     @Test
     public void getLong_empty() {
         final long default_value = 123456;
-        final long result = DeviceConfig.getLong(sNamespace, sKey, default_value);
+        final long result = DeviceConfig.getLong(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(default_value);
     }
 
@@ -227,18 +234,18 @@
     public void getLong_valid() {
         final long value = 456789;
         final long default_value = 123456;
-        DeviceConfig.setProperty(sNamespace, sKey, String.valueOf(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, String.valueOf(value), false);
 
-        final long result = DeviceConfig.getLong(sNamespace, sKey, default_value);
+        final long result = DeviceConfig.getLong(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(value);
     }
 
     @Test
     public void getLong_invalid() {
         final long default_value = 123456;
-        DeviceConfig.setProperty(sNamespace, sKey, "not_a_long", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, "not_a_long", false);
 
-        final long result = DeviceConfig.getLong(sNamespace, sKey, default_value);
+        final long result = DeviceConfig.getLong(NAMESPACE, KEY, default_value);
         // Failure to parse results in using the default value
         assertThat(result).isEqualTo(default_value);
     }
@@ -246,7 +253,7 @@
     @Test
     public void getLong_nullNamespace() {
         try {
-            DeviceConfig.getLong(null, sKey, 0);
+            DeviceConfig.getLong(null, KEY, 0);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -256,7 +263,7 @@
     @Test
     public void getLong_nullName() {
         try {
-            DeviceConfig.getLong(sNamespace, null, 0);
+            DeviceConfig.getLong(NAMESPACE, null, 0);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -266,7 +273,7 @@
     @Test
     public void getFloat_empty() {
         final float default_value = 123.456f;
-        final float result = DeviceConfig.getFloat(sNamespace, sKey, default_value);
+        final float result = DeviceConfig.getFloat(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(default_value);
     }
 
@@ -274,18 +281,18 @@
     public void getFloat_valid() {
         final float value = 456.789f;
         final float default_value = 123.456f;
-        DeviceConfig.setProperty(sNamespace, sKey, String.valueOf(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, String.valueOf(value), false);
 
-        final float result = DeviceConfig.getFloat(sNamespace, sKey, default_value);
+        final float result = DeviceConfig.getFloat(NAMESPACE, KEY, default_value);
         assertThat(result).isEqualTo(value);
     }
 
     @Test
     public void getFloat_invalid() {
         final float default_value = 123.456f;
-        DeviceConfig.setProperty(sNamespace, sKey, "not_a_float", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, "not_a_float", false);
 
-        final float result = DeviceConfig.getFloat(sNamespace, sKey, default_value);
+        final float result = DeviceConfig.getFloat(NAMESPACE, KEY, default_value);
         // Failure to parse results in using the default value
         assertThat(result).isEqualTo(default_value);
     }
@@ -293,7 +300,7 @@
     @Test
     public void getFloat_nullNamespace() {
         try {
-            DeviceConfig.getFloat(null, sKey, 0);
+            DeviceConfig.getFloat(null, KEY, 0);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -303,7 +310,7 @@
     @Test
     public void getFloat_nullName() {
         try {
-            DeviceConfig.getFloat(sNamespace, null, 0);
+            DeviceConfig.getFloat(NAMESPACE, null, 0);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -313,7 +320,7 @@
     @Test
     public void setProperty_nullNamespace() {
         try {
-            DeviceConfig.setProperty(null, sKey, sValue, false);
+            DeviceConfig.setProperty(null, KEY, VALUE, false);
             Assert.fail("Null namespace should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -323,7 +330,7 @@
     @Test
     public void setProperty_nullName() {
         try {
-            DeviceConfig.setProperty(sNamespace, null, sValue, false);
+            DeviceConfig.setProperty(NAMESPACE, null, VALUE, false);
             Assert.fail("Null name should have resulted in an NPE.");
         } catch (NullPointerException e) {
             // expected
@@ -332,16 +339,16 @@
 
     @Test
     public void setAndGetProperty_sameNamespace() {
-        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
-        String result = DeviceConfig.getProperty(sNamespace, sKey);
-        assertThat(result).isEqualTo(sValue);
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        String result = DeviceConfig.getProperty(NAMESPACE, KEY);
+        assertThat(result).isEqualTo(VALUE);
     }
 
     @Test
     public void setAndGetProperty_differentNamespace() {
         String newNamespace = "namespace2";
-        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
-        String result = DeviceConfig.getProperty(newNamespace, sKey);
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        String result = DeviceConfig.getProperty(newNamespace, KEY);
         assertThat(result).isNull();
     }
 
@@ -349,41 +356,147 @@
     public void setAndGetProperty_multipleNamespaces() {
         String newNamespace = "namespace2";
         String newValue = "value2";
-        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
-        DeviceConfig.setProperty(newNamespace, sKey, newValue, false);
-        String result = DeviceConfig.getProperty(sNamespace, sKey);
-        assertThat(result).isEqualTo(sValue);
-        result = DeviceConfig.getProperty(newNamespace, sKey);
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        DeviceConfig.setProperty(newNamespace, KEY, newValue, false);
+        String result = DeviceConfig.getProperty(NAMESPACE, KEY);
+        assertThat(result).isEqualTo(VALUE);
+        result = DeviceConfig.getProperty(newNamespace, KEY);
         assertThat(result).isEqualTo(newValue);
 
         // clean up
-        deleteViaContentProvider(newNamespace, sKey);
+        deleteViaContentProvider(newNamespace, KEY);
     }
 
     @Test
     public void setAndGetProperty_overrideValue() {
         String newValue = "value2";
-        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
-        DeviceConfig.setProperty(sNamespace, sKey, newValue, false);
-        String result = DeviceConfig.getProperty(sNamespace, sKey);
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        DeviceConfig.setProperty(NAMESPACE, KEY, newValue, false);
+        String result = DeviceConfig.getProperty(NAMESPACE, KEY);
         assertThat(result).isEqualTo(newValue);
     }
 
     @Test
+    public void getProperties_fullNamespace() {
+        Properties properties = DeviceConfig.getProperties(NAMESPACE);
+        assertThat(properties.getKeyset()).isEmpty();
+
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, VALUE2, false);
+        properties = DeviceConfig.getProperties(NAMESPACE);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE);
+        assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(VALUE2);
+
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE3, false);
+        properties = DeviceConfig.getProperties(NAMESPACE);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE3);
+        assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(VALUE2);
+    }
+
+    @Test
+    public void getProperties_getString() {
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, VALUE2, false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE);
+        assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(VALUE2);
+    }
+
+    @Test
+    public void getProperties_getBoolean() {
+        DeviceConfig.setProperty(NAMESPACE, KEY, "true", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, "false", false);
+        DeviceConfig.setProperty(NAMESPACE, KEY3, "not a valid boolean", false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2, KEY3);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2, KEY3);
+        assertThat(properties.getBoolean(KEY, true)).isTrue();
+        assertThat(properties.getBoolean(KEY, false)).isTrue();
+        assertThat(properties.getBoolean(KEY2, true)).isFalse();
+        assertThat(properties.getBoolean(KEY2, false)).isFalse();
+        // KEY3 was set to garbage, anything nonnull but "true" will parse as false
+        assertThat(properties.getBoolean(KEY3, true)).isFalse();
+        assertThat(properties.getBoolean(KEY3, false)).isFalse();
+        // If a key was not set, it will return the default value
+        assertThat(properties.getBoolean("missing_key", true)).isTrue();
+        assertThat(properties.getBoolean("missing_key", false)).isFalse();
+    }
+
+    @Test
+    public void getProperties_getInt() {
+        final int value = 101;
+
+        DeviceConfig.setProperty(NAMESPACE, KEY, Integer.toString(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, "not a valid int", false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getInt(KEY, -1)).isEqualTo(value);
+        // KEY2 was set to garbage, the default value is returned if an int cannot be parsed
+        assertThat(properties.getInt(KEY2, -1)).isEqualTo(-1);
+    }
+
+    @Test
+    public void getProperties_getFloat() {
+        final float value = 101.010f;
+
+        DeviceConfig.setProperty(NAMESPACE, KEY, Float.toString(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, "not a valid float", false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getFloat(KEY, -1.0f)).isEqualTo(value);
+        // KEY2 was set to garbage, the default value is returned if a float cannot be parsed
+        assertThat(properties.getFloat(KEY2, -1.0f)).isEqualTo(-1.0f);
+    }
+
+    @Test
+    public void getProperties_getLong() {
+        final long value = 101;
+
+        DeviceConfig.setProperty(NAMESPACE, KEY, Long.toString(value), false);
+        DeviceConfig.setProperty(NAMESPACE, KEY2, "not a valid long", false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2);
+        assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
+        assertThat(properties.getLong(KEY, -1)).isEqualTo(value);
+        // KEY2 was set to garbage, the default value is returned if a long cannot be parsed
+        assertThat(properties.getLong(KEY2, -1)).isEqualTo(-1);
+    }
+
+    @Test
+    public void getProperties_defaults() {
+        DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
+        DeviceConfig.setProperty(NAMESPACE, KEY3, VALUE3, false);
+
+        Properties properties = DeviceConfig.getProperties(NAMESPACE, KEY, KEY2);
+        assertThat(properties.getKeyset()).containsExactly(KEY);
+        assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE);
+        // not set in DeviceConfig, but requested in getProperties
+        assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE);
+        // set in DeviceConfig, but not requested in getProperties
+        assertThat(properties.getString(KEY3, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE);
+    }
+
+    @Test
     public void testOnPropertiesChangedListener() throws InterruptedException {
         final CountDownLatch countDownLatch = new CountDownLatch(1);
 
         OnPropertiesChangedListener changeListener = (properties) -> {
-            assertThat(properties.getNamespace()).isEqualTo(sNamespace);
-            assertThat(properties.getKeyset()).contains(sKey);
-            assertThat(properties.getString(sKey, "default_value")).isEqualTo(sValue);
+            assertThat(properties.getNamespace()).isEqualTo(NAMESPACE);
+            assertThat(properties.getKeyset()).contains(KEY);
+            assertThat(properties.getString(KEY, "default_value")).isEqualTo(VALUE);
             countDownLatch.countDown();
         };
 
         try {
-            DeviceConfig.addOnPropertiesChangedListener(sNamespace,
+            DeviceConfig.addOnPropertiesChangedListener(NAMESPACE,
                     ActivityThread.currentApplication().getMainExecutor(), changeListener);
-            DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
+            DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false);
             assertThat(countDownLatch.await(
                     WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
         } catch (InterruptedException e) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index d0ffe7a..8fb879d 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -16,7 +16,6 @@
 
 package com.android.providers.settings;
 
-import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.app.ActivityManager;
 import android.content.IContentProvider;
@@ -195,8 +194,15 @@
                             : "Failed to delete " + key + " from " + namespace);
                     break;
                 case LIST:
-                    for (String line : list(iprovider, namespace)) {
-                        pout.println(line);
+                    if (namespace != null) {
+                        DeviceConfig.Properties properties = DeviceConfig.getProperties(namespace);
+                        for (String name : properties.getKeyset()) {
+                            pout.println(name + "=" + properties.getString(name, null));
+                        }
+                    } else {
+                        for (String line : listAll(iprovider)) {
+                            pout.println(line);
+                        }
                     }
                     break;
                 case RESET:
@@ -251,16 +257,13 @@
             return success;
         }
 
-        private List<String> list(IContentProvider provider, @Nullable String namespace) {
+        private List<String> listAll(IContentProvider provider) {
             final ArrayList<String> lines = new ArrayList<>();
 
             try {
                 Bundle args = new Bundle();
                 args.putInt(Settings.CALL_METHOD_USER_KEY,
                         ActivityManager.getService().getCurrentUser().id);
-                if (namespace != null) {
-                    args.putString(Settings.CALL_METHOD_PREFIX_KEY, namespace);
-                }
                 Bundle b = provider.call(resolveCallingPackage(), Settings.AUTHORITY,
                         Settings.CALL_METHOD_LIST_CONFIG, null, args);
                 if (b != null) {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 16c96e6..a9c466e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -439,10 +439,8 @@
 
             case Settings.CALL_METHOD_LIST_CONFIG: {
                 String prefix = getSettingPrefix(args);
-                Bundle result = new Bundle();
-                result.putSerializable(
-                        Settings.NameValueTable.VALUE, (HashMap) getAllConfigFlags(prefix));
-                return result;
+                return packageValuesForCallResult(getAllConfigFlags(prefix),
+                        isTrackingGeneration(args));
             }
 
             case Settings.CALL_METHOD_LIST_GLOBAL: {
@@ -1076,7 +1074,7 @@
         return false;
     }
 
-    private Map<String, String> getAllConfigFlags(@Nullable String prefix) {
+    private HashMap<String, String> getAllConfigFlags(@Nullable String prefix) {
         if (DEBUG) {
             Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
         }
@@ -1085,12 +1083,11 @@
             // Get the settings.
             SettingsState settingsState = mSettingsRegistry.getSettingsLocked(
                     SETTINGS_TYPE_CONFIG, UserHandle.USER_SYSTEM);
-
             List<String> names = getSettingsNamesLocked(SETTINGS_TYPE_CONFIG,
                     UserHandle.USER_SYSTEM);
 
             final int nameCount = names.size();
-            Map<String, String> flagsToValues = new HashMap<>(names.size());
+            HashMap<String, String> flagsToValues = new HashMap<>(names.size());
 
             for (int i = 0; i < nameCount; i++) {
                 String name = names.get(i);
@@ -2057,8 +2054,7 @@
                 "get/set setting for user", null);
     }
 
-    private Bundle packageValueForCallResult(Setting setting,
-            boolean trackingGeneration) {
+    private Bundle packageValueForCallResult(Setting setting, boolean trackingGeneration) {
         if (!trackingGeneration) {
             if (setting == null || setting.isNull()) {
                 return NULL_SETTING_BUNDLE;
@@ -2073,6 +2069,21 @@
         return result;
     }
 
+    private Bundle packageValuesForCallResult(HashMap<String, String> keyValues,
+            boolean trackingGeneration) {
+        Bundle result = new Bundle();
+        result.putSerializable(Settings.NameValueTable.VALUE, keyValues);
+        if (trackingGeneration) {
+            synchronized (mLock) {
+                mSettingsRegistry.mGenerationRegistry.addGenerationData(result,
+                        mSettingsRegistry.getSettingsLocked(
+                                SETTINGS_TYPE_CONFIG, UserHandle.USER_SYSTEM).mKey);
+            }
+        }
+
+        return result;
+    }
+
     private static int getRequestingUserId(Bundle args) {
         final int callingUserId = UserHandle.getCallingUserId();
         return (args != null) ? args.getInt(Settings.CALL_METHOD_USER_KEY, callingUserId)