1 /* 2 * Copyright (C) 2021 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.systemui.flags; 18 19 import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS; 20 import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG; 21 import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS; 22 import static com.android.systemui.flags.FlagManager.EXTRA_NAME; 23 import static com.android.systemui.flags.FlagManager.EXTRA_VALUE; 24 import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS; 25 26 import static java.util.Objects.requireNonNull; 27 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.res.Resources; 33 import android.os.Bundle; 34 import android.os.UserHandle; 35 import android.util.Log; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 40 import com.android.systemui.dagger.SysUISingleton; 41 import com.android.systemui.dagger.qualifiers.Main; 42 import com.android.systemui.util.settings.GlobalSettings; 43 44 import org.jetbrains.annotations.NotNull; 45 46 import java.io.PrintWriter; 47 import java.util.ArrayList; 48 import java.util.Map; 49 import java.util.Objects; 50 import java.util.TreeMap; 51 import java.util.function.Consumer; 52 53 import javax.inject.Inject; 54 import javax.inject.Named; 55 56 /** 57 * Concrete implementation of the a Flag manager that returns default values for debug builds 58 * 59 * Flags can be set (or unset) via the following adb command: 60 * 61 * adb shell cmd statusbar flag <id> <on|off|toggle|erase> 62 * 63 * Alternatively, you can change flags via a broadcast intent: 64 * 65 * adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>] 66 * 67 * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command. 68 */ 69 @SysUISingleton 70 public class FeatureFlagsDebug implements FeatureFlags { 71 static final String TAG = "SysUIFlags"; 72 73 private final FlagManager mFlagManager; 74 private final Context mContext; 75 private final GlobalSettings mGlobalSettings; 76 private final Resources mResources; 77 private final SystemPropertiesHelper mSystemProperties; 78 private final ServerFlagReader mServerFlagReader; 79 private final Map<String, Flag<?>> mAllFlags; 80 private final Map<String, Boolean> mBooleanFlagCache = new TreeMap<>(); 81 private final Map<String, String> mStringFlagCache = new TreeMap<>(); 82 private final Map<String, Integer> mIntFlagCache = new TreeMap<>(); 83 private final Restarter mRestarter; 84 85 private final ServerFlagReader.ChangeListener mOnPropertiesChanged = 86 new ServerFlagReader.ChangeListener() { 87 @Override 88 public void onChange(Flag<?> flag, String value) { 89 boolean shouldRestart = false; 90 if (mBooleanFlagCache.containsKey(flag.getName())) { 91 boolean newValue = value == null ? false : Boolean.parseBoolean(value); 92 if (mBooleanFlagCache.get(flag.getName()) != newValue) { 93 shouldRestart = true; 94 } 95 } else if (mStringFlagCache.containsKey(flag.getName())) { 96 if (!mStringFlagCache.get(flag.getName()).equals(value)) { 97 shouldRestart = true; 98 } 99 } else if (mIntFlagCache.containsKey(flag.getName())) { 100 int newValue = 0; 101 try { 102 newValue = value == null ? 0 : Integer.parseInt(value); 103 } catch (NumberFormatException e) { 104 } 105 if (mIntFlagCache.get(flag.getName()) != newValue) { 106 shouldRestart = true; 107 } 108 } 109 if (shouldRestart) { 110 mRestarter.restartSystemUI( 111 "Server flag change: " + flag.getNamespace() + "." 112 + flag.getName()); 113 114 } 115 } 116 }; 117 118 @Inject FeatureFlagsDebug( FlagManager flagManager, Context context, GlobalSettings globalSettings, SystemPropertiesHelper systemProperties, @Main Resources resources, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags, Restarter restarter)119 public FeatureFlagsDebug( 120 FlagManager flagManager, 121 Context context, 122 GlobalSettings globalSettings, 123 SystemPropertiesHelper systemProperties, 124 @Main Resources resources, 125 ServerFlagReader serverFlagReader, 126 @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags, 127 Restarter restarter) { 128 mFlagManager = flagManager; 129 mContext = context; 130 mGlobalSettings = globalSettings; 131 mResources = resources; 132 mSystemProperties = systemProperties; 133 mServerFlagReader = serverFlagReader; 134 mAllFlags = allFlags; 135 mRestarter = restarter; 136 } 137 138 /** Call after construction to setup listeners. */ init()139 void init() { 140 IntentFilter filter = new IntentFilter(); 141 filter.addAction(ACTION_SET_FLAG); 142 filter.addAction(ACTION_GET_FLAGS); 143 mFlagManager.setOnSettingsChangedAction( 144 suppressRestart -> restartSystemUI(suppressRestart, "Settings changed")); 145 mFlagManager.setClearCacheAction(this::removeFromCache); 146 mContext.registerReceiver(mReceiver, filter, null, null, 147 Context.RECEIVER_EXPORTED_UNAUDITED); 148 mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged); 149 } 150 151 @Override isEnabled(@otNull UnreleasedFlag flag)152 public boolean isEnabled(@NotNull UnreleasedFlag flag) { 153 return isEnabledInternal(flag); 154 } 155 156 @Override isEnabled(@otNull ReleasedFlag flag)157 public boolean isEnabled(@NotNull ReleasedFlag flag) { 158 return isEnabledInternal(flag); 159 } 160 isEnabledInternal(@otNull BooleanFlag flag)161 private boolean isEnabledInternal(@NotNull BooleanFlag flag) { 162 String name = flag.getName(); 163 if (!mBooleanFlagCache.containsKey(name)) { 164 mBooleanFlagCache.put(name, 165 readBooleanFlagInternal(flag, flag.getDefault())); 166 } 167 168 return mBooleanFlagCache.get(name); 169 } 170 171 @Override isEnabled(@onNull ResourceBooleanFlag flag)172 public boolean isEnabled(@NonNull ResourceBooleanFlag flag) { 173 String name = flag.getName(); 174 if (!mBooleanFlagCache.containsKey(name)) { 175 mBooleanFlagCache.put(name, 176 readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId()))); 177 } 178 179 return mBooleanFlagCache.get(name); 180 } 181 182 @Override isEnabled(@onNull SysPropBooleanFlag flag)183 public boolean isEnabled(@NonNull SysPropBooleanFlag flag) { 184 String name = flag.getName(); 185 if (!mBooleanFlagCache.containsKey(name)) { 186 // Use #readFlagValue to get the default. That will allow it to fall through to 187 // teamfood if need be. 188 mBooleanFlagCache.put( 189 name, 190 mSystemProperties.getBoolean( 191 flag.getName(), 192 readBooleanFlagInternal(flag, flag.getDefault()))); 193 } 194 195 return mBooleanFlagCache.get(name); 196 } 197 198 @NonNull 199 @Override getString(@onNull StringFlag flag)200 public String getString(@NonNull StringFlag flag) { 201 String name = flag.getName(); 202 if (!mStringFlagCache.containsKey(name)) { 203 mStringFlagCache.put(name, 204 readFlagValueInternal(name, flag.getDefault(), StringFlagSerializer.INSTANCE)); 205 } 206 207 return mStringFlagCache.get(name); 208 } 209 210 @NonNull 211 @Override getString(@onNull ResourceStringFlag flag)212 public String getString(@NonNull ResourceStringFlag flag) { 213 String name = flag.getName(); 214 if (!mStringFlagCache.containsKey(name)) { 215 mStringFlagCache.put(name, 216 readFlagValueInternal(name, mResources.getString(flag.getResourceId()), 217 StringFlagSerializer.INSTANCE)); 218 } 219 220 return mStringFlagCache.get(name); 221 } 222 223 @Override getInt(@onNull IntFlag flag)224 public int getInt(@NonNull IntFlag flag) { 225 String name = flag.getName(); 226 if (!mIntFlagCache.containsKey(name)) { 227 mIntFlagCache.put(name, 228 readFlagValueInternal(name, flag.getDefault(), IntFlagSerializer.INSTANCE)); 229 } 230 231 return mIntFlagCache.get(name); 232 } 233 234 @Override getInt(@onNull ResourceIntFlag flag)235 public int getInt(@NonNull ResourceIntFlag flag) { 236 String name = flag.getName(); 237 if (!mIntFlagCache.containsKey(name)) { 238 mIntFlagCache.put(name, 239 readFlagValueInternal(name, mResources.getInteger(flag.getResourceId()), 240 IntFlagSerializer.INSTANCE)); 241 } 242 243 return mIntFlagCache.get(name); 244 } 245 246 /** Specific override for Boolean flags that checks against the teamfood list.*/ readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue)247 private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) { 248 Boolean result = readBooleanFlagOverride(flag.getName()); 249 boolean hasServerOverride = mServerFlagReader.hasOverride( 250 flag.getNamespace(), flag.getName()); 251 252 // Only check for teamfood if the default is false 253 // and there is no server override. 254 if (!hasServerOverride 255 && !defaultValue 256 && result == null 257 && !flag.getName().equals(Flags.TEAMFOOD.getName()) 258 && flag.getTeamfood()) { 259 return isEnabled(Flags.TEAMFOOD); 260 } 261 262 return result == null ? mServerFlagReader.readServerOverride( 263 flag.getNamespace(), flag.getName(), defaultValue) : result; 264 } 265 266 readBooleanFlagOverride(String name)267 private Boolean readBooleanFlagOverride(String name) { 268 return readFlagValueInternal(name, BooleanFlagSerializer.INSTANCE); 269 } 270 271 @NonNull readFlagValueInternal( String name, @NonNull T defaultValue, FlagSerializer<T> serializer)272 private <T> T readFlagValueInternal( 273 String name, @NonNull T defaultValue, FlagSerializer<T> serializer) { 274 requireNonNull(defaultValue, "defaultValue"); 275 T resultForName = readFlagValueInternal(name, serializer); 276 if (resultForName == null) { 277 return defaultValue; 278 } 279 return resultForName; 280 } 281 282 /** Returns the stored value or null if not set. */ 283 @Nullable readFlagValueInternal(String name, FlagSerializer<T> serializer)284 private <T> T readFlagValueInternal(String name, FlagSerializer<T> serializer) { 285 try { 286 return mFlagManager.readFlagValue(name, serializer); 287 } catch (Exception e) { 288 eraseInternal(name); 289 } 290 return null; 291 } 292 setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer)293 private <T> void setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer) { 294 requireNonNull(value, "Cannot set a null value"); 295 T currentValue = readFlagValueInternal(name, serializer); 296 if (Objects.equals(currentValue, value)) { 297 Log.i(TAG, "Flag \"" + name + "\" is already " + value); 298 return; 299 } 300 setFlagValueInternal(name, value, serializer); 301 Log.i(TAG, "Set flag \"" + name + "\" to " + value); 302 removeFromCache(name); 303 mFlagManager.dispatchListenersAndMaybeRestart( 304 name, 305 suppressRestart -> restartSystemUI( 306 suppressRestart, "Flag \"" + name + "\" changed to " + value)); 307 } 308 setFlagValueInternal( String name, @NonNull T value, FlagSerializer<T> serializer)309 private <T> void setFlagValueInternal( 310 String name, @NonNull T value, FlagSerializer<T> serializer) { 311 final String data = serializer.toSettingsData(value); 312 if (data == null) { 313 Log.w(TAG, "Failed to set flag " + name + " to " + value); 314 return; 315 } 316 mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), data, 317 UserHandle.USER_CURRENT); 318 } 319 eraseFlag(Flag<T> flag)320 <T> void eraseFlag(Flag<T> flag) { 321 if (flag instanceof SysPropFlag) { 322 mSystemProperties.erase(flag.getName()); 323 dispatchListenersAndMaybeRestart( 324 flag.getName(), 325 suppressRestart -> restartSystemUI( 326 suppressRestart, 327 "SysProp Flag \"" + flag.getNamespace() + "." 328 + flag.getName() + "\" reset to default.")); 329 } else { 330 eraseFlag(flag.getName()); 331 } 332 } 333 334 /** Erase a flag's overridden value if there is one. */ eraseFlag(String name)335 private void eraseFlag(String name) { 336 eraseInternal(name); 337 removeFromCache(name); 338 dispatchListenersAndMaybeRestart( 339 name, 340 suppressRestart -> restartSystemUI( 341 suppressRestart, "Flag \"" + name + "\" reset to default")); 342 } 343 dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction)344 private void dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction) { 345 mFlagManager.dispatchListenersAndMaybeRestart(name, restartAction); 346 } 347 348 /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */ eraseInternal(String name)349 private void eraseInternal(String name) { 350 // We can't actually "erase" things from settings, but we can set them to empty! 351 mGlobalSettings.putStringForUser(mFlagManager.nameToSettingsKey(name), "", 352 UserHandle.USER_CURRENT); 353 Log.i(TAG, "Erase name " + name); 354 } 355 356 @Override addListener(@onNull Flag<?> flag, @NonNull Listener listener)357 public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) { 358 mFlagManager.addListener(flag, listener); 359 } 360 361 @Override removeListener(@onNull Listener listener)362 public void removeListener(@NonNull Listener listener) { 363 mFlagManager.removeListener(listener); 364 } 365 restartSystemUI(boolean requestSuppress, String reason)366 private void restartSystemUI(boolean requestSuppress, String reason) { 367 if (requestSuppress) { 368 Log.i(TAG, "SystemUI Restart Suppressed"); 369 return; 370 } 371 mRestarter.restartSystemUI(reason); 372 } 373 restartAndroid(boolean requestSuppress, String reason)374 private void restartAndroid(boolean requestSuppress, String reason) { 375 if (requestSuppress) { 376 Log.i(TAG, "Android Restart Suppressed"); 377 return; 378 } 379 mRestarter.restartAndroid(reason); 380 } 381 setBooleanFlagInternal(Flag<?> flag, boolean value)382 void setBooleanFlagInternal(Flag<?> flag, boolean value) { 383 if (flag instanceof BooleanFlag) { 384 setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE); 385 } else if (flag instanceof ResourceBooleanFlag) { 386 setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE); 387 } else if (flag instanceof SysPropBooleanFlag) { 388 // Store SysProp flags in SystemProperties where they can read by outside parties. 389 mSystemProperties.setBoolean(flag.getName(), value); 390 dispatchListenersAndMaybeRestart( 391 flag.getName(), 392 suppressRestart -> restartSystemUI( 393 suppressRestart, 394 "Flag \"" + flag.getName() + "\" changed to " + value)); 395 } else { 396 throw new IllegalArgumentException("Unknown flag type"); 397 } 398 } 399 setStringFlagInternal(Flag<?> flag, String value)400 void setStringFlagInternal(Flag<?> flag, String value) { 401 if (flag instanceof StringFlag) { 402 setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE); 403 } else if (flag instanceof ResourceStringFlag) { 404 setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE); 405 } else { 406 throw new IllegalArgumentException("Unknown flag type"); 407 } 408 } 409 setIntFlagInternal(Flag<?> flag, int value)410 void setIntFlagInternal(Flag<?> flag, int value) { 411 if (flag instanceof IntFlag) { 412 setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE); 413 } else if (flag instanceof ResourceIntFlag) { 414 setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE); 415 } else { 416 throw new IllegalArgumentException("Unknown flag type"); 417 } 418 } 419 420 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 421 @Override 422 public void onReceive(Context context, Intent intent) { 423 String action = intent == null ? null : intent.getAction(); 424 if (action == null) { 425 return; 426 } 427 if (ACTION_SET_FLAG.equals(action)) { 428 handleSetFlag(intent.getExtras()); 429 } else if (ACTION_GET_FLAGS.equals(action)) { 430 ArrayList<Flag<?>> flags = new ArrayList<>(mAllFlags.values()); 431 432 // Convert all flags to parcelable flags. 433 ArrayList<ParcelableFlag<?>> pFlags = new ArrayList<>(); 434 for (Flag<?> f : flags) { 435 ParcelableFlag<?> pf = toParcelableFlag(f); 436 if (pf != null) { 437 pFlags.add(pf); 438 } 439 } 440 441 Bundle extras = getResultExtras(true); 442 if (extras != null) { 443 extras.putParcelableArrayList(EXTRA_FLAGS, pFlags); 444 } 445 } 446 } 447 448 private void handleSetFlag(Bundle extras) { 449 if (extras == null) { 450 Log.w(TAG, "No extras"); 451 return; 452 } 453 String name = extras.getString(EXTRA_NAME); 454 if (name == null || name.isEmpty()) { 455 Log.w(TAG, "NAME not set or is empty: " + name); 456 return; 457 } 458 459 if (!mAllFlags.containsKey(name)) { 460 Log.w(TAG, "Tried to set unknown name: " + name); 461 return; 462 } 463 Flag<?> flag = mAllFlags.get(name); 464 465 if (!extras.containsKey(EXTRA_VALUE)) { 466 eraseFlag(flag); 467 return; 468 } 469 470 Object value = extras.get(EXTRA_VALUE); 471 472 try { 473 if (value instanceof Boolean) { 474 setBooleanFlagInternal(flag, (Boolean) value); 475 } else if (value instanceof String) { 476 setStringFlagInternal(flag, (String) value); 477 } else { 478 throw new IllegalArgumentException("Unknown value type"); 479 } 480 } catch (IllegalArgumentException e) { 481 Log.w(TAG, 482 "Unable to set " + flag.getName() + " of type " + flag.getClass() 483 + " to value of type " + (value == null ? null : value.getClass())); 484 } 485 } 486 487 /** 488 * Ensures that the data we send to the app reflects the current state of the flags. 489 * 490 * Also converts an non-parcelable versions of the flags to their parcelable versions. 491 */ 492 @Nullable 493 private ParcelableFlag<?> toParcelableFlag(Flag<?> f) { 494 boolean enabled; 495 boolean teamfood = f.getTeamfood(); 496 boolean overridden; 497 498 if (f instanceof ReleasedFlag) { 499 enabled = isEnabled((ReleasedFlag) f); 500 overridden = readBooleanFlagOverride(f.getName()) != null; 501 } else if (f instanceof UnreleasedFlag) { 502 enabled = isEnabled((UnreleasedFlag) f); 503 overridden = readBooleanFlagOverride(f.getName()) != null; 504 } else if (f instanceof ResourceBooleanFlag) { 505 enabled = isEnabled((ResourceBooleanFlag) f); 506 overridden = readBooleanFlagOverride(f.getName()) != null; 507 } else if (f instanceof SysPropBooleanFlag) { 508 // TODO(b/223379190): Teamfood not supported for sysprop flags yet. 509 enabled = isEnabled((SysPropBooleanFlag) f); 510 teamfood = false; 511 overridden = !mSystemProperties.get(f.getName()).isEmpty(); 512 } else { 513 // TODO: add support for other flag types. 514 Log.w(TAG, "Unsupported Flag Type. Please file a bug."); 515 return null; 516 } 517 518 if (enabled) { 519 return new ReleasedFlag(f.getName(), f.getNamespace(), teamfood, overridden); 520 } else { 521 return new UnreleasedFlag(f.getName(), f.getNamespace(), teamfood, overridden); 522 } 523 } 524 }; 525 removeFromCache(String name)526 private void removeFromCache(String name) { 527 mBooleanFlagCache.remove(name); 528 mStringFlagCache.remove(name); 529 } 530 531 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)532 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 533 pw.println("can override: true"); 534 pw.println("booleans: " + mBooleanFlagCache.size()); 535 mBooleanFlagCache.forEach((key, value) -> pw.println(" sysui_flag_" + key + ": " + value)); 536 pw.println("Strings: " + mStringFlagCache.size()); 537 mStringFlagCache.forEach((key, value) -> pw.println(" sysui_flag_" + key 538 + ": [length=" + value.length() + "] \"" + value + "\"")); 539 } 540 541 } 542