Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 48c8f191 authored by Massimo Carli's avatar Massimo Carli Committed by Android (Google) Code Review
Browse files

Merge "[1/n] Implement SynchedDeviceConfig utility class" into udc-qpr-dev

parents 176cef87 debbfa4a
Loading
Loading
Loading
Loading
+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;
        }
    }
}
+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);
    }
}