Loading services/core/java/com/android/server/wm/SynchedDeviceConfig.java 0 → 100644 +190 −0 Original line number Diff line number Diff line /* * 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; } } } services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java 0 → 100644 +194 −0 Original line number Diff line number Diff line /* * 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); } } Loading
services/core/java/com/android/server/wm/SynchedDeviceConfig.java 0 → 100644 +190 −0 Original line number Diff line number Diff line /* * 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; } } }
services/tests/wmtests/src/com/android/server/wm/SynchedDeviceConfigTests.java 0 → 100644 +194 −0 Original line number Diff line number Diff line /* * 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); } }