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