diff --git a/api/system-current.txt b/api/system-current.txt index a4fb46d6f8707..232144fd3d162 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5845,6 +5845,7 @@ package android.provider { } public final class DeviceConfig { + method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener); method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static void addOnPropertyChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertyChangedListener); method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static boolean getBoolean(String, String, boolean); method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static float getFloat(String, String, float); @@ -5852,7 +5853,8 @@ package android.provider { method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static long getLong(String, String, long); method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getProperty(String, String); method @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG) public static String getString(String, String, String); - method public static void removeOnPropertyChangedListener(android.provider.DeviceConfig.OnPropertyChangedListener); + method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener); + method public static void removeOnPropertyChangedListener(@NonNull android.provider.DeviceConfig.OnPropertyChangedListener); 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 setProperty(String, String, String, boolean); field public static final String NAMESPACE_ACTIVITY_MANAGER = "activity_manager"; @@ -5887,8 +5889,11 @@ package android.provider { field public static final String NAMESPACE = "intelligence_attention"; } + public static interface DeviceConfig.OnPropertiesChangedListener { + method public void onPropertiesChanged(@NonNull android.provider.DeviceConfig.Properties); + } + public static interface DeviceConfig.OnPropertyChangedListener { - method public default void onPropertiesChanged(@NonNull android.provider.DeviceConfig.Properties); method public void onPropertyChanged(String, String, String); } diff --git a/api/test-current.txt b/api/test-current.txt index 1856b45afe334..97f5ffe6c849a 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2002,6 +2002,7 @@ package android.provider { } public final class DeviceConfig { + method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static void addOnPropertiesChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertiesChangedListener); method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static void addOnPropertyChangedListener(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.provider.DeviceConfig.OnPropertyChangedListener); method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static boolean getBoolean(String, String, boolean); method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static float getFloat(String, String, float); @@ -2009,15 +2010,19 @@ package android.provider { method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static long getLong(String, String, long); method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static String getProperty(String, String); method @RequiresPermission("android.permission.READ_DEVICE_CONFIG") public static String getString(String, String, String); - method public static void removeOnPropertyChangedListener(android.provider.DeviceConfig.OnPropertyChangedListener); + method public static void removeOnPropertiesChangedListener(@NonNull android.provider.DeviceConfig.OnPropertiesChangedListener); + method public static void removeOnPropertyChangedListener(@NonNull android.provider.DeviceConfig.OnPropertyChangedListener); method @RequiresPermission("android.permission.WRITE_DEVICE_CONFIG") public static void resetToDefaults(int, @Nullable String); method @RequiresPermission("android.permission.WRITE_DEVICE_CONFIG") public static boolean setProperty(String, String, String, boolean); field public static final String NAMESPACE_AUTOFILL = "autofill"; field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture"; } + public static interface DeviceConfig.OnPropertiesChangedListener { + method public void onPropertiesChanged(@NonNull android.provider.DeviceConfig.Properties); + } + public static interface DeviceConfig.OnPropertyChangedListener { - method public default void onPropertiesChanged(@NonNull android.provider.DeviceConfig.Properties); method public void onPropertyChanged(String, String, String); } diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 0ac7c37ee293f..e4593e55a2c64 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -30,6 +30,7 @@ import android.content.ContentResolver; import android.database.ContentObserver; import android.net.Uri; import android.provider.Settings.ResetMode; +import android.util.ArrayMap; import android.util.Pair; import com.android.internal.annotations.GuardedBy; @@ -398,8 +399,11 @@ public final class DeviceConfig { private static final Object sLock = new Object(); @GuardedBy("sLock") - private static Map> sListeners = - new HashMap<>(); + private static ArrayMap> sSingleListeners = + new ArrayMap<>(); + @GuardedBy("sLock") + private static ArrayMap> sListeners = + new ArrayMap<>(); @GuardedBy("sLock") private static Map> sNamespaces = new HashMap<>(); @@ -597,20 +601,58 @@ public final class DeviceConfig { @NonNull String namespace, @NonNull @CallbackExecutor Executor executor, @NonNull OnPropertyChangedListener onPropertyChangedListener) { - // TODO enforce READ_DEVICE_CONFIG permission synchronized (sLock) { - Pair oldNamespace = sListeners.get(onPropertyChangedListener); + Pair oldNamespace = sSingleListeners.get(onPropertyChangedListener); if (oldNamespace == null) { // Brand new listener, add it to the list. - sListeners.put(onPropertyChangedListener, new Pair<>(namespace, executor)); + sSingleListeners.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)); + sSingleListeners.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)); + decrementNamespace(sSingleListeners.get(onPropertyChangedListener).first); + sSingleListeners.put(onPropertyChangedListener, new Pair<>(namespace, executor)); + incrementNamespace(namespace); + } + } + } + + /** + * Add a listener for property changes. + *

+ * 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 #removeOnPropertiesChangedListener(OnPropertiesChangedListener)}. + * + * @param namespace The namespace containing properties to monitor. + * @param executor The executor which will be used to run callbacks. + * @param onPropertiesChangedListener The listener to add. + * @hide + * @see #removeOnPropertiesChangedListener(OnPropertiesChangedListener) + */ + @SystemApi + @TestApi + @RequiresPermission(READ_DEVICE_CONFIG) + public static void addOnPropertiesChangedListener( + @NonNull String namespace, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnPropertiesChangedListener onPropertiesChangedListener) { + synchronized (sLock) { + Pair oldNamespace = sListeners.get(onPropertiesChangedListener); + if (oldNamespace == null) { + // Brand new listener, add it to the list. + sListeners.put(onPropertiesChangedListener, 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(onPropertiesChangedListener, new Pair<>(namespace, executor)); + } else { + // Update this listener from an old namespace to the new one. + decrementNamespace(sListeners.get(onPropertiesChangedListener).first); + sListeners.put(onPropertiesChangedListener, new Pair<>(namespace, executor)); incrementNamespace(namespace); } } @@ -627,11 +669,33 @@ public final class DeviceConfig { @SystemApi @TestApi public static void removeOnPropertyChangedListener( - OnPropertyChangedListener onPropertyChangedListener) { + @NonNull OnPropertyChangedListener onPropertyChangedListener) { + Preconditions.checkNotNull(onPropertyChangedListener); synchronized (sLock) { - if (sListeners.containsKey(onPropertyChangedListener)) { - decrementNamespace(sListeners.get(onPropertyChangedListener).first); - sListeners.remove(onPropertyChangedListener); + if (sSingleListeners.containsKey(onPropertyChangedListener)) { + decrementNamespace(sSingleListeners.get(onPropertyChangedListener).first); + sSingleListeners.remove(onPropertyChangedListener); + } + } + } + + /** + * Remove a listener for property changes. The listener will receive no further notification of + * property changes. + * + * @param onPropertiesChangedListener The listener to remove. + * @hide + * @see #addOnPropertiesChangedListener(String, Executor, OnPropertiesChangedListener) + */ + @SystemApi + @TestApi + public static void removeOnPropertiesChangedListener( + @NonNull OnPropertiesChangedListener onPropertiesChangedListener) { + Preconditions.checkNotNull(onPropertiesChangedListener); + synchronized (sLock) { + if (sListeners.containsKey(onPropertiesChangedListener)) { + decrementNamespace(sListeners.get(onPropertiesChangedListener).first); + sListeners.remove(onPropertiesChangedListener); } } } @@ -700,14 +764,30 @@ public final class DeviceConfig { final String name = pathSegments.get(2); final String value = getProperty(namespace, name); synchronized (sLock) { - for (final OnPropertyChangedListener listener : sListeners.keySet()) { - if (namespace.equals(sListeners.get(listener).first)) { - sListeners.get(listener).second.execute(new Runnable() { + // OnPropertiesChangedListeners + for (int i = 0; i < sListeners.size(); i++) { + if (namespace.equals(sListeners.valueAt(i).first)) { + final int j = i; + sListeners.valueAt(i).second.execute(new Runnable() { @Override public void run() { Map propertyMap = new HashMap(1); propertyMap.put(name, value); - listener.onPropertiesChanged(new Properties(namespace, propertyMap)); + sListeners.keyAt(j) + .onPropertiesChanged(new Properties(namespace, propertyMap)); + } + + }); + } + } + // OnPropertyChangedListeners + for (int i = 0; i < sSingleListeners.size(); i++) { + if (namespace.equals(sSingleListeners.valueAt(i).first)) { + final int j = i; + sSingleListeners.valueAt(i).second.execute(new Runnable() { + @Override + public void run() { + sSingleListeners.keyAt(j).onPropertyChanged(namespace, name, value); } }); @@ -717,7 +797,7 @@ public final class DeviceConfig { } /** - * Interface for monitoring to properties. + * Interface for monitoring single property changes. *

* Override {@link #onPropertyChanged(String, String, String)} to handle callbacks for changes. * @@ -734,22 +814,25 @@ public final class DeviceConfig { * @param value The new value of the property which has changed. */ void onPropertyChanged(String namespace, String name, String value); + } + /** + * Interface for monitoring changes to properties. + *

+ * Override {@link #onPropertiesChanged(Properties)} to handle callbacks for changes. + * + * @hide + */ + @SystemApi + @TestApi + public interface OnPropertiesChangedListener { /** * Called when one or more properties have changed. * * @param properties Contains the complete collection of properties which have changed for a - * single namespace. + * single namespace. */ - default void onPropertiesChanged(@NonNull Properties properties) { - // During the transitional period, this method calls the old one to ensure legacy - // callers continue to function as expected. Ignore this if you are implementing it for - // yourself. - String namespace = properties.getNamespace(); - for (String name : properties.getKeyset()) { - onPropertyChanged(namespace, name, properties.getString(name, null)); - } - } + void onPropertiesChanged(@NonNull Properties properties); } /** diff --git a/core/tests/coretests/src/android/provider/DeviceConfigTest.java b/core/tests/coretests/src/android/provider/DeviceConfigTest.java index d100f40aa4f70..04fa52426f015 100644 --- a/core/tests/coretests/src/android/provider/DeviceConfigTest.java +++ b/core/tests/coretests/src/android/provider/DeviceConfigTest.java @@ -16,6 +16,7 @@ package android.provider; +import static android.provider.DeviceConfig.OnPropertiesChangedListener; import static android.provider.DeviceConfig.OnPropertyChangedListener; import static com.google.common.truth.Truth.assertThat; @@ -224,29 +225,31 @@ public class DeviceConfigTest { } @Test - public void testListener_propertiesCallback() throws InterruptedException { + public void testOnPropertiesChangedListener() throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(1); - OnPropertyChangedListener changeListener = new OnPropertyChangedListener() { - public void onPropertyChanged(String namespace, String name, String value) { - // ignore legacy callback - } - - @Override - public void onPropertiesChanged(DeviceConfig.Properties properties) { - assertThat(properties.getNamespace()).isEqualTo(sNamespace); - assertThat(properties.getKeyset().size()).isEqualTo(1); - assertThat(properties.getKeyset()).contains(sKey); - assertThat(properties.getString(sKey, "default_value")).isEqualTo(sValue); - countDownLatch.countDown(); - } + OnPropertiesChangedListener changeListener = (properties) -> { + assertThat(properties.getNamespace()).isEqualTo(sNamespace); + assertThat(properties.getKeyset()).contains(sKey); + assertThat(properties.getString(sKey, "default_value")).isEqualTo(sValue); + countDownLatch.countDown(); }; - testListener(countDownLatch, changeListener); + try { + DeviceConfig.addOnPropertiesChangedListener(sNamespace, + ActivityThread.currentApplication().getMainExecutor(), changeListener); + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + assertThat(countDownLatch.await( + WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue(); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } finally { + DeviceConfig.removeOnPropertiesChangedListener(changeListener); + } } @Test - public void testListener_legacyCallback() throws InterruptedException { + public void testOnPropertyChangedListener() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); OnPropertyChangedListener changeListener = (namespace, name, value) -> { @@ -256,12 +259,6 @@ public class DeviceConfigTest { countDownLatch.countDown(); }; - testListener(countDownLatch, changeListener); - - } - - private void testListener(CountDownLatch countDownLatch, - OnPropertyChangedListener changeListener) { try { DeviceConfig.addOnPropertyChangedListener(sNamespace, ActivityThread.currentApplication().getMainExecutor(), changeListener); @@ -273,6 +270,7 @@ public class DeviceConfigTest { } finally { DeviceConfig.removeOnPropertyChangedListener(changeListener); } + } private static boolean deleteViaContentProvider(String namespace, String key) {