diff --git a/api/system-current.txt b/api/system-current.txt index f3cdac3d20ee5..bacbf014182e6 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6230,6 +6230,7 @@ package android.provider { 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); method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static void resetToDefaults(int, @Nullable String); + method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperties(@NonNull android.provider.DeviceConfig.Properties); method @RequiresPermission(android.Manifest.permission.WRITE_DEVICE_CONFIG) public static boolean setProperty(@NonNull String, @NonNull String, @Nullable String, boolean); field public static final String NAMESPACE_ACTIVITY_MANAGER = "activity_manager"; field public static final String NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT = "activity_manager_native_boot"; diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 1d7e33806e0c4..abf34ca2d21ad 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -391,8 +391,9 @@ public final class DeviceConfig { * 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. + *

+ * Each call to {@link #setProperties(Properties)} is also atomic and ensures that either none + * or all of the change is picked up here, but never only part of it. * * @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 @@ -559,6 +560,27 @@ public final class DeviceConfig { return Settings.Config.putString(contentResolver, namespace, name, value, makeDefault); } + /** + * Set all of the properties for a specific namespace. Pre-existing properties will be updated + * and new properties will be added if necessary. Any pre-existing properties for the specific + * namespace which are not part of the provided {@link Properties} object will be deleted from + * the namespace. These changes are all applied atomically, such that no calls to read or reset + * these properties can happen in the middle of this update. + *

+ * Each call to {@link #getProperties(String, String...)} is also atomic and ensures that either + * none or all of this update is picked up, but never only part of it. + * + * @param properties the complete set of properties to set for a specific namespace. + * @hide + */ + @SystemApi + @RequiresPermission(WRITE_DEVICE_CONFIG) + public static boolean setProperties(@NonNull Properties properties) { + ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver(); + return Settings.Config.setStrings(contentResolver, properties.getNamespace(), + properties.mMap); + } + /** * Reset properties to their default values. *

@@ -703,23 +725,26 @@ public final class DeviceConfig { List pathSegments = uri.getPathSegments(); // pathSegments(0) is "config" final String namespace = pathSegments.get(1); - final String name = pathSegments.get(2); - final String value; + Map propertyMap = new ArrayMap<>(); try { - value = getProperty(namespace, name); + Properties allProperties = getProperties(namespace); + for (int i = 2; i < pathSegments.size(); ++i) { + String key = pathSegments.get(i); + propertyMap.put(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); + synchronized (sLock) { for (int i = 0; i < sListeners.size(); i++) { if (namespace.equals(sListeners.valueAt(i).first)) { final OnPropertiesChangedListener listener = sListeners.keyAt(i); sListeners.valueAt(i).second.execute(() -> { - Map propertyMap = new ArrayMap<>(1); - propertyMap.put(name, value); - listener.onPropertiesChanged(new Properties(namespace, propertyMap)); + listener.onPropertiesChanged(properties); }); } } @@ -741,7 +766,11 @@ public final class DeviceConfig { } /** - * Interface for monitoring changes to properties. + * Interface for monitoring changes to properties. Implementations will receive callbacks when + * properties change, including a {@link Properties} object which contains a single namespace + * and all of the properties which changed for that namespace. This includes properties which + * were added, updated, or deleted. This is not necessarily a complete list of all properties + * belonging to the namespace, as properties which don't change are omitted. *

* Override {@link #onPropertiesChanged(Properties)} to handle callbacks for changes. * @@ -751,10 +780,13 @@ public final class DeviceConfig { @TestApi public interface OnPropertiesChangedListener { /** - * Called when one or more properties have changed. + * Called when one or more properties have changed, providing a Properties object with all + * of the changed properties. This object will contain only properties which have changed, + * not the complete set of all properties belonging to the namespace. * * @param properties Contains the complete collection of properties which have changed for a - * single namespace. + * single namespace. This includes only those which were added, updated, + * or deleted. */ void onPropertiesChanged(@NonNull Properties properties); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 3ac7deb0db082..cce0d4fb0ca66 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1960,6 +1960,11 @@ public final class Settings { */ public static final String CALL_METHOD_PREFIX_KEY = "_prefix"; + /** + * @hide - String argument extra to the fast-path call()-based requests + */ + public static final String CALL_METHOD_FLAGS_KEY = "_flags"; + /** @hide - Private call() method to write to 'system' table */ public static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; @@ -1972,6 +1977,9 @@ public final class Settings { /** @hide - Private call() method to write to 'configuration' table */ public static final String CALL_METHOD_PUT_CONFIG = "PUT_config"; + /** @hide - Private call() method to write to and delete from the 'configuration' table */ + public static final String CALL_METHOD_SET_ALL_CONFIG = "SET_ALL_config"; + /** @hide - Private call() method to delete from the 'system' table */ public static final String CALL_METHOD_DELETE_SYSTEM = "DELETE_system"; @@ -2304,21 +2312,23 @@ public final class Settings { private final String mCallGetCommand; private final String mCallSetCommand; private final String mCallListCommand; + private final String mCallSetAllCommand; @GuardedBy("this") private GenerationTracker mGenerationTracker; public NameValueCache(Uri uri, String getCommand, String setCommand, ContentProviderHolder providerHolder) { - this(uri, getCommand, setCommand, null, providerHolder); + this(uri, getCommand, setCommand, null, null, providerHolder); } NameValueCache(Uri uri, String getCommand, String setCommand, String listCommand, - ContentProviderHolder providerHolder) { + String setAllCommand, ContentProviderHolder providerHolder) { mUri = uri; mCallGetCommand = getCommand; mCallSetCommand = setCommand; mCallListCommand = listCommand; + mCallSetAllCommand = setAllCommand; mProviderHolder = providerHolder; } @@ -2344,6 +2354,26 @@ public final class Settings { return true; } + public boolean setStringsForPrefix(ContentResolver cr, String prefix, + HashMap keyValues) { + if (mCallSetAllCommand == null) { + // This NameValueCache does not support atomically setting multiple flags + return false; + } + try { + Bundle args = new Bundle(); + args.putString(CALL_METHOD_PREFIX_KEY, prefix); + args.putSerializable(CALL_METHOD_FLAGS_KEY, keyValues); + IContentProvider cp = mProviderHolder.getProvider(cr); + cp.call(cr.getPackageName(), cr.getFeatureId(), mProviderHolder.mUri.getAuthority(), + mCallSetAllCommand, null, args); + } catch (RemoteException e) { + // Not supported by the remote side + return false; + } + return true; + } + @UnsupportedAppUsage public String getStringForUser(ContentResolver cr, String name, final int userHandle) { final boolean isSelf = (userHandle == UserHandle.myUserId()); @@ -13718,6 +13748,7 @@ public final class Settings { CALL_METHOD_GET_CONFIG, CALL_METHOD_PUT_CONFIG, CALL_METHOD_LIST_CONFIG, + CALL_METHOD_SET_ALL_CONFIG, sProviderHolder); /** @@ -13791,6 +13822,29 @@ public final class Settings { value, null, makeDefault, resolver.getUserId()); } + /** + * Clear all name/value pairs for the provided namespace and save new name/value pairs in + * their place. + * + * @param resolver to access the database with. + * @param namespace to which the names should be set. + * @param keyValues map of key names (without the prefix) to values. + * @return + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_DEVICE_CONFIG) + static boolean setStrings(@NonNull ContentResolver resolver, @NonNull String namespace, + @NonNull Map keyValues) { + HashMap compositeKeyValueMap = new HashMap<>(keyValues.keySet().size()); + for (Map.Entry entry : keyValues.entrySet()) { + compositeKeyValueMap.put( + createCompositeName(namespace, entry.getKey()), entry.getValue()); + } + return sNameValueCache.setStringsForPrefix(resolver, createPrefix(namespace), + compositeKeyValueMap); + } + /** * Reset the values to their defaults. *

diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java index 0c83390f9f1e0..8c1c3b5693030 100644 --- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java +++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java @@ -33,6 +33,9 @@ import org.junit.Assert; 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) @@ -484,12 +487,74 @@ public class DeviceConfigTest { assertThat(properties.getString(KEY3, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE); } - // TODO(mpape): resolve b/142727848 and re-enable this test + @Test + public void setProperties() { + Map keyValues = new HashMap<>(); + keyValues.put(KEY, VALUE); + keyValues.put(KEY2, VALUE2); + + DeviceConfig.setProperties(new Properties(NAMESPACE, keyValues)); + 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 newKeyValues = new HashMap<>(); + newKeyValues.put(KEY, VALUE2); + newKeyValues.put(KEY3, VALUE3); + + DeviceConfig.setProperties(new Properties(NAMESPACE, newKeyValues)); + properties = DeviceConfig.getProperties(NAMESPACE); + assertThat(properties.getKeyset()).containsExactly(KEY, KEY3); + assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(VALUE2); + assertThat(properties.getString(KEY3, DEFAULT_VALUE)).isEqualTo(VALUE3); + + assertThat(properties.getKeyset()).doesNotContain(KEY2); + assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE); + } + + @Test + public void setProperties_multipleNamespaces() { + Map keyValues = new HashMap<>(); + keyValues.put(KEY, VALUE); + keyValues.put(KEY2, VALUE2); + + Map 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 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); + + assertThat(properties.getKeyset()).doesNotContain(KEY3); + assertThat(properties.getString(KEY3, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE); + + properties = DeviceConfig.getProperties(namespace2); + assertThat(properties.getKeyset()).containsExactly(KEY2, KEY3); + assertThat(properties.getString(KEY2, DEFAULT_VALUE)).isEqualTo(VALUE); + assertThat(properties.getString(KEY3, DEFAULT_VALUE)).isEqualTo(VALUE2); + + assertThat(properties.getKeyset()).doesNotContain(KEY); + assertThat(properties.getString(KEY, DEFAULT_VALUE)).isEqualTo(DEFAULT_VALUE); + + // clean up + deleteViaContentProvider(namespace2, KEY); + deleteViaContentProvider(namespace2, KEY2); + deleteViaContentProvider(namespace2, KEY3); + } + + // TODO(mpape): resolve b/142727848 and re-enable listener tests // @Test -// public void testOnPropertiesChangedListener() throws InterruptedException { +// public void onPropertiesChangedListener_setPropertyCallback() throws InterruptedException { // final CountDownLatch countDownLatch = new CountDownLatch(1); // -// OnPropertiesChangedListener changeListener = (properties) -> { +// DeviceConfig.OnPropertiesChangedListener changeListener = (properties) -> { // assertThat(properties.getNamespace()).isEqualTo(NAMESPACE); // assertThat(properties.getKeyset()).contains(KEY); // assertThat(properties.getString(KEY, "default_value")).isEqualTo(VALUE); @@ -507,6 +572,42 @@ public class DeviceConfigTest { // } finally { // DeviceConfig.removeOnPropertiesChangedListener(changeListener); // } +// } +// +// @Test +// public void onPropertiesChangedListener_setPropertiesCallback() throws InterruptedException { +// final CountDownLatch countDownLatch = new CountDownLatch(1); +// DeviceConfig.setProperty(NAMESPACE, KEY, VALUE, false); +// DeviceConfig.setProperty(NAMESPACE, KEY2, VALUE2, false); +// +// Map keyValues = new HashMap<>(2); +// keyValues.put(KEY, VALUE2); +// keyValues.put(KEY3, VALUE3); +// Properties setProperties = new Properties(NAMESPACE, keyValues); +// +// DeviceConfig.OnPropertiesChangedListener changeListener = (properties) -> { +// assertThat(properties.getNamespace()).isEqualTo(NAMESPACE); +// assertThat(properties.getKeyset()).containsExactly(KEY, KEY2, KEY3); +// // KEY updated from VALUE to VALUE2 +// assertThat(properties.getString(KEY, "default_value")).isEqualTo(VALUE2); +// // KEY2 deleted (returns default_value) +// assertThat(properties.getString(KEY2, "default_value")).isEqualTo("default_value"); +// //KEY3 added with VALUE3 +// assertThat(properties.getString(KEY3, "default_value")).isEqualTo(VALUE3); +// countDownLatch.countDown(); +// }; +// +// try { +// DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, +// ActivityThread.currentApplication().getMainExecutor(), changeListener); +// DeviceConfig.setProperties(setProperties); +// assertThat(countDownLatch.await( +// WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue(); +// } catch (InterruptedException e) { +// Assert.fail(e.getMessage()); +// } finally { +// DeviceConfig.removeOnPropertiesChangedListener(changeListener); +// } // } private static boolean deleteViaContentProvider(String namespace, String key) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index f5d1ccfe378b0..0959de9238b55 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -101,6 +101,7 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -383,6 +384,13 @@ public class SettingsProvider extends ContentProvider { break; } + case Settings.CALL_METHOD_SET_ALL_CONFIG: { + String prefix = getSettingPrefix(args); + Map flags = getSettingFlags(args); + setAllConfigSettings(prefix, flags); + break; + } + case Settings.CALL_METHOD_RESET_CONFIG: { final int mode = getResetModeEnforcingPermission(args); String prefix = getSettingPrefix(args); @@ -1030,6 +1038,19 @@ public class SettingsProvider extends ContentProvider { MUTATION_OPERATION_INSERT, 0); } + private boolean setAllConfigSettings(String prefix, Map keyValues) { + if (DEBUG) { + Slog.v(LOG_TAG, "setAllConfigSettings for prefix: " + prefix); + } + + enforceWritePermission(Manifest.permission.WRITE_DEVICE_CONFIG); + + synchronized (mLock) { + return mSettingsRegistry.setSettingsLocked(SETTINGS_TYPE_CONFIG, UserHandle.USER_SYSTEM, + prefix, keyValues, resolveCallingPackage()); + } + } + private boolean deleteConfigSetting(String name) { if (DEBUG) { Slog.v(LOG_TAG, "deleteConfigSetting(" + name + ")"); @@ -2117,6 +2138,11 @@ public class SettingsProvider extends ContentProvider { return (args != null) ? args.getString(Settings.CALL_METHOD_PREFIX_KEY) : null; } + private static Map getSettingFlags(Bundle args) { + return (args != null) ? (HashMap) args.getSerializable(Settings.CALL_METHOD_FLAGS_KEY) + : Collections.emptyMap(); + } + private static boolean getSettingMakeDefault(Bundle args) { return (args != null) && args.getBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY); } @@ -2485,7 +2511,7 @@ public class SettingsProvider extends ContentProvider { final int key = makeKey(type, userId); SettingsState settingsState = peekSettingsStateLocked(key); if (settingsState == null) { - return new ArrayList(); + return new ArrayList<>(); } return settingsState.getSettingNamesLocked(); } @@ -2645,6 +2671,22 @@ public class SettingsProvider extends ContentProvider { return success; } + public boolean setSettingsLocked(int type, int userId, String prefix, + Map keyValues, String packageName) { + final int key = makeKey(type, userId); + + SettingsState settingsState = peekSettingsStateLocked(key); + if (settingsState != null) { + List changedSettings = + settingsState.setSettingsLocked(prefix, keyValues, packageName); + if (!changedSettings.isEmpty()) { + notifyForConfigSettingsChangeLocked(key, prefix, changedSettings); + } + } + + return settingsState != null; + } + public boolean deleteSettingLocked(int type, int userId, String name, boolean forceNotify, Set criticalSettings) { final int key = makeKey(type, userId); @@ -3043,6 +3085,28 @@ public class SettingsProvider extends ContentProvider { mHandler.obtainMessage(MyHandler.MSG_NOTIFY_DATA_CHANGED).sendToTarget(); } + private void notifyForConfigSettingsChangeLocked(int key, String prefix, + List changedSettings) { + + // Increment the generation first, so observers always see the new value + mGenerationRegistry.incrementGeneration(key); + + StringBuilder stringBuilder = new StringBuilder(prefix); + for (int i = 0; i < changedSettings.size(); ++i) { + stringBuilder.append(changedSettings.get(i).split("/")[1]).append("/"); + } + + final long token = Binder.clearCallingIdentity(); + try { + notifySettingChangeForRunningUsers(key, stringBuilder.toString()); + } finally { + Binder.restoreCallingIdentity(token); + } + + // Always notify that our data changed + mHandler.obtainMessage(MyHandler.MSG_NOTIFY_DATA_CHANGED).sendToTarget(); + } + private void maybeNotifyProfiles(int type, int userId, Uri uri, String name, Collection keysCloned) { if (keysCloned.contains(name)) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index de6a3a8840a47..4731e6894baf0 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -64,6 +64,7 @@ import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -415,6 +416,57 @@ final class SettingsState { return true; } + // The settings provider must hold its lock when calling here. + // Returns the list of keys which changed (added, updated, or deleted). + @GuardedBy("mLock") + public List setSettingsLocked(String prefix, Map keyValues, + String packageName) { + List changedKeys = new ArrayList<>(); + // Delete old keys with the prefix that are not part of the new set. + for (int i = 0; i < mSettings.keySet().size(); ++i) { + String key = mSettings.keyAt(i); + if (key.startsWith(prefix) && !keyValues.containsKey(key)) { + Setting oldState = mSettings.remove(key); + + StatsLog.write(StatsLog.SETTING_CHANGED, key, /* value= */ "", /* newValue= */ "", + oldState.value, /* tag */ "", false, getUserIdFromKey(mKey), + StatsLog.SETTING_CHANGED__REASON__DELETED); + addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState); + changedKeys.add(key); // key was removed + } + } + + // Update/add new keys + for (String key : keyValues.keySet()) { + String value = keyValues.get(key); + String oldValue = null; + Setting state = mSettings.get(key); + if (state == null) { + state = new Setting(key, value, false, packageName, null); + mSettings.put(key, state); + changedKeys.add(key); // key was added + } else if (state.value != value) { + oldValue = state.value; + state.update(value, false, packageName, null, true); + changedKeys.add(key); // key was updated + } else { + // this key/value already exists, no change and no logging necessary + continue; + } + + StatsLog.write(StatsLog.SETTING_CHANGED, key, value, state.value, oldValue, + /* tag */ null, /* make default */ false, + getUserIdFromKey(mKey), StatsLog.SETTING_CHANGED__REASON__UPDATED); + addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, state); + } + + if (!changedKeys.isEmpty()) { + scheduleWriteIfNeededLocked(); + } + + return changedKeys; + } + // The settings provider must hold its lock when calling here. public void persistSyncLocked() { mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);