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