summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Massimo Carli <mcarli@google.com> 2023-06-14 16:24:40 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-06-14 16:24:40 +0000
commit48c8f191853ac10a60c92230f918e252eebf8a87 (patch)
tree73f65772d0cf81e69d5d181bfd7a1105b7001910
parent176cef8731b45cd7fa1d7ae74eb22fb2f0371ec9 (diff)
parentdebbfa4a9b554ab74c95d347808ca75b154d897b (diff)
Merge "[1/n] Implement SynchedDeviceConfig utility class" into udc-qpr-dev
-rw-r--r--services/core/java/com/android/server/wm/SynchedDeviceConfig.java190
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java194
2 files changed, 384 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/wm/SynchedDeviceConfig.java b/services/core/java/com/android/server/wm/SynchedDeviceConfig.java
new file mode 100644
index 000000000000..c2e819e4c60b
--- /dev/null
+++ b/services/core/java/com/android/server/wm/SynchedDeviceConfig.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2023 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 com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * Utility class that caches {@link DeviceConfig} flags and listens to updates by implementing
+ * {@link DeviceConfig.OnPropertiesChangedListener}.
+ */
+final class SynchedDeviceConfig implements DeviceConfig.OnPropertiesChangedListener {
+
+ private final String mNamespace;
+ private final Executor mExecutor;
+
+ private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries;
+
+ /**
+ * @param namespace The namespace for the {@link DeviceConfig}
+ * @param executor The {@link Executor} implementation to use when receiving updates
+ * @return the Builder implementation for the SynchedDeviceConfig
+ */
+ @NonNull
+ static SynchedDeviceConfigBuilder builder(@NonNull String namespace,
+ @NonNull Executor executor) {
+ return new SynchedDeviceConfigBuilder(namespace, executor);
+ }
+
+ private SynchedDeviceConfig(@NonNull String namespace, @NonNull Executor executor,
+ @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries) {
+ mNamespace = namespace;
+ mExecutor = executor;
+ mDeviceConfigEntries = deviceConfigEntries;
+ }
+
+ @Override
+ public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) {
+ for (SynchedDeviceConfigEntry entry : mDeviceConfigEntries.values()) {
+ if (properties.getKeyset().contains(entry.mFlagKey)) {
+ entry.updateValue(properties.getBoolean(entry.mFlagKey, entry.mDefaultValue));
+ }
+ }
+ }
+
+ /**
+ * Builds the {@link SynchedDeviceConfig} and start listening to the {@link DeviceConfig}
+ * updates.
+ *
+ * @return The {@link SynchedDeviceConfig}
+ */
+ @NonNull
+ private SynchedDeviceConfig start() {
+ DeviceConfig.addOnPropertiesChangedListener(mNamespace,
+ mExecutor, /* onPropertiesChangedListener */ this);
+ return this;
+ }
+
+ /**
+ * Requests a {@link DeviceConfig} update for all the flags
+ */
+ @NonNull
+ private SynchedDeviceConfig updateFlags() {
+ mDeviceConfigEntries.forEach((key, entry) -> entry.updateValue(
+ isDeviceConfigFlagEnabled(key, entry.mDefaultValue)));
+ return this;
+ }
+
+ /**
+ * Returns values of the {@code key} flag with the following criteria:
+ *
+ * <ul>
+ * <li>{@code false} if the build time flag is disabled.
+ * <li>{@code defaultValue} if the build time flag is enabled and no {@link DeviceConfig}
+ * updates happened
+ * <li>Last value from {@link DeviceConfig} in case of updates.
+ * </ul>
+ *
+ * @throws IllegalArgumentException {@code key} isn't recognised.
+ */
+ boolean getFlagValue(@NonNull String key) {
+ return findEntry(key).map(SynchedDeviceConfigEntry::getValue)
+ .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key));
+ }
+
+ /**
+ * @return {@code true} if the flag for the given {@code key} was enabled at build time.
+ */
+ boolean isBuildTimeFlagEnabled(@NonNull String key) {
+ return findEntry(key).map(SynchedDeviceConfigEntry::isBuildTimeFlagEnabled)
+ .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key));
+ }
+
+ private boolean isDeviceConfigFlagEnabled(@NonNull String key, boolean defaultValue) {
+ return DeviceConfig.getBoolean(mNamespace, key, defaultValue);
+ }
+
+ @NonNull
+ private Optional<SynchedDeviceConfigEntry> findEntry(@NonNull String key) {
+ return Optional.ofNullable(mDeviceConfigEntries.get(key));
+ }
+
+ static class SynchedDeviceConfigBuilder {
+
+ private final String mNamespace;
+ private final Executor mExecutor;
+
+ private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries =
+ new ConcurrentHashMap<>();
+
+ private SynchedDeviceConfigBuilder(@NonNull String namespace, @NonNull Executor executor) {
+ mNamespace = namespace;
+ mExecutor = executor;
+ }
+
+ @NonNull
+ SynchedDeviceConfigBuilder addDeviceConfigEntry(@NonNull String key,
+ boolean defaultValue, boolean enabled) {
+ if (mDeviceConfigEntries.containsKey(key)) {
+ throw new AssertionError("Key already present: " + key);
+ }
+ mDeviceConfigEntries.put(key,
+ new SynchedDeviceConfigEntry(key, defaultValue, enabled));
+ return this;
+ }
+
+ @NonNull
+ SynchedDeviceConfig build() {
+ return new SynchedDeviceConfig(mNamespace, mExecutor,
+ mDeviceConfigEntries).updateFlags().start();
+ }
+ }
+
+ /**
+ * Contains all the information related to an entry to be managed by DeviceConfig
+ */
+ private static class SynchedDeviceConfigEntry {
+
+ // The key of the specific configuration flag
+ private final String mFlagKey;
+
+ // The value of the flag at build time.
+ private final boolean mBuildTimeFlagEnabled;
+
+ // The initial value of the flag when mBuildTimeFlagEnabled is true.
+ private final boolean mDefaultValue;
+
+ // The current value of the flag when mBuildTimeFlagEnabled is true.
+ private volatile boolean mOverrideValue;
+
+ private SynchedDeviceConfigEntry(@NonNull String flagKey, boolean defaultValue,
+ boolean enabled) {
+ mFlagKey = flagKey;
+ mOverrideValue = mDefaultValue = defaultValue;
+ mBuildTimeFlagEnabled = enabled;
+ }
+
+ @NonNull
+ private void updateValue(boolean newValue) {
+ mOverrideValue = newValue;
+ }
+
+ private boolean getValue() {
+ return mBuildTimeFlagEnabled && mOverrideValue;
+ }
+
+ private boolean isBuildTimeFlagEnabled() {
+ return mBuildTimeFlagEnabled;
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java b/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java
new file mode 100644
index 000000000000..ecab62f72f69
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 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 com.android.server.wm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.app.ActivityThread;
+import android.platform.test.annotations.Presubmit;
+import android.provider.DeviceConfig;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.modules.utils.testing.TestableDeviceConfig;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test class for {@link SynchedDeviceConfig}.
+ *
+ * atest WmTests:SynchedDeviceConfigTests
+ */
+@SmallTest
+@Presubmit
+public class SynchedDeviceConfigTests {
+
+ private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
+ private static final String NAMESPACE_FOR_TEST = "TestingNameSpace";
+
+ private SynchedDeviceConfig mDeviceConfig;
+
+ private Executor mExecutor;
+
+ @Rule
+ public final TestableDeviceConfig.TestableDeviceConfigRule
+ mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule();
+
+ @Before
+ public void setUp() {
+ mExecutor = Objects.requireNonNull(ActivityThread.currentApplication()).getMainExecutor();
+ mDeviceConfig = SynchedDeviceConfig
+ .builder(/* nameSpace */ NAMESPACE_FOR_TEST, /* executor */ mExecutor)
+ .addDeviceConfigEntry(/* key */ "key1", /* default */ true, /* enabled */ true)
+ .addDeviceConfigEntry(/* key */ "key2", /* default */ false, /* enabled */ true)
+ .addDeviceConfigEntry(/* key */ "key3", /* default */ true, /* enabled */ false)
+ .addDeviceConfigEntry(/* key */ "key4", /* default */ false, /* enabled */ false)
+ .addDeviceConfigEntry(/* key */ "key5", /* default */ true, /* enabled */ false)
+ .addDeviceConfigEntry(/* key */ "key6", /* default */ false, /* enabled */ false)
+ .build();
+ }
+
+ @After
+ public void tearDown() {
+ DeviceConfig.removeOnPropertiesChangedListener(mDeviceConfig);
+ }
+
+ @Test
+ public void testWhenStarted_initialValuesAreDefaultOrFalseIfDisabled() {
+ assertFlagValue(/* key */ "key1", /* expected */ true); // enabled
+ assertFlagValue(/* key */ "key2", /* expected */ false); // enabled
+ assertFlagValue(/* key */ "key3", /* expected */ false); // disabled
+ assertFlagValue(/* key */ "key4", /* expected */ false); // disabled
+ assertFlagValue(/* key */ "key5", /* expected */ false); // disabled
+ assertFlagValue(/* key */ "key6", /* expected */ false); // disabled
+ }
+
+ @Test
+ public void testIsEnabled() {
+ assertFlagEnabled(/* key */ "key1", /* expected */ true);
+ assertFlagEnabled(/* key */ "key2", /* expected */ true);
+ assertFlagEnabled(/* key */ "key3", /* expected */ false);
+ assertFlagEnabled(/* key */ "key4", /* expected */ false);
+ assertFlagEnabled(/* key */ "key5", /* expected */ false);
+ assertFlagEnabled(/* key */ "key6", /* expected */ false);
+ }
+
+ @Test
+ public void testWhenUpdated_onlyEnabledChanges() {
+ final CountDownLatch countDownLatch = new CountDownLatch(4);
+ final DeviceConfig.OnPropertiesChangedListener countDownLatchListener =
+ properties -> countDownLatch.countDown();
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_FOR_TEST, mExecutor,
+ countDownLatchListener);
+
+ try {
+ // We update all the keys
+ updateProperty(/* key */ "key1", /* value */ false);
+ updateProperty(/* key */ "key2", /* value */ true);
+ updateProperty(/* key */ "key3", /* value */ false);
+ updateProperty(/* key */ "key4", /* value */ true);
+
+ assertThat(countDownLatch.await(
+ WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+
+ // We update all the flags but only the enabled ones change
+ assertFlagValue(/* key */ "key1", /* expected */ false); // changes
+ assertFlagValue(/* key */ "key2", /* expected */ true); // changes
+ assertFlagValue(/* key */ "key3", /* expected */ false); // disabled
+ assertFlagValue(/* key */ "key4", /* expected */ false); // disabled
+ } catch (InterruptedException e) {
+ Assert.fail(e.getMessage());
+ } finally {
+ DeviceConfig.removeOnPropertiesChangedListener(countDownLatchListener);
+ }
+ }
+
+ @Test
+ public void testWhenEnabled_updatesAreUsed() {
+ final CountDownLatch countDownLatchBefore = new CountDownLatch(2);
+ final CountDownLatch countDownLatchAfter = new CountDownLatch(2);
+ final DeviceConfig.OnPropertiesChangedListener countDownLatchBeforeListener =
+ properties -> countDownLatchBefore.countDown();
+ final DeviceConfig.OnPropertiesChangedListener countDownLatchAfterListener =
+ properties -> countDownLatchAfter.countDown();
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_FOR_TEST, mExecutor,
+ countDownLatchBeforeListener);
+
+ try {
+ // We update disabled values
+ updateProperty(/* key */ "key3", /* value */ false);
+ updateProperty(/* key */ "key4", /* value */ true);
+
+ assertThat(countDownLatchBefore.await(
+ WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+
+ // We check they haven't been updated
+ assertFlagValue(/* key */ "key3", /* expected */ false);
+ assertFlagValue(/* key */ "key4", /* expected */ false);
+
+
+ DeviceConfig.removeOnPropertiesChangedListener(countDownLatchBeforeListener);
+ DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_FOR_TEST, mExecutor,
+ countDownLatchAfterListener);
+
+ // We update enabled flags
+ updateProperty(/* key */ "key1", /* value */ false);
+ updateProperty(/* key */ "key2", /* value */ true);
+
+ assertThat(countDownLatchAfter.await(
+ WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue();
+
+ // Value have been updated
+ assertFlagValue(/* key */ "key1", /* expected */ false);
+ assertFlagValue(/* key */ "key2", /* expected */ true);
+
+ } catch (InterruptedException e) {
+ Assert.fail(e.getMessage());
+ } finally {
+ DeviceConfig.removeOnPropertiesChangedListener(countDownLatchAfterListener);
+ }
+ }
+
+
+ private void assertFlagValue(String key, boolean expectedValue) {
+ assertEquals(/* message */"Flag " + key + " value is not " + expectedValue, /* expected */
+ expectedValue, /* actual */ mDeviceConfig.getFlagValue(key));
+ }
+
+
+ private void assertFlagEnabled(String key, boolean expectedValue) {
+ assertEquals(/* message */
+ "Flag " + key + " enabled is not " + expectedValue, /* expected */
+ expectedValue, /* actual */ mDeviceConfig.isBuildTimeFlagEnabled(key));
+ }
+
+ private void updateProperty(String key, Boolean value) {
+ DeviceConfig.setProperty(NAMESPACE_FOR_TEST, key, /* value */
+ value.toString(), /* makeDefault */ false);
+ }
+}