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.Optional; 24 import java.util.concurrent.ConcurrentHashMap; 25 import java.util.concurrent.Executor; 26 27 /** 28 * Utility class that caches {@link DeviceConfig} flags and listens to updates by implementing 29 * {@link DeviceConfig.OnPropertiesChangedListener}. 30 */ 31 final class SynchedDeviceConfig implements DeviceConfig.OnPropertiesChangedListener { 32 33 private final String mNamespace; 34 private final Executor mExecutor; 35 36 private final Map<String, SynchedDeviceConfigEntry> mDeviceConfigEntries; 37 38 /** 39 * @param namespace The namespace for the {@link DeviceConfig} 40 * @param executor The {@link Executor} implementation to use when receiving updates 41 * @return the Builder implementation for the SynchedDeviceConfig 42 */ 43 @NonNull builder(@onNull String namespace, @NonNull Executor executor)44 static SynchedDeviceConfigBuilder builder(@NonNull String namespace, 45 @NonNull Executor executor) { 46 return new SynchedDeviceConfigBuilder(namespace, executor); 47 } 48 SynchedDeviceConfig(@onNull String namespace, @NonNull Executor executor, @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries)49 private SynchedDeviceConfig(@NonNull String namespace, @NonNull Executor executor, 50 @NonNull Map<String, SynchedDeviceConfigEntry> deviceConfigEntries) { 51 mNamespace = namespace; 52 mExecutor = executor; 53 mDeviceConfigEntries = deviceConfigEntries; 54 } 55 56 @Override onPropertiesChanged(@onNull final DeviceConfig.Properties properties)57 public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) { 58 for (SynchedDeviceConfigEntry entry : mDeviceConfigEntries.values()) { 59 if (properties.getKeyset().contains(entry.mFlagKey)) { 60 entry.updateValue(properties.getBoolean(entry.mFlagKey, entry.mDefaultValue)); 61 } 62 } 63 } 64 65 /** 66 * Builds the {@link SynchedDeviceConfig} and start listening to the {@link DeviceConfig} 67 * updates. 68 * 69 * @return The {@link SynchedDeviceConfig} 70 */ 71 @NonNull start()72 private SynchedDeviceConfig start() { 73 DeviceConfig.addOnPropertiesChangedListener(mNamespace, 74 mExecutor, /* onPropertiesChangedListener */ this); 75 return this; 76 } 77 78 /** 79 * Requests a {@link DeviceConfig} update for all the flags 80 */ 81 @NonNull updateFlags()82 private SynchedDeviceConfig updateFlags() { 83 mDeviceConfigEntries.forEach((key, entry) -> entry.updateValue( 84 isDeviceConfigFlagEnabled(key, entry.mDefaultValue))); 85 return this; 86 } 87 88 /** 89 * Returns values of the {@code key} flag with the following criteria: 90 * 91 * <ul> 92 * <li>{@code false} if the build time flag is disabled. 93 * <li>{@code defaultValue} if the build time flag is enabled and no {@link DeviceConfig} 94 * updates happened 95 * <li>Last value from {@link DeviceConfig} in case of updates. 96 * </ul> 97 * 98 * @throws IllegalArgumentException {@code key} isn't recognised. 99 */ getFlagValue(@onNull String key)100 boolean getFlagValue(@NonNull String key) { 101 return findEntry(key).map(SynchedDeviceConfigEntry::getValue) 102 .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key)); 103 } 104 105 /** 106 * @return {@code true} if the flag for the given {@code key} was enabled at build time. 107 */ isBuildTimeFlagEnabled(@onNull String key)108 boolean isBuildTimeFlagEnabled(@NonNull String key) { 109 return findEntry(key).map(SynchedDeviceConfigEntry::isBuildTimeFlagEnabled) 110 .orElseThrow(() -> new IllegalArgumentException("Unexpected flag name: " + key)); 111 } 112 isDeviceConfigFlagEnabled(@onNull String key, boolean defaultValue)113 private boolean isDeviceConfigFlagEnabled(@NonNull String key, boolean defaultValue) { 114 return DeviceConfig.getBoolean(mNamespace, key, defaultValue); 115 } 116 117 @NonNull findEntry(@onNull String key)118 private Optional<SynchedDeviceConfigEntry> findEntry(@NonNull String key) { 119 return Optional.ofNullable(mDeviceConfigEntries.get(key)); 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