Merge "[1/n] Implement SynchedDeviceConfig utility class" into udc-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
48c8f19185
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user