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.Build; 22 import android.os.Handler; 23 import android.os.Looper; 24 import android.os.UserHandle; 25 import android.os.UserManager; 26 import android.provider.Settings.Secure; 27 import android.service.quicksettings.Tile; 28 import android.text.TextUtils; 29 import android.util.ArraySet; 30 import android.util.Log; 31 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.R; 37 import com.android.systemui.broadcast.BroadcastDispatcher; 38 import com.android.systemui.dagger.SysUISingleton; 39 import com.android.systemui.dagger.qualifiers.Background; 40 import com.android.systemui.dagger.qualifiers.Main; 41 import com.android.systemui.dump.DumpManager; 42 import com.android.systemui.flags.FeatureFlags; 43 import com.android.systemui.plugins.PluginListener; 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.TileServices; 52 import com.android.systemui.qs.logging.QSLogger; 53 import com.android.systemui.settings.UserTracker; 54 import com.android.systemui.shared.plugins.PluginManager; 55 import com.android.systemui.statusbar.phone.AutoTileManager; 56 import com.android.systemui.statusbar.phone.StatusBar; 57 import com.android.systemui.statusbar.phone.StatusBarIconController; 58 import com.android.systemui.tuner.TunerService; 59 import com.android.systemui.tuner.TunerService.Tunable; 60 import com.android.systemui.util.leak.GarbageMonitor; 61 import com.android.systemui.util.settings.SecureSettings; 62 63 import java.io.FileDescriptor; 64 import java.io.PrintWriter; 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.Collection; 68 import java.util.LinkedHashMap; 69 import java.util.List; 70 import java.util.Optional; 71 import java.util.Set; 72 import java.util.function.Predicate; 73 74 import javax.inject.Inject; 75 import javax.inject.Provider; 76 77 /** Platform implementation of the quick settings tile host **/ 78 @SysUISingleton 79 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { 80 private static final String TAG = "QSTileHost"; 81 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 82 private static final int MAX_QS_INSTANCE_ID = 1 << 20; 83 84 public static final int POSITION_AT_END = -1; 85 public static final String TILES_SETTING = Secure.QS_TILES; 86 87 private final Context mContext; 88 private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>(); 89 protected final ArrayList<String> mTileSpecs = new ArrayList<>(); 90 private final TileServices mServices; 91 private final TunerService mTunerService; 92 private final PluginManager mPluginManager; 93 private final DumpManager mDumpManager; 94 private final BroadcastDispatcher mBroadcastDispatcher; 95 private final QSLogger mQSLogger; 96 private final UiEventLogger mUiEventLogger; 97 private final InstanceIdSequence mInstanceIdSequence; 98 private final CustomTileStatePersister mCustomTileStatePersister; 99 private final FeatureFlags mFeatureFlags; 100 101 private final List<Callback> mCallbacks = new ArrayList<>(); 102 private AutoTileManager mAutoTiles; 103 private final StatusBarIconController mIconController; 104 private final ArrayList<QSFactory> mQsFactories = new ArrayList<>(); 105 private int mCurrentUser; 106 private final Optional<StatusBar> mStatusBarOptional; 107 private Context mUserContext; 108 private UserTracker mUserTracker; 109 private SecureSettings mSecureSettings; 110 111 @Inject QSTileHost(Context context, StatusBarIconController iconController, QSFactory defaultFactory, @Main Handler mainHandler, @Background Looper bgLooper, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, DumpManager dumpManager, BroadcastDispatcher broadcastDispatcher, Optional<StatusBar> statusBarOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, FeatureFlags featureFlags )112 public QSTileHost(Context context, 113 StatusBarIconController iconController, 114 QSFactory defaultFactory, 115 @Main Handler mainHandler, 116 @Background Looper bgLooper, 117 PluginManager pluginManager, 118 TunerService tunerService, 119 Provider<AutoTileManager> autoTiles, 120 DumpManager dumpManager, 121 BroadcastDispatcher broadcastDispatcher, 122 Optional<StatusBar> statusBarOptional, 123 QSLogger qsLogger, 124 UiEventLogger uiEventLogger, 125 UserTracker userTracker, 126 SecureSettings secureSettings, 127 CustomTileStatePersister customTileStatePersister, 128 FeatureFlags featureFlags 129 ) { 130 mIconController = iconController; 131 mContext = context; 132 mUserContext = context; 133 mTunerService = tunerService; 134 mPluginManager = pluginManager; 135 mDumpManager = dumpManager; 136 mQSLogger = qsLogger; 137 mUiEventLogger = uiEventLogger; 138 mBroadcastDispatcher = broadcastDispatcher; 139 140 mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID); 141 mServices = new TileServices(this, bgLooper, mBroadcastDispatcher, userTracker); 142 mStatusBarOptional = statusBarOptional; 143 144 mQsFactories.add(defaultFactory); 145 pluginManager.addPluginListener(this, QSFactory.class, true); 146 mDumpManager.registerDumpable(TAG, this); 147 mUserTracker = userTracker; 148 mSecureSettings = secureSettings; 149 mCustomTileStatePersister = customTileStatePersister; 150 mFeatureFlags = featureFlags; 151 152 mainHandler.post(() -> { 153 // This is technically a hack to avoid circular dependency of 154 // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation 155 // finishes before creating any tiles. 156 tunerService.addTunable(this, TILES_SETTING); 157 // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. 158 mAutoTiles = autoTiles.get(); 159 }); 160 } 161 getIconController()162 public StatusBarIconController getIconController() { 163 return mIconController; 164 } 165 166 @Override getNewInstanceId()167 public InstanceId getNewInstanceId() { 168 return mInstanceIdSequence.newInstanceId(); 169 } 170 destroy()171 public void destroy() { 172 mTiles.values().forEach(tile -> tile.destroy()); 173 mAutoTiles.destroy(); 174 mTunerService.removeTunable(this); 175 mServices.destroy(); 176 mPluginManager.removePluginListener(this); 177 mDumpManager.unregisterDumpable(TAG); 178 } 179 180 @Override onPluginConnected(QSFactory plugin, Context pluginContext)181 public void onPluginConnected(QSFactory plugin, Context pluginContext) { 182 // Give plugins priority over creation so they can override if they wish. 183 mQsFactories.add(0, plugin); 184 String value = mTunerService.getValue(TILES_SETTING); 185 // Force remove and recreate of all tiles. 186 onTuningChanged(TILES_SETTING, ""); 187 onTuningChanged(TILES_SETTING, value); 188 } 189 190 @Override onPluginDisconnected(QSFactory plugin)191 public void onPluginDisconnected(QSFactory plugin) { 192 mQsFactories.remove(plugin); 193 // Force remove and recreate of all tiles. 194 String value = mTunerService.getValue(TILES_SETTING); 195 onTuningChanged(TILES_SETTING, ""); 196 onTuningChanged(TILES_SETTING, value); 197 } 198 199 @Override getUiEventLogger()200 public UiEventLogger getUiEventLogger() { 201 return mUiEventLogger; 202 } 203 204 @Override addCallback(Callback callback)205 public void addCallback(Callback callback) { 206 mCallbacks.add(callback); 207 } 208 209 @Override removeCallback(Callback callback)210 public void removeCallback(Callback callback) { 211 mCallbacks.remove(callback); 212 } 213 214 @Override getTiles()215 public Collection<QSTile> getTiles() { 216 return mTiles.values(); 217 } 218 219 @Override warn(String message, Throwable t)220 public void warn(String message, Throwable t) { 221 // already logged 222 } 223 224 @Override collapsePanels()225 public void collapsePanels() { 226 mStatusBarOptional.ifPresent(StatusBar::postAnimateCollapsePanels); 227 } 228 229 @Override forceCollapsePanels()230 public void forceCollapsePanels() { 231 mStatusBarOptional.ifPresent(StatusBar::postAnimateForceCollapsePanels); 232 } 233 234 @Override openPanels()235 public void openPanels() { 236 mStatusBarOptional.ifPresent(StatusBar::postAnimateOpenPanels); 237 } 238 239 @Override getContext()240 public Context getContext() { 241 return mContext; 242 } 243 244 @Override getUserContext()245 public Context getUserContext() { 246 return mUserContext; 247 } 248 249 @Override getUserId()250 public int getUserId() { 251 return mCurrentUser; 252 } 253 254 @Override getTileServices()255 public TileServices getTileServices() { 256 return mServices; 257 } 258 indexOf(String spec)259 public int indexOf(String spec) { 260 return mTileSpecs.indexOf(spec); 261 } 262 263 @Override onTuningChanged(String key, String newValue)264 public void onTuningChanged(String key, String newValue) { 265 if (!TILES_SETTING.equals(key)) { 266 return; 267 } 268 Log.d(TAG, "Recreating tiles"); 269 if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { 270 newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); 271 } 272 final List<String> tileSpecs = loadTileSpecs(mContext, newValue, mFeatureFlags); 273 int currentUser = mUserTracker.getUserId(); 274 if (currentUser != mCurrentUser) { 275 mUserContext = mUserTracker.getUserContext(); 276 if (mAutoTiles != null) { 277 mAutoTiles.changeUser(UserHandle.of(currentUser)); 278 } 279 } 280 if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; 281 mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( 282 tile -> { 283 Log.d(TAG, "Destroying tile: " + tile.getKey()); 284 mQSLogger.logTileDestroyed(tile.getKey(), "Tile removed"); 285 tile.getValue().destroy(); 286 }); 287 final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>(); 288 for (String tileSpec : tileSpecs) { 289 QSTile tile = mTiles.get(tileSpec); 290 if (tile != null && (!(tile instanceof CustomTile) 291 || ((CustomTile) tile).getUser() == currentUser)) { 292 if (tile.isAvailable()) { 293 if (DEBUG) Log.d(TAG, "Adding " + tile); 294 tile.removeCallbacks(); 295 if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) { 296 tile.userSwitch(currentUser); 297 } 298 newTiles.put(tileSpec, tile); 299 mQSLogger.logTileAdded(tileSpec); 300 } else { 301 tile.destroy(); 302 Log.d(TAG, "Destroying not available tile: " + tileSpec); 303 mQSLogger.logTileDestroyed(tileSpec, "Tile not available"); 304 } 305 } else { 306 // This means that the tile is a CustomTile AND the user is different, so let's 307 // destroy it 308 if (tile != null) { 309 tile.destroy(); 310 Log.d(TAG, "Destroying tile for wrong user: " + tileSpec); 311 mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user"); 312 } 313 Log.d(TAG, "Creating tile: " + tileSpec); 314 try { 315 tile = createTile(tileSpec); 316 if (tile != null) { 317 tile.setTileSpec(tileSpec); 318 if (tile.isAvailable()) { 319 newTiles.put(tileSpec, tile); 320 mQSLogger.logTileAdded(tileSpec); 321 } else { 322 tile.destroy(); 323 Log.d(TAG, "Destroying not available tile: " + tileSpec); 324 mQSLogger.logTileDestroyed(tileSpec, "Tile not available"); 325 } 326 } 327 } catch (Throwable t) { 328 Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); 329 } 330 } 331 } 332 mCurrentUser = currentUser; 333 List<String> currentSpecs = new ArrayList<>(mTileSpecs); 334 mTileSpecs.clear(); 335 mTileSpecs.addAll(tileSpecs); 336 mTiles.clear(); 337 mTiles.putAll(newTiles); 338 if (newTiles.isEmpty() && !tileSpecs.isEmpty()) { 339 // If we didn't manage to create any tiles, set it to empty (default) 340 Log.d(TAG, "No valid tiles on tuning changed. Setting to default."); 341 changeTiles(currentSpecs, loadTileSpecs(mContext, "", mFeatureFlags)); 342 } else { 343 for (int i = 0; i < mCallbacks.size(); i++) { 344 mCallbacks.get(i).onTilesChanged(); 345 } 346 } 347 } 348 349 @Override removeTile(String spec)350 public void removeTile(String spec) { 351 changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)); 352 } 353 354 /** 355 * Remove many tiles at once. 356 * 357 * It will only save to settings once (as opposed to {@link QSTileHost#removeTile} called 358 * multiple times). 359 */ 360 @Override removeTiles(Collection<String> specs)361 public void removeTiles(Collection<String> specs) { 362 changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs)); 363 } 364 365 @Override unmarkTileAsAutoAdded(String spec)366 public void unmarkTileAsAutoAdded(String spec) { 367 if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); 368 } 369 370 /** 371 * Add a tile to the end 372 * 373 * @param spec string matching a pre-defined tilespec 374 */ addTile(String spec)375 public void addTile(String spec) { 376 addTile(spec, POSITION_AT_END); 377 } 378 379 /** 380 * Add a tile into the requested spot, or at the end if the position is greater than the number 381 * of tiles. 382 * @param spec string matching a pre-defined tilespec 383 * @param requestPosition -1 for end, 0 for beginning, or X for insertion at position X 384 */ addTile(String spec, int requestPosition)385 public void addTile(String spec, int requestPosition) { 386 if (spec.equals("work")) Log.wtfStack(TAG, "Adding work tile"); 387 changeTileSpecs(tileSpecs -> { 388 if (tileSpecs.contains(spec)) return false; 389 390 int size = tileSpecs.size(); 391 if (requestPosition == POSITION_AT_END || requestPosition >= size) { 392 tileSpecs.add(spec); 393 } else { 394 tileSpecs.add(requestPosition, spec); 395 } 396 return true; 397 }); 398 } 399 saveTilesToSettings(List<String> tileSpecs)400 void saveTilesToSettings(List<String> tileSpecs) { 401 if (tileSpecs.contains("work")) Log.wtfStack(TAG, "Saving work tile"); 402 mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs), 403 null /* tag */, false /* default */, mCurrentUser, 404 true /* overrideable by restore */); 405 } 406 changeTileSpecs(Predicate<List<String>> changeFunction)407 private void changeTileSpecs(Predicate<List<String>> changeFunction) { 408 final String setting = mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser); 409 final List<String> tileSpecs = loadTileSpecs(mContext, setting, mFeatureFlags); 410 if (changeFunction.test(tileSpecs)) { 411 saveTilesToSettings(tileSpecs); 412 } 413 } 414 addTile(ComponentName tile)415 public void addTile(ComponentName tile) { 416 addTile(tile, /* end */ false); 417 } 418 419 /** 420 * Adds a custom tile to the set of current tiles. 421 * @param tile the component name of the {@link android.service.quicksettings.TileService} 422 * @param end if true, the tile will be added at the end. If false, at the beginning. 423 */ addTile(ComponentName tile, boolean end)424 public void addTile(ComponentName tile, boolean end) { 425 String spec = CustomTile.toSpec(tile); 426 if (!mTileSpecs.contains(spec)) { 427 List<String> newSpecs = new ArrayList<>(mTileSpecs); 428 if (end) { 429 newSpecs.add(spec); 430 } else { 431 newSpecs.add(0, spec); 432 } 433 changeTiles(mTileSpecs, newSpecs); 434 } 435 } 436 removeTile(ComponentName tile)437 public void removeTile(ComponentName tile) { 438 List<String> newSpecs = new ArrayList<>(mTileSpecs); 439 newSpecs.remove(CustomTile.toSpec(tile)); 440 changeTiles(mTileSpecs, newSpecs); 441 } 442 443 /** 444 * Change the tiles triggered by the user editing. 445 * <p> 446 * This is not called on device start, or on user change. 447 */ changeTiles(List<String> previousTiles, List<String> newTiles)448 public void changeTiles(List<String> previousTiles, List<String> newTiles) { 449 final List<String> copy = new ArrayList<>(previousTiles); 450 final int NP = copy.size(); 451 for (int i = 0; i < NP; i++) { 452 String tileSpec = copy.get(i); 453 if (!tileSpec.startsWith(CustomTile.PREFIX)) continue; 454 if (!newTiles.contains(tileSpec)) { 455 ComponentName component = CustomTile.getComponentFromSpec(tileSpec); 456 Intent intent = new Intent().setComponent(component); 457 TileLifecycleManager lifecycleManager = new TileLifecycleManager(new Handler(), 458 mContext, mServices, new Tile(), intent, 459 new UserHandle(mCurrentUser), 460 mBroadcastDispatcher); 461 lifecycleManager.onStopListening(); 462 lifecycleManager.onTileRemoved(); 463 mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser)); 464 TileLifecycleManager.setTileAdded(mContext, component, false); 465 lifecycleManager.flushMessagesAndUnbind(); 466 } 467 } 468 if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles); 469 saveTilesToSettings(newTiles); 470 } 471 createTile(String tileSpec)472 public QSTile createTile(String tileSpec) { 473 for (int i = 0; i < mQsFactories.size(); i++) { 474 QSTile t = mQsFactories.get(i).createTile(tileSpec); 475 if (t != null) { 476 return t; 477 } 478 } 479 return null; 480 } 481 482 /** 483 * Create a view for a tile, iterating over all possible {@link QSFactory}. 484 * 485 * @see QSFactory#createTileView 486 */ createTileView(Context themedContext, QSTile tile, boolean collapsedView)487 public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) { 488 for (int i = 0; i < mQsFactories.size(); i++) { 489 QSTileView view = mQsFactories.get(i) 490 .createTileView(themedContext, tile, collapsedView); 491 if (view != null) { 492 return view; 493 } 494 } 495 throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); 496 } 497 loadTileSpecs( Context context, String tileList, FeatureFlags featureFlags)498 protected static List<String> loadTileSpecs( 499 Context context, String tileList, FeatureFlags featureFlags) { 500 final Resources res = context.getResources(); 501 502 if (TextUtils.isEmpty(tileList)) { 503 tileList = res.getString(R.string.quick_settings_tiles); 504 if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); 505 } else { 506 if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); 507 } 508 final ArrayList<String> tiles = new ArrayList<String>(); 509 boolean addedDefault = false; 510 Set<String> addedSpecs = new ArraySet<>(); 511 for (String tile : tileList.split(",")) { 512 tile = tile.trim(); 513 if (tile.isEmpty()) continue; 514 if (tile.equals("default")) { 515 if (!addedDefault) { 516 List<String> defaultSpecs = getDefaultSpecs(context); 517 for (String spec : defaultSpecs) { 518 if (!addedSpecs.contains(spec)) { 519 tiles.add(spec); 520 addedSpecs.add(spec); 521 } 522 } 523 addedDefault = true; 524 } 525 } else { 526 if (!addedSpecs.contains(tile)) { 527 tiles.add(tile); 528 addedSpecs.add(tile); 529 } 530 } 531 } 532 if (featureFlags.isProviderModelSettingEnabled()) { 533 if (!tiles.contains("internet")) { 534 if (tiles.contains("wifi")) { 535 // Replace the WiFi with Internet, and remove the Cell 536 tiles.set(tiles.indexOf("wifi"), "internet"); 537 tiles.remove("cell"); 538 } else if (tiles.contains("cell")) { 539 // Replace the Cell with Internet 540 tiles.set(tiles.indexOf("cell"), "internet"); 541 } 542 } else { 543 tiles.remove("wifi"); 544 tiles.remove("cell"); 545 } 546 } 547 return tiles; 548 } 549 550 /** 551 * Returns the default QS tiles for the context. 552 * @param context the context to obtain the resources from 553 * @return a list of specs of the default tiles 554 */ getDefaultSpecs(Context context)555 public static List<String> getDefaultSpecs(Context context) { 556 final ArrayList<String> tiles = new ArrayList<String>(); 557 558 final Resources res = context.getResources(); 559 final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); 560 561 tiles.addAll(Arrays.asList(defaultTileList.split(","))); 562 if (Build.IS_DEBUGGABLE 563 && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) { 564 tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); 565 } 566 return tiles; 567 } 568 569 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)570 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 571 pw.println("QSTileHost:"); 572 mTiles.values().stream().filter(obj -> obj instanceof Dumpable) 573 .forEach(o -> ((Dumpable) o).dump(fd, pw, args)); 574 } 575 } 576