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