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