Merge "Add the API surface for DeviceConfig."
diff --git a/api/system-current.txt b/api/system-current.txt
index d15e826..5234ea4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4537,6 +4537,18 @@
     field public static final java.lang.String STATE = "state";
   }
 
+  public final class DeviceConfig {
+    method public static void addOnPropertyChangedListener(java.lang.String, java.util.concurrent.Executor, android.provider.DeviceConfig.OnPropertyChangedListener);
+    method public static java.lang.String getProperty(java.lang.String, java.lang.String);
+    method public static void removeOnPropertyChangedListener(android.provider.DeviceConfig.OnPropertyChangedListener);
+    method public static void resetToDefaults(int, java.lang.String);
+    method public static boolean setProperty(java.lang.String, java.lang.String, java.lang.String, boolean);
+  }
+
+  public static abstract interface DeviceConfig.OnPropertyChangedListener {
+    method public abstract void onPropertyChanged(java.lang.String, java.lang.String, java.lang.String);
+  }
+
   public final class DocumentsContract {
     method public static boolean isManageMode(android.net.Uri);
     method public static android.net.Uri setManageMode(android.net.Uri);
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
new file mode 100644
index 0000000..4e207ed
--- /dev/null
+++ b/core/java/android/provider/DeviceConfig.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2018 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 android.provider;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings.ResetMode;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Device level configuration parameters which can be tuned by a separate configuration service.
+ *
+ * @hide
+ */
+@SystemApi
+public final class DeviceConfig {
+    /**
+     * The content:// style URL for the config table.
+     *
+     * @hide
+     */
+    public static final Uri CONTENT_URI = Uri.parse("content://" + Settings.AUTHORITY + "/config");
+
+    private static final Object sLock = new Object();
+    @GuardedBy("sLock")
+    private static Map<OnPropertyChangedListener, Pair<String, Executor>> sListeners =
+            new HashMap<>();
+    @GuardedBy("sLock")
+    private static Map<String, Pair<ContentObserver, Integer>> sNamespaces = new HashMap<>();
+
+    // Should never be invoked
+    private DeviceConfig() {
+    }
+
+    /**
+     * Look up the value of a property for a particular namespace.
+     *
+     * @param namespace The namespace containing the property to look up.
+     * @param name The name of the property to look up.
+     * @return the corresponding value, or null if not present.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static String getProperty(String namespace, String name) {
+        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
+        String compositeName = createCompositeName(namespace, name);
+        return Settings.Config.getString(contentResolver, compositeName);
+    }
+
+    /**
+     * Create a new property with the the provided name and value in the provided namespace, or
+     * update the value of such a property if it already exists. The same name can exist in multiple
+     * namespaces and might have different values in any or all namespaces.
+     * <p>
+     * The method takes an argument indicating whether to make the value the default for this
+     * property.
+     * <p>
+     * All properties stored for a particular scope can be reverted to their default values
+     * by passing the namespace to {@link #resetToDefaults(int, String)}.
+     *
+     * @param namespace The namespace containing the property to create or update.
+     * @param name The name of the property to create or update.
+     * @param value The value to store for the property.
+     * @param makeDefault Whether to make the new value the default one.
+     * @return True if the value was set, false if the storage implementation throws errors.
+     * @see #resetToDefaults(int, String).
+     *
+     * @hide
+     */
+    @SystemApi
+    public static boolean setProperty(
+            String namespace, String name, String value, boolean makeDefault) {
+        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
+        String compositeName = createCompositeName(namespace, name);
+        return Settings.Config.putString(contentResolver, compositeName, value, makeDefault);
+    }
+
+    /**
+     * Reset properties to their default values.
+     * <p>
+     * The method accepts an optional namespace parameter. If provided, only properties set within
+     * that namespace will be reset. Otherwise, all properties will be reset.
+     *
+     * @param resetMode The reset mode to use.
+     * @param namespace Optionally, the specific namespace which resets will be limited to.
+     * @see #setProperty(String, String, String, boolean)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void resetToDefaults(@ResetMode int resetMode, @Nullable String namespace) {
+        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
+        Settings.Config.resetToDefaults(contentResolver, resetMode, namespace);
+    }
+
+    /**
+     * Add a listener for property changes.
+     * <p>
+     * This listener will be called whenever properties in the specified namespace change. Callbacks
+     * will be made on the specified executor. Future calls to this method with the same listener
+     * will replace the old namespace and executor. Remove the listener entirely by calling
+     * {@link #removeOnPropertyChangedListener(OnPropertyChangedListener)}.
+     *
+     * @param namespace The namespace containing properties to monitor.
+     * @param executor The executor which will be used to run callbacks.
+     * @param onPropertyChangedListener The listener to add.
+     * @see #removeOnPropertyChangedListener(OnPropertyChangedListener)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void addOnPropertyChangedListener(
+            @NonNull String namespace,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnPropertyChangedListener onPropertyChangedListener) {
+        synchronized (sLock) {
+            Pair<String, Executor> oldNamespace = sListeners.get(onPropertyChangedListener);
+            if (oldNamespace == null) {
+                // Brand new listener, add it to the list.
+                sListeners.put(onPropertyChangedListener, new Pair<>(namespace, executor));
+                incrementNamespace(namespace);
+            } else if (namespace.equals(oldNamespace.first)) {
+                // Listener is already registered for this namespace, update executor just in case.
+                sListeners.put(onPropertyChangedListener, new Pair<>(namespace, executor));
+            } else {
+                // Update this listener from an old namespace to the new one.
+                decrementNamespace(sListeners.get(onPropertyChangedListener).first);
+                sListeners.put(onPropertyChangedListener, new Pair<>(namespace, executor));
+                incrementNamespace(namespace);
+            }
+        }
+    }
+
+    /**
+     * Remove a listener for property changes. The listener will receive no further notification of
+     * property changes.
+     *
+     * @param onPropertyChangedListener The listener to remove.
+     * @see #addOnPropertyChangedListener(String, Executor, OnPropertyChangedListener)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void removeOnPropertyChangedListener(
+            OnPropertyChangedListener onPropertyChangedListener) {
+        synchronized (sLock) {
+            if (sListeners.containsKey(onPropertyChangedListener)) {
+                decrementNamespace(sListeners.get(onPropertyChangedListener).first);
+                sListeners.remove(onPropertyChangedListener);
+            }
+        }
+    }
+
+    private static String createCompositeName(String namespace, String name) {
+        return namespace + "/" + name;
+    }
+
+    private static Uri createNamespaceUri(String namespace) {
+        return CONTENT_URI.buildUpon().appendPath(namespace).build();
+    }
+
+    /**
+     * Increment the count used to represent the number of listeners subscribed to the given
+     * namespace. If this is the first (i.e. incrementing from 0 to 1) for the given namespace, a
+     * ContentObserver is registered.
+     *
+     * @param namespace The namespace to increment the count for.
+     */
+    @GuardedBy("sLock")
+    private static void incrementNamespace(String namespace) {
+        Pair<ContentObserver, Integer> namespaceCount = sNamespaces.get(namespace);
+        if (namespaceCount != null) {
+            sNamespaces.put(namespace, new Pair<>(namespaceCount.first, namespaceCount.second + 1));
+        } else {
+            // This is a new namespace, register a ContentObserver for it.
+            ContentObserver contentObserver = new ContentObserver(null) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    handleChange(uri);
+                }
+            };
+            ActivityThread.currentApplication().getContentResolver()
+                    .registerContentObserver(createNamespaceUri(namespace), true, contentObserver);
+            sNamespaces.put(namespace, new Pair<>(contentObserver, 1));
+        }
+    }
+
+    /**
+     * Decrement the count used to represent th enumber of listeners subscribed to the given
+     * namespace. If this is the final decrement call (i.e. decrementing from 1 to 0) for the given
+     * namespace, the ContentObserver that had been tracking it will be removed.
+     *
+     * @param namespace The namespace to decrement the count for.
+     */
+    @GuardedBy("sLock")
+    private static void decrementNamespace(String namespace) {
+        Pair<ContentObserver, Integer> namespaceCount = sNamespaces.get(namespace);
+        if (namespaceCount == null) {
+            // This namespace is not registered and does not need to be decremented
+            return;
+        } else if (namespaceCount.second > 1) {
+            sNamespaces.put(namespace, new Pair<>(namespaceCount.first, namespaceCount.second - 1));
+        } else {
+            // Decrementing a namespace to zero means we no longer need its ContentObserver.
+            ActivityThread.currentApplication().getContentResolver()
+                    .unregisterContentObserver(namespaceCount.first);
+            sNamespaces.remove(namespace);
+        }
+    }
+
+    private static void handleChange(Uri uri) {
+        List<String> pathSegments = uri.getPathSegments();
+        // pathSegments(0) is "config"
+        String namespace = pathSegments.get(1);
+        String name = pathSegments.get(2);
+        String value = getProperty(namespace, name);
+        synchronized (sLock) {
+            for (OnPropertyChangedListener listener : sListeners.keySet()) {
+                if (namespace.equals(sListeners.get(listener).first)) {
+                    sListeners.get(listener).second.execute(new Runnable() {
+                        @Override
+                        public void run() {
+                            listener.onPropertyChanged(namespace, name, value);
+                        }
+
+                    });
+                }
+            }
+        }
+    }
+
+    /**
+     * Interface for monitoring to properties.
+     * <p>
+     * Override {@link #onPropertyChanged(String, String, String)} to handle callbacks for changes.
+     */
+    public interface OnPropertyChangedListener {
+        /**
+         * Called when a property has changed.
+         *
+         * @param namespace The namespace containing the property which has changed.
+         * @param name The name of the property which has changed.
+         * @param value The new value of the property which has changed.
+         */
+        void onPropertyChanged(String namespace, String name, String value);
+    }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 7f135fb..299db73 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13972,21 +13972,12 @@
      * @hide
      */
     public static final class Config extends NameValueTable {
-        /**
-         * The content:// style URL for the config table.
-         *
-         * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a
-         *     System API.
-         */
-        private static final Uri CONTENT_URI =
-                Uri.parse("content://" + AUTHORITY + "/config");
-
         private static final ContentProviderHolder sProviderHolder =
-                new ContentProviderHolder(CONTENT_URI);
+                new ContentProviderHolder(DeviceConfig.CONTENT_URI);
 
         // Populated lazily, guarded by class object:
         private static final NameValueCache sNameValueCache = new NameValueCache(
-                CONTENT_URI,
+                DeviceConfig.CONTENT_URI,
                 CALL_METHOD_GET_CONFIG,
                 CALL_METHOD_PUT_CONFIG,
                 sProviderHolder);
@@ -14060,7 +14051,7 @@
                 cp.call(resolver.getPackageName(), sProviderHolder.mUri.getAuthority(),
                         CALL_METHOD_RESET_CONFIG, null, arg);
             } catch (RemoteException e) {
-                Log.w(TAG, "Can't reset to defaults for " + CONTENT_URI, e);
+                Log.w(TAG, "Can't reset to defaults for " + DeviceConfig.CONTENT_URI, e);
             }
         }
     }
diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
new file mode 100644
index 0000000..800b864
--- /dev/null
+++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2018 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 android.provider;
+
+import static android.provider.DeviceConfig.OnPropertyChangedListener;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.fail;
+
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.platform.test.annotations.Presubmit;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Tests that ensure appropriate settings are backed up. */
+@Presubmit
+@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 final Object mLock = new Object();
+
+    @After
+    public void cleanUp() {
+        deleteViaContentProvider(sNamespace, sKey);
+    }
+
+    @Test
+    public void getProperty_empty() {
+        String result = DeviceConfig.getProperty(sNamespace, sKey);
+        assertNull(result);
+    }
+
+    @Test
+    public void setAndGetProperty_sameNamespace() {
+        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
+        String result = DeviceConfig.getProperty(sNamespace, sKey);
+        assertEquals(sValue, result);
+    }
+
+    @Test
+    public void setAndGetProperty_differentNamespace() {
+        String newNamespace = "namespace2";
+        DeviceConfig.setProperty(sNamespace, sKey, sValue, false);
+        String result = DeviceConfig.getProperty(newNamespace, sKey);
+        assertNull(result);
+    }
+
+    @Test
+    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);
+        assertEquals(sValue, result);
+        result = DeviceConfig.getProperty(newNamespace, sKey);
+        assertEquals(newValue, result);
+
+        // clean up
+        deleteViaContentProvider(newNamespace, sKey);
+    }
+
+    @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);
+        assertEquals(newValue, result);
+    }
+
+    @Test
+    public void testListener() {
+        setPropertyAndAssertSuccessfulChange(sNamespace, sKey, sValue);
+    }
+
+    private void setPropertyAndAssertSuccessfulChange(String setNamespace, String setName,
+            String setValue) {
+        final AtomicBoolean success = new AtomicBoolean();
+
+        OnPropertyChangedListener changeListener = new OnPropertyChangedListener() {
+                    @Override
+                    public void onPropertyChanged(String namespace, String name, String value) {
+                        assertEquals(setNamespace, namespace);
+                        assertEquals(setName, name);
+                        assertEquals(setValue, value);
+                        success.set(true);
+
+                        synchronized (mLock) {
+                            mLock.notifyAll();
+                        }
+                    }
+                };
+        Executor executor = ActivityThread.currentApplication().getMainExecutor();
+        DeviceConfig.addOnPropertyChangedListener(setNamespace, executor, changeListener);
+        try {
+            DeviceConfig.setProperty(setNamespace, setName, setValue, false);
+
+            final long startTimeMillis = SystemClock.uptimeMillis();
+            synchronized (mLock) {
+                while (true) {
+                    if (success.get()) {
+                        return;
+                    }
+                    final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+                    if (elapsedTimeMillis >= WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS) {
+                        fail("Could not change setting for "
+                                + WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS + " ms");
+                    }
+                    final long remainingTimeMillis = WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS
+                            - elapsedTimeMillis;
+                    try {
+                        mLock.wait(remainingTimeMillis);
+                    } catch (InterruptedException ie) {
+                        /* ignore */
+                    }
+                }
+            }
+        } finally {
+            DeviceConfig.removeOnPropertyChangedListener(changeListener);
+        }
+    }
+
+    private static boolean deleteViaContentProvider(String namespace, String key) {
+        ContentResolver resolver = InstrumentationRegistry.getContext().getContentResolver();
+        String compositeName = namespace + "/" + key;
+        Bundle result = resolver.call(
+                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
+        assertNotNull(result);
+        return compositeName.equals(result.getString(Settings.NameValueTable.VALUE));
+    }
+
+}
diff --git a/core/tests/coretests/src/android/provider/SettingsProviderTest.java b/core/tests/coretests/src/android/provider/SettingsProviderTest.java
index 04e8802..cb6f0e6 100644
--- a/core/tests/coretests/src/android/provider/SettingsProviderTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsProviderTest.java
@@ -44,12 +44,6 @@
 
 /** Unit test for SettingsProvider. */
 public class SettingsProviderTest extends AndroidTestCase {
-    /**
-     * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System
-     *     API.
-     */
-    private static final Uri CONFIG_CONTENT_URI =
-            Uri.parse("content://" + Settings.AUTHORITY + "/config");
 
     @MediumTest
     public void testNameValueCache() {
@@ -406,27 +400,27 @@
         try {
             // value is empty
             Bundle results =
-                    r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+                    r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertNull(results.get(Settings.NameValueTable.VALUE));
 
             // save value
-            results = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
             assertNull(results);
 
             // value is no longer empty
-            results = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(value, results.get(Settings.NameValueTable.VALUE));
 
             // save new value
             args.putString(Settings.NameValueTable.VALUE, newValue);
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
 
             // new value is returned
-            results = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(newValue, results.get(Settings.NameValueTable.VALUE));
         } finally {
             // clean up
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
         }
     }
 
@@ -440,22 +434,23 @@
 
         try {
             // save value
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
 
             // get value
             Bundle results =
-                    r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+                    r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertEquals(value, results.get(Settings.NameValueTable.VALUE));
 
             // delete value
-            results = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name,
+                    null);
 
             // value is empty now
-            results = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
+            results = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, name, null);
             assertNull(results.get(Settings.NameValueTable.VALUE));
         } finally {
             // clean up
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
         }
     }
 
@@ -473,12 +468,12 @@
 
         try {
             // save both values
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, name, args);
             args.putString(Settings.NameValueTable.VALUE, newValue);
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, newName, args);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, newName, args);
 
             // list all values
-            Bundle result = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG,
+            Bundle result = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG,
                     null, null);
             Map<String, String> keyValueMap =
                     (HashMap) result.getSerializable(Settings.NameValueTable.VALUE);
@@ -488,14 +483,14 @@
 
             // list values for prefix
             args.putString(Settings.CALL_METHOD_PREFIX_KEY, prefix);
-            result = r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG, null, args);
+            result = r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_LIST_CONFIG, null, args);
             keyValueMap = (HashMap) result.getSerializable(Settings.NameValueTable.VALUE);
             assertThat(keyValueMap, aMapWithSize(1));
             assertEquals(value, keyValueMap.get(name));
         } finally {
             // clean up
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
-            r.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, newName, null);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, name, null);
+            r.call(DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, newName, null);
         }
     }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
index 3520918..f2b2719 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java
@@ -20,7 +20,6 @@
 import android.annotation.SystemApi;
 import android.app.ActivityManager;
 import android.content.IContentProvider;
-import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Process;
@@ -28,6 +27,7 @@
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 
 import java.io.FileDescriptor;
@@ -46,13 +46,6 @@
  */
 @SystemApi
 public final class DeviceConfigService extends Binder {
-    /**
-     * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System
-     *     API.
-     */
-    private static final Uri CONFIG_CONTENT_URI =
-            Uri.parse("content://" + Settings.AUTHORITY + "/config");
-
     final SettingsProvider mProvider;
 
     public DeviceConfigService(SettingsProvider provider) {
@@ -191,10 +184,10 @@
             final PrintWriter pout = getOutPrintWriter();
             switch (verb) {
                 case GET:
-                    pout.println(get(iprovider, namespace, key));
+                    pout.println(DeviceConfig.getProperty(namespace, key));
                     break;
                 case PUT:
-                    put(iprovider, namespace, key, value, makeDefault);
+                    DeviceConfig.setProperty(namespace, key, value, makeDefault);
                     break;
                 case DELETE:
                     pout.println(delete(iprovider, namespace, key)
@@ -207,7 +200,7 @@
                     }
                     break;
                 case RESET:
-                    reset(iprovider, resetMode, namespace);
+                    DeviceConfig.resetToDefaults(resetMode, namespace);
                     break;
                 default:
                     perr.println("Unspecified command");
@@ -241,43 +234,6 @@
                     + "flags are reset");
         }
 
-        private String get(IContentProvider provider, String namespace, String key) {
-            String compositeKey = namespace + "/" + key;
-            String result = null;
-            try {
-                Bundle args = new Bundle();
-                args.putInt(Settings.CALL_METHOD_USER_KEY,
-                        ActivityManager.getService().getCurrentUser().id);
-                Bundle b = provider.call(resolveCallingPackage(), Settings.CALL_METHOD_GET_CONFIG,
-                        compositeKey, args);
-                if (b != null) {
-                    result = b.getPairValue();
-                }
-            } catch (RemoteException e) {
-                throw new RuntimeException("Failed in IPC", e);
-            }
-            return result;
-        }
-
-        private void put(IContentProvider provider, String namespace, String key, String value,
-                boolean makeDefault) {
-            String compositeKey = namespace + "/" + key;
-
-            try {
-                Bundle args = new Bundle();
-                args.putString(Settings.NameValueTable.VALUE, value);
-                args.putInt(Settings.CALL_METHOD_USER_KEY,
-                        ActivityManager.getService().getCurrentUser().id);
-                if (makeDefault) {
-                    args.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true);
-                }
-                provider.call(resolveCallingPackage(), Settings.CALL_METHOD_PUT_CONFIG,
-                        compositeKey, args);
-            } catch (RemoteException e) {
-                throw new RuntimeException("Failed in IPC", e);
-            }
-        }
-
         private boolean delete(IContentProvider provider, String namespace, String key) {
             String compositeKey = namespace + "/" + key;
             boolean success;
@@ -322,20 +278,6 @@
             return lines;
         }
 
-        private void reset(IContentProvider provider, int resetMode, @Nullable String namespace) {
-            try {
-                Bundle args = new Bundle();
-                args.putInt(Settings.CALL_METHOD_USER_KEY,
-                        ActivityManager.getService().getCurrentUser().id);
-                args.putInt(Settings.CALL_METHOD_RESET_MODE_KEY, resetMode);
-                args.putString(Settings.CALL_METHOD_PREFIX_KEY, namespace);
-                provider.call(
-                        resolveCallingPackage(), Settings.CALL_METHOD_RESET_CONFIG, null, args);
-            } catch (RemoteException e) {
-                throw new RuntimeException("Failed in IPC", e);
-            }
-        }
-
         private static String resolveCallingPackage() {
             switch (Binder.getCallingUid()) {
                 case Process.ROOT_UID: {
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index b071355..ce529a0 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -63,6 +63,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.UserManagerInternal;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
@@ -195,13 +196,6 @@
     private static final Set<String> OVERLAY_ALLOWED_SYSTEM_INSTANT_APP_SETTINGS = new ArraySet<>();
     private static final Set<String> OVERLAY_ALLOWED_SECURE_INSTANT_APP_SETTINGS = new ArraySet<>();
 
-    /**
-     * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System
-     *     API.
-     */
-    private static final Uri CONFIG_CONTENT_URI =
-            Uri.parse("content://" + Settings.AUTHORITY + "/config");
-
     static {
         for (String name : Resources.getSystem().getStringArray(
                 com.android.internal.R.array.config_allowedGlobalInstantAppSettings)) {
@@ -3148,8 +3142,8 @@
 
         private Uri getNotificationUriFor(int key, String name) {
             if (isConfigSettingsKey(key)) {
-                return (name != null) ? Uri.withAppendedPath(CONFIG_CONTENT_URI, name)
-                        : CONFIG_CONTENT_URI;
+                return (name != null) ? Uri.withAppendedPath(DeviceConfig.CONTENT_URI, name)
+                        : DeviceConfig.CONTENT_URI;
             } else if (isGlobalSettingsKey(key)) {
                 return (name != null) ? Uri.withAppendedPath(Settings.Global.CONTENT_URI, name)
                         : Settings.Global.CONTENT_URI;
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 9d0462e..5587cba 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java
@@ -21,8 +21,8 @@
 import static junit.framework.Assert.assertNull;
 
 import android.content.ContentResolver;
-import android.net.Uri;
 import android.os.Bundle;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.AndroidJUnit4;
@@ -43,12 +43,6 @@
  */
 @RunWith(AndroidJUnit4.class)
 public class DeviceConfigServiceTest {
-    /**
-     * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System
-     *     API.
-     */
-    private static final Uri CONFIG_CONTENT_URI =
-            Uri.parse("content://" + Settings.AUTHORITY + "/config");
     private static final String sNamespace = "namespace1";
     private static final String sKey = "key1";
     private static final String sValue = "value1";
@@ -152,7 +146,7 @@
         // make sValue the default value
         executeShellCommand(
                 "device_config put " + sNamespace + " " + sKey + " " + sValue + " default");
-        // make newValue the current value
+        // make newValue the current value (as set by a trusted package)
         executeShellCommand(
                 "device_config put " + sNamespace + " " + sKey + " " + newValue);
         String result = getFromContentProvider(mContentResolver, sNamespace, sKey);
@@ -161,14 +155,14 @@
         // reset values that were set by untrusted packages
         executeShellCommand("device_config reset untrusted_defaults " + sNamespace);
         result = getFromContentProvider(mContentResolver, sNamespace, sKey);
-        // the default value has been restored
-        assertEquals(sValue, result);
+        // the current value was set by a trusted package, so it's not reset
+        assertEquals(newValue, result);
 
-        // clear values that were set by untrusted packages
+        // reset values that were set by untrusted or trusted packages
         executeShellCommand("device_config reset trusted_defaults " + sNamespace);
         result = getFromContentProvider(mContentResolver, sNamespace, sKey);
-        // even the default value is gone now
-        assertNull(result);
+        // the default value has been restored
+        assertEquals(sValue, result);
     }
 
     private static void executeShellCommand(String command) throws IOException {
@@ -190,14 +184,15 @@
         if (makeDefault) {
             args.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true);
         }
-        resolver.call(CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, compositeName, args);
+        resolver.call(
+                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, compositeName, args);
     }
 
     private static String getFromContentProvider(ContentResolver resolver, String namespace,
             String key) {
         String compositeName = namespace + "/" + key;
         Bundle result = resolver.call(
-                CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, compositeName, null);
+                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, compositeName, null);
         assertNotNull(result);
         return result.getString(Settings.NameValueTable.VALUE);
     }
@@ -206,7 +201,7 @@
             String key) {
         String compositeName = namespace + "/" + key;
         Bundle result = resolver.call(
-                CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
+                DeviceConfig.CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null);
         assertNotNull(result);
         return compositeName.equals(result.getString(Settings.NameValueTable.VALUE));
     }