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.Log; 32 33 import com.android.systemui.Dependency; 34 import com.android.systemui.DumpController; 35 import com.android.systemui.Dumpable; 36 import com.android.systemui.R; 37 import com.android.systemui.SysUiServiceProvider; 38 import com.android.systemui.plugins.PluginListener; 39 import com.android.systemui.plugins.qs.QSFactory; 40 import com.android.systemui.plugins.qs.QSTile; 41 import com.android.systemui.plugins.qs.QSTileView; 42 import com.android.systemui.qs.external.CustomTile; 43 import com.android.systemui.qs.external.TileLifecycleManager; 44 import com.android.systemui.qs.external.TileServices; 45 import com.android.systemui.qs.tileimpl.QSFactoryImpl; 46 import com.android.systemui.shared.plugins.PluginManager; 47 import com.android.systemui.statusbar.phone.AutoTileManager; 48 import com.android.systemui.statusbar.phone.StatusBar; 49 import com.android.systemui.statusbar.phone.StatusBarIconController; 50 import com.android.systemui.tuner.TunerService; 51 import com.android.systemui.tuner.TunerService.Tunable; 52 import com.android.systemui.util.leak.GarbageMonitor; 53 54 import java.io.FileDescriptor; 55 import java.io.PrintWriter; 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collection; 59 import java.util.LinkedHashMap; 60 import java.util.List; 61 import java.util.function.Predicate; 62 63 import javax.inject.Inject; 64 import javax.inject.Named; 65 import javax.inject.Provider; 66 import javax.inject.Singleton; 67 68 /** Platform implementation of the quick settings tile host **/ 69 @Singleton 70 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, Dumpable { 71 private static final String TAG = "QSTileHost"; 72 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 73 74 public static final String TILES_SETTING = Secure.QS_TILES; 75 76 private final Context mContext; 77 private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>(); 78 protected final ArrayList<String> mTileSpecs = new ArrayList<>(); 79 private final TileServices mServices; 80 private final TunerService mTunerService; 81 private final PluginManager mPluginManager; 82 private final DumpController mDumpController; 83 84 private final List<Callback> mCallbacks = new ArrayList<>(); 85 private AutoTileManager mAutoTiles; 86 private final StatusBarIconController mIconController; 87 private final ArrayList<QSFactory> mQsFactories = new ArrayList<>(); 88 private int mCurrentUser; 89 private StatusBar mStatusBar; 90 91 @Inject QSTileHost(Context context, StatusBarIconController iconController, QSFactoryImpl defaultFactory, @Named(Dependency.MAIN_HANDLER_NAME) Handler mainHandler, @Named(Dependency.BG_LOOPER_NAME) Looper bgLooper, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, DumpController dumpController)92 public QSTileHost(Context context, 93 StatusBarIconController iconController, 94 QSFactoryImpl defaultFactory, 95 @Named(Dependency.MAIN_HANDLER_NAME) Handler mainHandler, 96 @Named(Dependency.BG_LOOPER_NAME) Looper bgLooper, 97 PluginManager pluginManager, 98 TunerService tunerService, 99 Provider<AutoTileManager> autoTiles, 100 DumpController dumpController) { 101 mIconController = iconController; 102 mContext = context; 103 mTunerService = tunerService; 104 mPluginManager = pluginManager; 105 mDumpController = dumpController; 106 107 mServices = new TileServices(this, bgLooper); 108 109 defaultFactory.setHost(this); 110 mQsFactories.add(defaultFactory); 111 pluginManager.addPluginListener(this, QSFactory.class, true); 112 mDumpController.addListener(this); 113 114 mainHandler.post(() -> { 115 // This is technically a hack to avoid circular dependency of 116 // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation 117 // finishes before creating any tiles. 118 tunerService.addTunable(this, TILES_SETTING); 119 // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. 120 mAutoTiles = autoTiles.get(); 121 }); 122 } 123 getIconController()124 public StatusBarIconController getIconController() { 125 return mIconController; 126 } 127 destroy()128 public void destroy() { 129 mTiles.values().forEach(tile -> tile.destroy()); 130 mAutoTiles.destroy(); 131 mTunerService.removeTunable(this); 132 mServices.destroy(); 133 mPluginManager.removePluginListener(this); 134 mDumpController.removeListener(this); 135 } 136 137 @Override onPluginConnected(QSFactory plugin, Context pluginContext)138 public void onPluginConnected(QSFactory plugin, Context pluginContext) { 139 // Give plugins priority over creation so they can override if they wish. 140 mQsFactories.add(0, plugin); 141 String value = mTunerService.getValue(TILES_SETTING); 142 // Force remove and recreate of all tiles. 143 onTuningChanged(TILES_SETTING, ""); 144 onTuningChanged(TILES_SETTING, value); 145 } 146 147 @Override onPluginDisconnected(QSFactory plugin)148 public void onPluginDisconnected(QSFactory plugin) { 149 mQsFactories.remove(plugin); 150 // Force remove and recreate of all tiles. 151 String value = mTunerService.getValue(TILES_SETTING); 152 onTuningChanged(TILES_SETTING, ""); 153 onTuningChanged(TILES_SETTING, value); 154 } 155 156 @Override addCallback(Callback callback)157 public void addCallback(Callback callback) { 158 mCallbacks.add(callback); 159 } 160 161 @Override removeCallback(Callback callback)162 public void removeCallback(Callback callback) { 163 mCallbacks.remove(callback); 164 } 165 166 @Override getTiles()167 public Collection<QSTile> getTiles() { 168 return mTiles.values(); 169 } 170 171 @Override warn(String message, Throwable t)172 public void warn(String message, Throwable t) { 173 // already logged 174 } 175 176 @Override collapsePanels()177 public void collapsePanels() { 178 if (mStatusBar == null) { 179 mStatusBar = SysUiServiceProvider.getComponent(mContext, StatusBar.class); 180 } 181 mStatusBar.postAnimateCollapsePanels(); 182 } 183 184 @Override forceCollapsePanels()185 public void forceCollapsePanels() { 186 if (mStatusBar == null) { 187 mStatusBar = SysUiServiceProvider.getComponent(mContext, StatusBar.class); 188 } 189 mStatusBar.postAnimateForceCollapsePanels(); 190 } 191 192 @Override openPanels()193 public void openPanels() { 194 if (mStatusBar == null) { 195 mStatusBar = SysUiServiceProvider.getComponent(mContext, StatusBar.class); 196 } 197 mStatusBar.postAnimateOpenPanels(); 198 } 199 200 @Override getContext()201 public Context getContext() { 202 return mContext; 203 } 204 205 getTileServices()206 public TileServices getTileServices() { 207 return mServices; 208 } 209 indexOf(String spec)210 public int indexOf(String spec) { 211 return mTileSpecs.indexOf(spec); 212 } 213 214 @Override onTuningChanged(String key, String newValue)215 public void onTuningChanged(String key, String newValue) { 216 if (!TILES_SETTING.equals(key)) { 217 return; 218 } 219 if (DEBUG) Log.d(TAG, "Recreating tiles"); 220 if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { 221 newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); 222 } 223 final List<String> tileSpecs = loadTileSpecs(mContext, newValue); 224 int currentUser = ActivityManager.getCurrentUser(); 225 if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; 226 mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( 227 tile -> { 228 if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); 229 tile.getValue().destroy(); 230 }); 231 final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>(); 232 for (String tileSpec : tileSpecs) { 233 QSTile tile = mTiles.get(tileSpec); 234 if (tile != null && (!(tile instanceof CustomTile) 235 || ((CustomTile) tile).getUser() == currentUser)) { 236 if (tile.isAvailable()) { 237 if (DEBUG) Log.d(TAG, "Adding " + tile); 238 tile.removeCallbacks(); 239 if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) { 240 tile.userSwitch(currentUser); 241 } 242 newTiles.put(tileSpec, tile); 243 } else { 244 tile.destroy(); 245 } 246 } else { 247 if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); 248 try { 249 tile = createTile(tileSpec); 250 if (tile != null) { 251 if (tile.isAvailable()) { 252 tile.setTileSpec(tileSpec); 253 newTiles.put(tileSpec, tile); 254 } else { 255 tile.destroy(); 256 } 257 } 258 } catch (Throwable t) { 259 Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); 260 } 261 } 262 } 263 mCurrentUser = currentUser; 264 List<String> currentSpecs = new ArrayList(mTileSpecs); 265 mTileSpecs.clear(); 266 mTileSpecs.addAll(tileSpecs); 267 mTiles.clear(); 268 mTiles.putAll(newTiles); 269 if (newTiles.isEmpty() && !tileSpecs.isEmpty()) { 270 // If we didn't manage to create any tiles, set it to empty (default) 271 if (DEBUG) Log.d(TAG, "No valid tiles on tuning changed. Setting to default."); 272 changeTiles(currentSpecs, loadTileSpecs(mContext, "")); 273 } else { 274 for (int i = 0; i < mCallbacks.size(); i++) { 275 mCallbacks.get(i).onTilesChanged(); 276 } 277 } 278 } 279 280 @Override removeTile(String spec)281 public void removeTile(String spec) { 282 changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)); 283 } 284 285 @Override unmarkTileAsAutoAdded(String spec)286 public void unmarkTileAsAutoAdded(String spec) { 287 if (mAutoTiles != null) mAutoTiles.unmarkTileAsAutoAdded(spec); 288 } 289 addTile(String spec)290 public void addTile(String spec) { 291 changeTileSpecs(tileSpecs-> tileSpecs.add(spec)); 292 } 293 changeTileSpecs(Predicate<List<String>> changeFunction)294 private void changeTileSpecs(Predicate<List<String>> changeFunction) { 295 final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(), 296 TILES_SETTING, ActivityManager.getCurrentUser()); 297 final List<String> tileSpecs = loadTileSpecs(mContext, setting); 298 if (changeFunction.test(tileSpecs)) { 299 Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, 300 TextUtils.join(",", tileSpecs), ActivityManager.getCurrentUser()); 301 } 302 } 303 addTile(ComponentName tile)304 public void addTile(ComponentName tile) { 305 List<String> newSpecs = new ArrayList<>(mTileSpecs); 306 newSpecs.add(0, CustomTile.toSpec(tile)); 307 changeTiles(mTileSpecs, newSpecs); 308 } 309 removeTile(ComponentName tile)310 public void removeTile(ComponentName tile) { 311 List<String> newSpecs = new ArrayList<>(mTileSpecs); 312 newSpecs.remove(CustomTile.toSpec(tile)); 313 changeTiles(mTileSpecs, newSpecs); 314 } 315 changeTiles(List<String> previousTiles, List<String> newTiles)316 public void changeTiles(List<String> previousTiles, List<String> newTiles) { 317 final int NP = previousTiles.size(); 318 final int NA = newTiles.size(); 319 for (int i = 0; i < NP; i++) { 320 String tileSpec = previousTiles.get(i); 321 if (!tileSpec.startsWith(CustomTile.PREFIX)) continue; 322 if (!newTiles.contains(tileSpec)) { 323 ComponentName component = CustomTile.getComponentFromSpec(tileSpec); 324 Intent intent = new Intent().setComponent(component); 325 TileLifecycleManager lifecycleManager = new TileLifecycleManager(new Handler(), 326 mContext, mServices, new Tile(), intent, 327 new UserHandle(ActivityManager.getCurrentUser())); 328 lifecycleManager.onStopListening(); 329 lifecycleManager.onTileRemoved(); 330 TileLifecycleManager.setTileAdded(mContext, component, false); 331 lifecycleManager.flushMessagesAndUnbind(); 332 } 333 } 334 if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles); 335 Secure.putStringForUser(getContext().getContentResolver(), QSTileHost.TILES_SETTING, 336 TextUtils.join(",", newTiles), ActivityManager.getCurrentUser()); 337 } 338 createTile(String tileSpec)339 public QSTile createTile(String tileSpec) { 340 for (int i = 0; i < mQsFactories.size(); i++) { 341 QSTile t = mQsFactories.get(i).createTile(tileSpec); 342 if (t != null) { 343 return t; 344 } 345 } 346 return null; 347 } 348 createTileView(QSTile tile, boolean collapsedView)349 public QSTileView createTileView(QSTile tile, boolean collapsedView) { 350 for (int i = 0; i < mQsFactories.size(); i++) { 351 QSTileView view = mQsFactories.get(i).createTileView(tile, collapsedView); 352 if (view != null) { 353 return view; 354 } 355 } 356 throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); 357 } 358 loadTileSpecs(Context context, String tileList)359 protected static List<String> loadTileSpecs(Context context, String tileList) { 360 final Resources res = context.getResources(); 361 final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); 362 if (TextUtils.isEmpty(tileList)) { 363 tileList = res.getString(R.string.quick_settings_tiles); 364 if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); 365 } else { 366 if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); 367 } 368 final ArrayList<String> tiles = new ArrayList<String>(); 369 boolean addedDefault = false; 370 for (String tile : tileList.split(",")) { 371 tile = tile.trim(); 372 if (tile.isEmpty()) continue; 373 if (tile.equals("default")) { 374 if (!addedDefault) { 375 tiles.addAll(Arrays.asList(defaultTileList.split(","))); 376 if (Build.IS_DEBUGGABLE 377 && GarbageMonitor.MemoryTile.ADD_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) { 378 tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); 379 } 380 addedDefault = true; 381 } 382 } else { 383 tiles.add(tile); 384 } 385 } 386 return tiles; 387 } 388 389 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)390 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 391 pw.println("QSTileHost:"); 392 mTiles.values().stream().filter(obj -> obj instanceof Dumpable) 393 .forEach(o -> ((Dumpable) o).dump(fd, pw, args)); 394 } 395 } 396