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