1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs; 16 17 import android.content.ComponentName; 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.res.Resources; 21 import android.os.UserHandle; 22 import android.os.UserManager; 23 import android.provider.Settings.Secure; 24 import android.text.TextUtils; 25 import android.util.ArraySet; 26 import android.util.Log; 27 28 import androidx.annotation.MainThread; 29 import androidx.annotation.Nullable; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.logging.InstanceId; 33 import com.android.internal.logging.InstanceIdSequence; 34 import com.android.internal.logging.UiEventLogger; 35 import com.android.systemui.Dumpable; 36 import com.android.systemui.ProtoDumpable; 37 import com.android.systemui.R; 38 import com.android.systemui.dagger.SysUISingleton; 39 import com.android.systemui.dagger.qualifiers.Main; 40 import com.android.systemui.dump.DumpManager; 41 import com.android.systemui.dump.nano.SystemUIProtoDump; 42 import com.android.systemui.plugins.PluginListener; 43 import com.android.systemui.plugins.PluginManager; 44 import com.android.systemui.plugins.qs.QSFactory; 45 import com.android.systemui.plugins.qs.QSTile; 46 import com.android.systemui.plugins.qs.QSTileView; 47 import com.android.systemui.qs.external.CustomTile; 48 import com.android.systemui.qs.external.CustomTileStatePersister; 49 import com.android.systemui.qs.external.TileLifecycleManager; 50 import com.android.systemui.qs.external.TileServiceKey; 51 import com.android.systemui.qs.external.TileServiceRequestController; 52 import com.android.systemui.qs.logging.QSLogger; 53 import com.android.systemui.qs.nano.QsTileState; 54 import com.android.systemui.settings.UserFileManager; 55 import com.android.systemui.settings.UserTracker; 56 import com.android.systemui.statusbar.phone.AutoTileManager; 57 import com.android.systemui.statusbar.phone.CentralSurfaces; 58 import com.android.systemui.tuner.TunerService; 59 import com.android.systemui.tuner.TunerService.Tunable; 60 import com.android.systemui.util.settings.SecureSettings; 61 62 import org.jetbrains.annotations.NotNull; 63 64 import java.io.PrintWriter; 65 import java.util.ArrayList; 66 import java.util.Collection; 67 import java.util.LinkedHashMap; 68 import java.util.List; 69 import java.util.Objects; 70 import java.util.Optional; 71 import java.util.Set; 72 import java.util.concurrent.Executor; 73 import java.util.function.Predicate; 74 import java.util.stream.Collectors; 75 76 import javax.inject.Inject; 77 import javax.inject.Provider; 78 79 /** Platform implementation of the quick settings tile host 80 * 81 * This class keeps track of the set of current tiles and is the in memory source of truth 82 * (ground truth is kept in {@link Secure#QS_TILES}). When the ground truth changes, 83 * {@link #onTuningChanged} will be called and the tiles will be re-created as needed. 84 * 85 * This class also provides the interface for adding/removing/changing tiles. 86 */ 87 @SysUISingleton 88 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable { 89 private static final String TAG = "QSTileHost"; 90 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 91 private static final int MAX_QS_INSTANCE_ID = 1 << 20; 92 93 // Shared prefs that hold tile lifecycle info. 94 @VisibleForTesting 95 static final String TILES = "tiles_prefs"; 96 97 private final Context mContext; 98 private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>(); 99 private final ArrayList<String> mTileSpecs = new ArrayList<>(); 100 private final TunerService mTunerService; 101 private final PluginManager mPluginManager; 102 private final DumpManager mDumpManager; 103 private final QSLogger mQSLogger; 104 private final UiEventLogger mUiEventLogger; 105 private final InstanceIdSequence mInstanceIdSequence; 106 private final CustomTileStatePersister mCustomTileStatePersister; 107 private final Executor mMainExecutor; 108 private final UserFileManager mUserFileManager; 109 110 private final List<Callback> mCallbacks = new ArrayList<>(); 111 @Nullable 112 private AutoTileManager mAutoTiles; 113 private final ArrayList<QSFactory> mQsFactories = new ArrayList<>(); 114 private int mCurrentUser; 115 private final Optional<CentralSurfaces> mCentralSurfacesOptional; 116 private Context mUserContext; 117 private UserTracker mUserTracker; 118 private SecureSettings mSecureSettings; 119 // Keep track of whether mTilesList contains the same information as the Settings value. 120 // This is a performance optimization to reduce the number of blocking calls to Settings from 121 // main thread. 122 // This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged 123 private boolean mTilesListDirty = true; 124 125 private final TileServiceRequestController mTileServiceRequestController; 126 private TileLifecycleManager.Factory mTileLifeCycleManagerFactory; 127 128 @Inject QSTileHost(Context context, QSFactory defaultFactory, @Main Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, DumpManager dumpManager, Optional<CentralSurfaces> centralSurfacesOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, UserFileManager userFileManager )129 public QSTileHost(Context context, 130 QSFactory defaultFactory, 131 @Main Executor mainExecutor, 132 PluginManager pluginManager, 133 TunerService tunerService, 134 Provider<AutoTileManager> autoTiles, 135 DumpManager dumpManager, 136 Optional<CentralSurfaces> centralSurfacesOptional, 137 QSLogger qsLogger, 138 UiEventLogger uiEventLogger, 139 UserTracker userTracker, 140 SecureSettings secureSettings, 141 CustomTileStatePersister customTileStatePersister, 142 TileServiceRequestController.Builder tileServiceRequestControllerBuilder, 143 TileLifecycleManager.Factory tileLifecycleManagerFactory, 144 UserFileManager userFileManager 145 ) { 146 mContext = context; 147 mUserContext = context; 148 mTunerService = tunerService; 149 mPluginManager = pluginManager; 150 mDumpManager = dumpManager; 151 mQSLogger = qsLogger; 152 mUiEventLogger = uiEventLogger; 153 mMainExecutor = mainExecutor; 154 mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this); 155 mTileLifeCycleManagerFactory = tileLifecycleManagerFactory; 156 mUserFileManager = userFileManager; 157 158 mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); 159 mCentralSurfacesOptional = centralSurfacesOptional; 160 161 mQsFactories.add(defaultFactory); 162 pluginManager.addPluginListener(this, QSFactory.class, true); 163 mDumpManager.registerDumpable(TAG, this); 164 mUserTracker = userTracker; 165 mSecureSettings = secureSettings; 166 mCustomTileStatePersister = customTileStatePersister; 167 168 mainExecutor.execute(() -> { 169 // This is technically a hack to avoid circular dependency of 170 // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation 171 // finishes before creating any tiles. 172 tunerService.addTunable(this, TILES_SETTING); 173 // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. 174 mAutoTiles = autoTiles.get(); 175 mTileServiceRequestController.init(); 176 }); 177 } 178 179 @Override getNewInstanceId()180 public InstanceId getNewInstanceId() { 181 return mInstanceIdSequence.newInstanceId(); 182 } 183 destroy()184 public void destroy() { 185 mTiles.values().forEach(tile -> tile.destroy()); 186 mAutoTiles.destroy(); 187 mTunerService.removeTunable(this); 188 mPluginManager.removePluginListener(this); 189 mDumpManager.unregisterDumpable(TAG); 190 mTileServiceRequestController.destroy(); 191 } 192 193 @Override onPluginConnected(QSFactory plugin, Context pluginContext)194 public void onPluginConnected(QSFactory plugin, Context pluginContext) { 195 // Give plugins priority over creation so they can override if they wish. 196 mQsFactories.add(0, plugin); 197 String value = mTunerService.getValue(TILES_SETTING); 198 // Force remove and recreate of all tiles. 199 onTuningChanged(TILES_SETTING, ""); 200 onTuningChanged(TILES_SETTING, value); 201 } 202 203 @Override onPluginDisconnected(QSFactory plugin)204 public void onPluginDisconnected(QSFactory plugin) { 205 mQsFactories.remove(plugin); 206 // Force remove and recreate of all tiles. 207 String value = mTunerService.getValue(TILES_SETTING); 208 onTuningChanged(TILES_SETTING, ""); 209 onTuningChanged(TILES_SETTING, value); 210 } 211 212 @Override getUiEventLogger()213 public UiEventLogger getUiEventLogger() { 214 return mUiEventLogger; 215 } 216 217 @Override addCallback(Callback callback)218 public void addCallback(Callback callback) { 219 mCallbacks.add(callback); 220 } 221 222 @Override removeCallback(Callback callback)223 public void removeCallback(Callback callback) { 224 mCallbacks.remove(callback); 225 } 226 227 @Override getTiles()228 public Collection<QSTile> getTiles() { 229 return mTiles.values(); 230 } 231 232 @Override warn(String message, Throwable t)233 public void warn(String message, Throwable t) { 234 // already logged 235 } 236 237 @Override collapsePanels()238 public void collapsePanels() { 239 mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateCollapsePanels); 240 } 241 242 @Override forceCollapsePanels()243 public void forceCollapsePanels() { 244 mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateForceCollapsePanels); 245 } 246 247 @Override openPanels()248 public void openPanels() { 249 mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateOpenPanels); 250 } 251 252 @Override getContext()253 public Context getContext() { 254 return mContext; 255 } 256 257 @Override getUserContext()258 public Context getUserContext() { 259 return mUserContext; 260 } 261 262 @Override getUserId()263 public int getUserId() { 264 return mCurrentUser; 265 } 266 indexOf(String spec)267 public int indexOf(String spec) { 268 return mTileSpecs.indexOf(spec); 269 } 270 271 /** 272 * Whenever the Secure Setting keeping track of the current tiles changes (or upon start) this 273 * will be called with the new value of the setting. 274 * 275 * This method will do the following: 276 * <ol> 277 * <li>Destroy any existing tile that's not one of the current tiles (in the setting)</li> 278 * <li>Create new tiles for those that don't already exist. If this tiles end up being 279 * not available, they'll also be destroyed.</li> 280 * <li>Save the resolved list of tiles (current tiles that are available) into the setting. 281 * This means that after this call ends, the tiles in the Setting, {@link #mTileSpecs}, 282 * and visible tiles ({@link #mTiles}) must match. 283 * </li> 284 * </ol> 285 * 286 * Additionally, if the user has changed, it'll do the following: 287 * <ul> 288 * <li>Change the user for SystemUI tiles: {@link QSTile#userSwitch}.</li> 289 * <li>Destroy any {@link CustomTile} and recreate it for the new user.</li> 290 * </ul> 291 * 292 * This happens in main thread as {@link com.android.systemui.tuner.TunerServiceImpl} dispatches 293 * in main thread. 294 * 295 * @see QSTile#isAvailable 296 */ 297 @MainThread 298 @Override onTuningChanged(String key, String newValue)299 public void onTuningChanged(String key, String newValue) { 300 if (!TILES_SETTING.equals(key)) { 301 return; 302 } 303 if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { 304 newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); 305 } 306 final List<String> tileSpecs = loadTileSpecs(mContext, newValue); 307 int currentUser = mUserTracker.getUserId(); 308 if (currentUser != mCurrentUser) { 309 mUserContext = mUserTracker.getUserContext(); 310 if (mAutoTiles != null) { 311 mAutoTiles.changeUser(UserHandle.of(currentUser)); 312 } 313 } 314 if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; 315 Log.d(TAG, "Recreating tiles: " + tileSpecs); 316 mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( 317 tile -> { 318 Log.d(TAG, "Destroying tile: " + tile.getKey()); 319 mQSLogger.logTileDestroyed(tile.getKey(), "Tile removed"); 320 tile.getValue().destroy(); 321 }); 322 final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>(); 323 for (String tileSpec : tileSpecs) { 324 QSTile tile = mTiles.get(tileSpec); 325 if (tile != null && (!(tile instanceof CustomTile) 326 || ((CustomTile) tile).getUser() == currentUser)) { 327 if (tile.isAvailable()) { 328 if (DEBUG) Log.d(TAG, "Adding " + tile); 329 tile.removeCallbacks(); 330 if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) { 331 tile.userSwitch(currentUser); 332 } 333 newTiles.put(tileSpec, tile); 334 mQSLogger.logTileAdded(tileSpec); 335 } else { 336 tile.destroy(); 337 Log.d(TAG, "Destroying not available tile: " + tileSpec); 338 mQSLogger.logTileDestroyed(tileSpec, "Tile not available"); 339 } 340 } else { 341 // This means that the tile is a CustomTile AND the user is different, so let's 342 // destroy it 343 if (tile != null) { 344 tile.destroy(); 345 Log.d(TAG, "Destroying tile for wrong user: " + tileSpec); 346 mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user"); 347 } 348 Log.d(TAG, "Creating tile: " + tileSpec); 349 try { 350 tile = createTile(tileSpec); 351 if (tile != null) { 352 tile.setTileSpec(tileSpec); 353 if (tile.isAvailable()) { 354 newTiles.put(tileSpec, tile); 355 mQSLogger.logTileAdded(tileSpec); 356 } else { 357 tile.destroy(); 358 Log.d(TAG, "Destroying not available tile: " + tileSpec); 359 mQSLogger.logTileDestroyed(tileSpec, "Tile not available"); 360 } 361 } else { 362 Log.d(TAG, "No factory for a spec: " + tileSpec); 363 } 364 } catch (Throwable t) { 365 Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); 366 } 367 } 368 } 369 mCurrentUser = currentUser; 370 List<String> currentSpecs = new ArrayList<>(mTileSpecs); 371 mTileSpecs.clear(); 372 mTileSpecs.addAll(newTiles.keySet()); // Only add the valid (available) tiles. 373 mTiles.clear(); 374 mTiles.putAll(newTiles); 375 if (newTiles.isEmpty() && !tileSpecs.isEmpty()) { 376 // If we didn't manage to create any tiles, set it to empty (default) 377 Log.d(TAG, "No valid tiles on tuning changed. Setting to default."); 378 changeTilesByUser(currentSpecs, loadTileSpecs(mContext, "")); 379 } else { 380 String resolvedTiles = TextUtils.join(",", mTileSpecs); 381 if (!resolvedTiles.equals(newValue)) { 382 // If the resolved tiles (those we actually ended up with) are different than 383 // the ones that are in the setting, update the Setting. 384 saveTilesToSettings(mTileSpecs); 385 } 386 mTilesListDirty = false; 387 for (int i = 0; i < mCallbacks.size(); i++) { 388 mCallbacks.get(i).onTilesChanged(); 389 } 390 } 391 } 392 393 /** 394 * Only use with [CustomTile] if the tile doesn't exist anymore (and therefore doesn't need 395 * its lifecycle terminated). 396 */ 397 @Override removeTile(String spec)398 public void removeTile(String spec) { 399 if (spec.startsWith(CustomTile.PREFIX)) { 400 // If the tile is removed (due to it not actually existing), mark it as removed. That 401 // way it will be marked as newly added if it appears in the future. 402 setTileAdded(CustomTile.getComponentFromSpec(spec), mCurrentUser, false); 403 } 404 mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec))); 405 } 406 407 /** 408 * Remove many tiles at once. 409 * 410 * It will only save to settings once (as opposed to {@link QSTileHost#removeTileByUser} called 411 * multiple times). 412 */ 413 @Override removeTiles(Collection<String> specs)414 public void removeTiles(Collection<String> specs) { 415 mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs))); 416 } 417 418 /** 419 * Add a tile to the end 420 * 421 * @param spec string matching a pre-defined tilespec 422 */ addTile(String spec)423 public void addTile(String spec) { 424 addTile(spec, POSITION_AT_END); 425 } 426 427 @Override addTile(String spec, int requestPosition)428 public void addTile(String spec, int requestPosition) { 429 mMainExecutor.execute(() -> 430 changeTileSpecs(tileSpecs -> { 431 if (tileSpecs.contains(spec)) return false; 432 433 int size = tileSpecs.size(); 434 if (requestPosition == POSITION_AT_END || requestPosition >= size) { 435 tileSpecs.add(spec); 436 } else { 437 tileSpecs.add(requestPosition, spec); 438 } 439 return true; 440 }) 441 ); 442 } 443 444 // When calling this, you may want to modify mTilesListDirty accordingly. 445 @MainThread saveTilesToSettings(List<String> tileSpecs)446 private void saveTilesToSettings(List<String> tileSpecs) { 447 mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs), 448 null /* tag */, false /* default */, mCurrentUser, 449 true /* overrideable by restore */); 450 } 451 452 @MainThread changeTileSpecs(Predicate<List<String>> changeFunction)453 private void changeTileSpecs(Predicate<List<String>> changeFunction) { 454 final List<String> tileSpecs; 455 if (!mTilesListDirty) { 456 tileSpecs = new ArrayList<>(mTileSpecs); 457 } else { 458 tileSpecs = loadTileSpecs(mContext, 459 mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser)); 460 } 461 if (changeFunction.test(tileSpecs)) { 462 mTilesListDirty = true; 463 saveTilesToSettings(tileSpecs); 464 } 465 } 466 467 @Override addTile(ComponentName tile)468 public void addTile(ComponentName tile) { 469 addTile(tile, /* end */ false); 470 } 471 472 @Override addTile(ComponentName tile, boolean end)473 public void addTile(ComponentName tile, boolean end) { 474 String spec = CustomTile.toSpec(tile); 475 addTile(spec, end ? POSITION_AT_END : 0); 476 } 477 478 /** 479 * This will call through {@link #changeTilesByUser}. It should only be used when a tile is 480 * removed by a <b>user action</b> like {@code adb}. 481 */ 482 @Override removeTileByUser(ComponentName tile)483 public void removeTileByUser(ComponentName tile) { 484 mMainExecutor.execute(() -> { 485 List<String> newSpecs = new ArrayList<>(mTileSpecs); 486 if (newSpecs.remove(CustomTile.toSpec(tile))) { 487 changeTilesByUser(mTileSpecs, newSpecs); 488 } 489 }); 490 } 491 492 /** 493 * Change the tiles triggered by the user editing. 494 * <p> 495 * This is not called on device start, or on user change. 496 * 497 * {@link android.service.quicksettings.TileService#onTileRemoved} will be called for tiles 498 * that are removed. 499 */ 500 @MainThread 501 @Override changeTilesByUser(List<String> previousTiles, List<String> newTiles)502 public void changeTilesByUser(List<String> previousTiles, List<String> newTiles) { 503 final List<String> copy = new ArrayList<>(previousTiles); 504 final int NP = copy.size(); 505 for (int i = 0; i < NP; i++) { 506 String tileSpec = copy.get(i); 507 if (!tileSpec.startsWith(CustomTile.PREFIX)) continue; 508 if (!newTiles.contains(tileSpec)) { 509 ComponentName component = CustomTile.getComponentFromSpec(tileSpec); 510 Intent intent = new Intent().setComponent(component); 511 TileLifecycleManager lifecycleManager = mTileLifeCycleManagerFactory.create( 512 intent, new UserHandle(mCurrentUser)); 513 lifecycleManager.onStopListening(); 514 lifecycleManager.onTileRemoved(); 515 mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser)); 516 setTileAdded(component, mCurrentUser, false); 517 lifecycleManager.flushMessagesAndUnbind(); 518 } 519 } 520 if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles); 521 mTilesListDirty = true; 522 saveTilesToSettings(newTiles); 523 } 524 525 @Nullable 526 @Override createTile(String tileSpec)527 public QSTile createTile(String tileSpec) { 528 for (int i = 0; i < mQsFactories.size(); i++) { 529 QSTile t = mQsFactories.get(i).createTile(tileSpec); 530 if (t != null) { 531 return t; 532 } 533 } 534 return null; 535 } 536 537 @Override createTileView(Context themedContext, QSTile tile, boolean collapsedView)538 public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) { 539 for (int i = 0; i < mQsFactories.size(); i++) { 540 QSTileView view = mQsFactories.get(i) 541 .createTileView(themedContext, tile, collapsedView); 542 if (view != null) { 543 return view; 544 } 545 } 546 throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); 547 } 548 549 /** 550 * Check if a particular {@link CustomTile} has been added for a user and has not been removed 551 * since. 552 * @param componentName the {@link ComponentName} of the 553 * {@link android.service.quicksettings.TileService} associated with the 554 * tile. 555 * @param userId the user to check 556 */ 557 @Override isTileAdded(ComponentName componentName, int userId)558 public boolean isTileAdded(ComponentName componentName, int userId) { 559 return mUserFileManager 560 .getSharedPreferences(TILES, 0, userId) 561 .getBoolean(componentName.flattenToString(), false); 562 } 563 564 /** 565 * Persists whether a particular {@link CustomTile} has been added and it's currently in the 566 * set of selected tiles ({@link #mTiles}. 567 * @param componentName the {@link ComponentName} of the 568 * {@link android.service.quicksettings.TileService} associated 569 * with the tile. 570 * @param userId the user for this tile 571 * @param added {@code true} if the tile is being added, {@code false} otherwise 572 */ 573 @Override setTileAdded(ComponentName componentName, int userId, boolean added)574 public void setTileAdded(ComponentName componentName, int userId, boolean added) { 575 mUserFileManager.getSharedPreferences(TILES, 0, userId) 576 .edit() 577 .putBoolean(componentName.flattenToString(), added) 578 .apply(); 579 } 580 581 @Override getSpecs()582 public List<String> getSpecs() { 583 return mTileSpecs; 584 } 585 loadTileSpecs(Context context, String tileList)586 protected static List<String> loadTileSpecs(Context context, String tileList) { 587 final Resources res = context.getResources(); 588 589 if (TextUtils.isEmpty(tileList)) { 590 tileList = res.getString(R.string.quick_settings_tiles); 591 if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList); 592 } else { 593 if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); 594 } 595 final ArrayList<String> tiles = new ArrayList<String>(); 596 boolean addedDefault = false; 597 Set<String> addedSpecs = new ArraySet<>(); 598 for (String tile : tileList.split(",")) { 599 tile = tile.trim(); 600 if (tile.isEmpty()) continue; 601 if (tile.equals("default")) { 602 if (!addedDefault) { 603 List<String> defaultSpecs = QSHost.getDefaultSpecs(context); 604 for (String spec : defaultSpecs) { 605 if (!addedSpecs.contains(spec)) { 606 tiles.add(spec); 607 addedSpecs.add(spec); 608 } 609 } 610 addedDefault = true; 611 } 612 } else { 613 if (!addedSpecs.contains(tile)) { 614 tiles.add(tile); 615 addedSpecs.add(tile); 616 } 617 } 618 } 619 620 if (!tiles.contains("internet")) { 621 if (tiles.contains("wifi")) { 622 // Replace the WiFi with Internet, and remove the Cell 623 tiles.set(tiles.indexOf("wifi"), "internet"); 624 tiles.remove("cell"); 625 } else if (tiles.contains("cell")) { 626 // Replace the Cell with Internet 627 tiles.set(tiles.indexOf("cell"), "internet"); 628 } 629 } else { 630 tiles.remove("wifi"); 631 tiles.remove("cell"); 632 } 633 return tiles; 634 } 635 636 @Override dump(PrintWriter pw, String[] args)637 public void dump(PrintWriter pw, String[] args) { 638 pw.println("QSTileHost:"); 639 mTiles.values().stream().filter(obj -> obj instanceof Dumpable) 640 .forEach(o -> ((Dumpable) o).dump(pw, args)); 641 } 642 643 @Override dumpProto(@otNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args)644 public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) { 645 List<QsTileState> data = mTiles.values().stream() 646 .map(QSTile::getState) 647 .map(TileStateToProtoKt::toProto) 648 .filter(Objects::nonNull) 649 .collect(Collectors.toList()); 650 651 systemUIProtoDump.tiles = data.toArray(new QsTileState[0]); 652 } 653 } 654