diff options
| author | 2023-06-14 16:24:40 +0000 | |
|---|---|---|
| committer | 2023-06-14 16:24:40 +0000 | |
| commit | 48c8f191853ac10a60c92230f918e252eebf8a87 (patch) | |
| tree | 73f65772d0cf81e69d5d181bfd7a1105b7001910 | |
| parent | 176cef8731b45cd7fa1d7ae74eb22fb2f0371ec9 (diff) | |
| parent | debbfa4a9b554ab74c95d347808ca75b154d897b (diff) | |
Merge "[1/n] Implement SynchedDeviceConfig utility class" into udc-qpr-dev
| -rw-r--r-- | services/core/java/com/android/server/wm/SynchedDeviceConfig.java | 190 | ||||
| -rw-r--r-- | services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java | 194 |
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); + } +} |