Add a DeviceConfig.Properties.Builder class.

Test: atest FrameworksCoreTests:DeviceConfigTest
      atest SettingsProviderTest:DeviceConfigServiceTest
Bug: 136135417

Change-Id: I2e1b2d467ba0b0590ef216eb10d42f73ba1ccda0
diff --git a/api/system-current.txt b/api/system-current.txt
index 781364e..5279857 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -7110,6 +7110,16 @@
     method @Nullable public String getString(@NonNull String, @Nullable String);
   }
 
+  public static final class DeviceConfig.Properties.Builder {
+    ctor public DeviceConfig.Properties.Builder(@NonNull String);
+    method @NonNull public android.provider.DeviceConfig.Properties build();
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setBoolean(@NonNull String, boolean);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setFloat(@NonNull String, float);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setInt(@NonNull String, int);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setLong(@NonNull String, long);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setString(@NonNull String, @Nullable String);
+  }
+
   public final class DocumentsContract {
     method public static boolean isManageMode(@NonNull android.net.Uri);
     method @NonNull public static android.net.Uri setManageMode(@NonNull android.net.Uri);
diff --git a/api/test-current.txt b/api/test-current.txt
index efb8538..00f66f1 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -2452,6 +2452,16 @@
     method @Nullable public String getString(@NonNull String, @Nullable String);
   }
 
+  public static final class DeviceConfig.Properties.Builder {
+    ctor public DeviceConfig.Properties.Builder(@NonNull String);
+    method @NonNull public android.provider.DeviceConfig.Properties build();
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setBoolean(@NonNull String, boolean);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setFloat(@NonNull String, float);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setInt(@NonNull String, int);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setLong(@NonNull String, long);
+    method @NonNull public android.provider.DeviceConfig.Properties.Builder setString(@NonNull String, @Nullable String);
+  }
+
   public final class MediaStore {
     method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static void deleteContributedMedia(android.content.Context, String, android.os.UserHandle) throws java.io.IOException;
     method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static long getContributedMediaSize(android.content.Context, String, android.os.UserHandle) throws java.io.IOException;
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index f8825ed..ef22d70 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -40,6 +40,7 @@
 import com.android.internal.util.Preconditions;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -734,19 +735,19 @@
         List<String> pathSegments = uri.getPathSegments();
         // pathSegments(0) is "config"
         final String namespace = pathSegments.get(1);
-        Map<String, String> propertyMap = new ArrayMap<>();
+        Properties.Builder propBuilder = new Properties.Builder(namespace);
         try {
             Properties allProperties = getProperties(namespace);
             for (int i = 2; i < pathSegments.size(); ++i) {
                 String key = pathSegments.get(i);
-                propertyMap.put(key, allProperties.getString(key, null));
+                propBuilder.setString(key, allProperties.getString(key, null));
             }
         } catch (SecurityException e) {
             // Silently failing to not crash binder or listener threads.
             Log.e(TAG, "OnPropertyChangedListener update failed: permission violation.");
             return;
         }
-        Properties properties = new Properties(namespace, propertyMap);
+        Properties properties = propBuilder.build();
 
         synchronized (sLock) {
             for (int i = 0; i < sListeners.size(); i++) {
@@ -819,6 +820,7 @@
     public static class Properties {
         private final String mNamespace;
         private final HashMap<String, String> mMap;
+        private Set<String> mKeyset;
 
         /**
          * Create a mapping of properties to values and the namespace they belong to.
@@ -849,7 +851,10 @@
          */
         @NonNull
         public Set<String> getKeyset() {
-            return mMap.keySet();
+            if (mKeyset == null) {
+                mKeyset = Collections.unmodifiableSet(mMap.keySet());
+            }
+            return mKeyset;
         }
 
         /**
@@ -944,6 +949,93 @@
                 return defaultValue;
             }
         }
+
+        /**
+         * Builder class for the construction of {@link Properties} objects.
+         */
+        public static final class Builder {
+            @NonNull
+            private final String mNamespace;
+            @NonNull
+            private final Map<String, String> mKeyValues = new HashMap<>();
+
+            /**
+             * Create a new Builders for the specified namespace.
+             * @param namespace non null namespace.
+             */
+            public Builder(@NonNull String namespace) {
+                mNamespace = namespace;
+            }
+
+            /**
+             * Add a new property with the specified key and value.
+             * @param name non null name of the property.
+             * @param value nullable string value of the property.
+             * @return this Builder object
+             */
+            @NonNull
+            public Builder setString(@NonNull String name, @Nullable String value) {
+                mKeyValues.put(name, value);
+                return this;
+            }
+
+            /**
+             * Add a new property with the specified key and value.
+             * @param name non null name of the property.
+             * @param value nullable string value of the property.
+             * @return this Builder object
+             */
+            @NonNull
+            public Builder setBoolean(@NonNull String name, boolean value) {
+                mKeyValues.put(name, Boolean.toString(value));
+                return this;
+            }
+
+            /**
+             * Add a new property with the specified key and value.
+             * @param name non null name of the property.
+             * @param value int value of the property.
+             * @return this Builder object
+             */
+            @NonNull
+            public Builder setInt(@NonNull String name, int value) {
+                mKeyValues.put(name, Integer.toString(value));
+                return this;
+            }
+
+            /**
+             * Add a new property with the specified key and value.
+             * @param name non null name of the property.
+             * @param value long value of the property.
+             * @return this Builder object
+             */
+            @NonNull
+            public Builder setLong(@NonNull String name, long value) {
+                mKeyValues.put(name, Long.toString(value));
+                return this;
+            }
+
+            /**
+             * Add a new property with the specified key and value.
+             * @param name non null name of the property.
+             * @param value float value of the property.
+             * @return this Builder object
+             */
+            @NonNull
+            public Builder setFloat(@NonNull String name, float value) {
+                mKeyValues.put(name, Float.toString(value));
+                return this;
+            }
+
+            /**
+             * Create a new {@link Properties} object.
+             * @return non null Properties.
+             */
+            @NonNull
+            public Properties build() {
+                return new Properties(mNamespace, mKeyValues);
+            }
+        }
     }
 
 }
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
index 2f91a09..ae835e4 100644
--- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -33,9 +33,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.HashMap;
-import java.util.Map;
-
 /** Tests that ensure appropriate settings are backed up. */
 @Presubmit
 @RunWith(AndroidJUnit4.class)
@@ -489,21 +486,19 @@
 
     @Test
     public void setProperties() throws DeviceConfig.BadConfigException {
-        Map<String, String> keyValues = new HashMap<>();
-        keyValues.put(KEY, VALUE);
-        keyValues.put(KEY2, VALUE2);
+        Properties properties = new Properties.Builder(NAMESPACE).setString(KEY, VALUE)
+                .setString(KEY2, VALUE2).build();
 
-        DeviceConfig.setProperties(new Properties(NAMESPACE, keyValues));
-        Properties properties = DeviceConfig.getProperties(NAMESPACE);
+        DeviceConfig.setProperties(properties);
+        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);
 
-        Map<String, String> newKeyValues = new HashMap<>();
-        newKeyValues.put(KEY, VALUE2);
-        newKeyValues.put(KEY3, VALUE3);
+        properties = new Properties.Builder(NAMESPACE).setString(KEY, VALUE2)
+                .setString(KEY3, VALUE3).build();
 
-        DeviceConfig.setProperties(new Properties(NAMESPACE, newKeyValues));
+        DeviceConfig.setProperties(properties);
         properties = DeviceConfig.getProperties(NAMESPACE);
         assertThat(properties.getKeyset()).containsExactly(KEY, KEY3);
         assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE2);
@@ -515,17 +510,14 @@
 
     @Test
     public void setProperties_multipleNamespaces() throws DeviceConfig.BadConfigException {
-        Map<String, String> keyValues = new HashMap<>();
-        keyValues.put(KEY, VALUE);
-        keyValues.put(KEY2, VALUE2);
-
-        Map<String, String> keyValues2 = new HashMap<>();
-        keyValues2.put(KEY2, VALUE);
-        keyValues2.put(KEY3, VALUE2);
-
         final String namespace2 = "namespace2";
-        DeviceConfig.setProperties(new Properties(NAMESPACE, keyValues));
-        DeviceConfig.setProperties(new Properties(namespace2, keyValues2));
+        Properties properties1 = new Properties.Builder(NAMESPACE).setString(KEY, VALUE)
+                .setString(KEY2, VALUE2).build();
+        Properties properties2 = new Properties.Builder(namespace2).setString(KEY2, VALUE)
+                .setString(KEY3, VALUE2).build();
+
+        DeviceConfig.setProperties(properties1);
+        DeviceConfig.setProperties(properties2);
 
         Properties properties = DeviceConfig.getProperties(NAMESPACE);
         assertThat(properties.getKeyset()).containsExactly(KEY, KEY2);
@@ -549,6 +541,26 @@
         deleteViaContentProvider(namespace2, KEY3);
     }
 
+    @Test
+    public void propertiesBuilder() {
+        boolean booleanValue = true;
+        int intValue = 123;
+        float floatValue = 4.56f;
+        long longValue = -789L;
+        String key4 = "key4";
+        String key5 = "key5";
+
+        Properties properties = new Properties.Builder(NAMESPACE).setString(KEY, VALUE)
+                .setBoolean(KEY2, booleanValue).setInt(KEY3, intValue).setLong(key4, longValue)
+                .setFloat(key5, floatValue).build();
+        assertThat(properties.getNamespace()).isEqualTo(NAMESPACE);
+        assertThat(properties.getString(KEY, "defaultValue")).isEqualTo(VALUE);
+        assertThat(properties.getBoolean(KEY2, false)).isEqualTo(booleanValue);
+        assertThat(properties.getInt(KEY3, 0)).isEqualTo(intValue);
+        assertThat(properties.getLong("key4", 0L)).isEqualTo(longValue);
+        assertThat(properties.getFloat("key5", 0f)).isEqualTo(floatValue);
+    }
+
     // TODO(mpape): resolve b/142727848 and re-enable listener tests
 //    @Test
 //    public void onPropertiesChangedListener_setPropertyCallback() throws InterruptedException {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java
index 426aba0..260ff2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java
@@ -69,8 +69,11 @@
         }
 
         for (Pair<Executor, OnPropertiesChangedListener> listener : mListeners) {
-            listener.first.execute(() -> listener.second.onPropertiesChanged(
-                    new Properties(namespace, mProperties.get(namespace))));
+            Properties.Builder propBuilder = new Properties.Builder(namespace);
+            for (String key : mProperties.get(namespace).keySet()) {
+                propBuilder.setString(key, mProperties.get(namespace).get(key));
+            }
+            listener.first.execute(() -> listener.second.onPropertiesChanged(propBuilder.build()));
         }
         return true;
     }
@@ -88,10 +91,12 @@
 
     private Properties propsForNamespaceAndName(String namespace, String name) {
         if (mProperties.containsKey(namespace) && mProperties.get(namespace).containsKey(name)) {
-            return new Properties(namespace, mProperties.get(namespace));
+            return new Properties.Builder(namespace)
+                    .setString(name, mProperties.get(namespace).get(name)).build();
         }
         if (mDefaultProperties.containsKey(namespace)) {
-            return new Properties(namespace, mDefaultProperties.get(namespace));
+            return new Properties.Builder(namespace)
+                    .setString(name, mDefaultProperties.get(namespace).get(name)).build();
         }
 
         return null;