1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.wm;
18 
19 import android.annotation.NonNull;
20 import android.provider.DeviceConfig;
21 
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.Executor;
25 
26 /**
27  * Utility class that caches {@link DeviceConfig} flags and listens to updates by implementing
28  * {@link DeviceConfig.OnPropertiesChangedListener}.
29  */
30 final class SynchedDeviceConfig implements DeviceConfig.OnPropertiesChangedListener {
31 
32     private final String mNamespace;
33     private final Executor mExecutor;
34 
35     private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries;
36 
37     /**
38      * @param namespace The namespace for the {@link DeviceConfig}
39      * @param executor  The {@link Executor} implementation to use when receiving updates
40      * @return the Builder implementation for the SynchedDeviceConfig
41      */
42     @NonNull
builder(@onNull String namespace, @NonNull Executor executor)43     static SynchedDeviceConfigBuilder builder(@NonNull String namespace,
44             @NonNull Executor executor) {
45         return new SynchedDeviceConfigBuilder(namespace, executor);
46     }
47 
SynchedDeviceConfig(@onNull String namespace, @NonNull Executor executor, @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries)48     private SynchedDeviceConfig(@NonNull String namespace, @NonNull Executor executor,
49             @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries) {
50         mNamespace = namespace;
51         mExecutor = executor;
52         mDeviceConfigEntries = deviceConfigEntries;
53     }
54 
55     @Override
onPropertiesChanged(@onNull final DeviceConfig.Properties properties)56     public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) {
57         for (SynchedDeviceConfigEntry entry : mDeviceConfigEntries.values()) {
58             if (properties.getKeyset().contains(entry.mFlagKey)) {
59                 entry.updateValue(properties.getBoolean(entry.mFlagKey, entry.mDefaultValue));
60             }
61         }
62     }
63 
64     /**
65      * Builds the {@link SynchedDeviceConfig} and start listening to the {@link DeviceConfig}
66      * updates.
67      *
68      * @return The {@link SynchedDeviceConfig}
69      */
70     @NonNull
start()71     private SynchedDeviceConfig start() {
72         DeviceConfig.addOnPropertiesChangedListener(mNamespace,
73                 mExecutor, /* onPropertiesChangedListener */ this);
74         return this;
75     }
76 
77     /**
78      * Requests a {@link DeviceConfig} update for all the flags
79      */
80     @NonNull
updateFlags()81     private SynchedDeviceConfig updateFlags() {
82         mDeviceConfigEntries.forEach((key, entry) -> entry.updateValue(
83                 isDeviceConfigFlagEnabled(key, entry.mDefaultValue)));
84         return this;
85     }
86 
87     /**
88      * Returns values of the {@code key} flag with the following criteria:
89      *
90      * <ul>
91      *     <li>{@code false} if the build time flag is disabled.
92      *     <li>{@code defaultValue} if the build time flag is enabled and no {@link DeviceConfig}
93      *          updates happened
94      *     <li>Last value from {@link DeviceConfig} in case of updates.
95      * </ul>
96      *
97      * @throws IllegalArgumentException {@code key} isn't recognised.
98      */
getFlagValue(@onNull String key)99     boolean getFlagValue(@NonNull String key) {
100         final SynchedDeviceConfigEntry entry = mDeviceConfigEntries.get(key);
101         if (entry == null) {
102             throw new IllegalArgumentException("Unexpected flag name: " + key);
103         }
104         return entry.getValue();
105     }
106 
107     /**
108      * @return {@code true} if the flag for the given {@code key} was enabled at build time.
109      */
isBuildTimeFlagEnabled(@onNull String key)110     boolean isBuildTimeFlagEnabled(@NonNull String key) {
111         final SynchedDeviceConfigEntry entry = mDeviceConfigEntries.get(key);
112         if (entry == null) {
113             throw new IllegalArgumentException("Unexpected flag name: " + key);
114         }
115         return entry.isBuildTimeFlagEnabled();
116     }
117 
isDeviceConfigFlagEnabled(@onNull String key, boolean defaultValue)118     private boolean isDeviceConfigFlagEnabled(@NonNull String key, boolean defaultValue) {
119         return DeviceConfig.getBoolean(mNamespace, key, defaultValue);
120     }
121 
122     static class SynchedDeviceConfigBuilder {
123 
124         private final String mNamespace;
125         private final Executor mExecutor;
126 
127         private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries =
128                 new ConcurrentHashMap<>();
129 
SynchedDeviceConfigBuilder(@onNull String namespace, @NonNull Executor executor)130         private SynchedDeviceConfigBuilder(@NonNull String namespace, @NonNull Executor executor) {
131             mNamespace = namespace;
132             mExecutor = executor;
133         }
134 
135         @NonNull
addDeviceConfigEntry(@onNull String key, boolean defaultValue, boolean enabled)136         SynchedDeviceConfigBuilder addDeviceConfigEntry(@NonNull String key,
137                 boolean defaultValue, boolean enabled) {
138             if (mDeviceConfigEntries.containsKey(key)) {
139                 throw new AssertionError("Key already present: " + key);
140             }
141             mDeviceConfigEntries.put(key,
142                     new SynchedDeviceConfigEntry(key, defaultValue, enabled));
143             return this;
144         }
145 
146         @NonNull
build()147         SynchedDeviceConfig build() {
148             return new SynchedDeviceConfig(mNamespace, mExecutor,
149                     mDeviceConfigEntries).updateFlags().start();
150         }
151     }
152 
153     /**
154      * Contains all the information related to an entry to be managed by DeviceConfig
155      */
156     private static class SynchedDeviceConfigEntry {
157 
158         // The key of the specific configuration flag
159         private final String mFlagKey;
160 
161         // The value of the flag at build time.
162         private final boolean mBuildTimeFlagEnabled;
163 
164         // The initial value of the flag when mBuildTimeFlagEnabled is true.
165         private final boolean mDefaultValue;
166 
167         // The current value of the flag when mBuildTimeFlagEnabled is true.
168         private volatile boolean mOverrideValue;
169 
SynchedDeviceConfigEntry(@onNull String flagKey, boolean defaultValue, boolean enabled)170         private SynchedDeviceConfigEntry(@NonNull String flagKey, boolean defaultValue,
171                 boolean enabled) {
172             mFlagKey = flagKey;
173             mOverrideValue = mDefaultValue = defaultValue;
174             mBuildTimeFlagEnabled = enabled;
175         }
176 
177         @NonNull
updateValue(boolean newValue)178         private void updateValue(boolean newValue) {
179             mOverrideValue = newValue;
180         }
181 
getValue()182         private boolean getValue() {
183             return mBuildTimeFlagEnabled && mOverrideValue;
184         }
185 
isBuildTimeFlagEnabled()186         private boolean isBuildTimeFlagEnabled() {
187             return mBuildTimeFlagEnabled;
188         }
189     }
190 }
191