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.Handler; 23 import android.os.UserHandle; 24 import android.os.UserManager; 25 import android.provider.Settings; 26 import android.provider.Settings.Secure; 27 import android.service.quicksettings.Tile; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import com.android.systemui.Dependency; 32 import com.android.systemui.R; 33 import com.android.systemui.plugins.PluginListener; 34 import com.android.systemui.plugins.PluginManager; 35 import com.android.systemui.plugins.qs.QSFactory; 36 import com.android.systemui.plugins.qs.QSTileView; 37 import com.android.systemui.plugins.qs.QSTile; 38 import com.android.systemui.qs.external.CustomTile; 39 import com.android.systemui.qs.external.TileLifecycleManager; 40 import com.android.systemui.qs.external.TileServices; 41 import com.android.systemui.qs.tileimpl.QSFactoryImpl; 42 import com.android.systemui.statusbar.phone.AutoTileManager; 43 import com.android.systemui.statusbar.phone.StatusBar; 44 import com.android.systemui.statusbar.phone.StatusBarIconController; 45 import com.android.systemui.tuner.TunerService; 46 import com.android.systemui.tuner.TunerService.Tunable; 47 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collection; 51 import java.util.LinkedHashMap; 52 import java.util.List; 53 54 /** Platform implementation of the quick settings tile host **/ 55 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory> { 56 private static final String TAG = "QSTileHost"; 57 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 58 59 public static final String TILES_SETTING = Secure.QS_TILES; 60 61 private final Context mContext; 62 private final StatusBar mStatusBar; 63 private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>(); 64 protected final ArrayList<String> mTileSpecs = new ArrayList<>(); 65 private final TileServices mServices; 66 67 private final List<Callback> mCallbacks = new ArrayList<>(); 68 private final AutoTileManager mAutoTiles; 69 private final StatusBarIconController mIconController; 70 private final ArrayList<QSFactory> mQsFactories = new ArrayList<>(); 71 private int mCurrentUser; 72 73 public QSTileHost(Context context, StatusBar statusBar, 74 StatusBarIconController iconController) { 75 mIconController = iconController; 76 mContext = context; 77 mStatusBar = statusBar; 78 79 mServices = new TileServices(this, Dependency.get(Dependency.BG_LOOPER)); 80 81 mQsFactories.add(new QSFactoryImpl(this)); 82 Dependency.get(PluginManager.class).addPluginListener(this, QSFactory.class, true); 83 84 Dependency.get(TunerService.class).addTunable(this, TILES_SETTING); 85 // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. 86 mAutoTiles = new AutoTileManager(context, this); 87 } 88 89 public StatusBarIconController getIconController() { 90 return mIconController; 91 } 92 93 public void destroy() { 94 mTiles.values().forEach(tile -> tile.destroy()); 95 mAutoTiles.destroy(); 96 Dependency.get(TunerService.class).removeTunable(this); 97 mServices.destroy(); 98 Dependency.get(PluginManager.class).removePluginListener(this); 99 } 100 101 @Override 102 public void onPluginConnected(QSFactory plugin, Context pluginContext) { 103 // Give plugins priority over creation so they can override if they wish. 104 mQsFactories.add(0, plugin); 105 String value = Dependency.get(TunerService.class).getValue(TILES_SETTING); 106 // Force remove and recreate of all tiles. 107 onTuningChanged(TILES_SETTING, ""); 108 onTuningChanged(TILES_SETTING, value); 109 } 110 111 @Override 112 public void onPluginDisconnected(QSFactory plugin) { 113 mQsFactories.remove(plugin); 114 // Force remove and recreate of all tiles. 115 String value = Dependency.get(TunerService.class).getValue(TILES_SETTING); 116 onTuningChanged(TILES_SETTING, ""); 117 onTuningChanged(TILES_SETTING, value); 118 } 119 120 @Override 121 public void addCallback(Callback callback) { 122 mCallbacks.add(callback); 123 } 124 125 @Override 126 public void removeCallback(Callback callback) { 127 mCallbacks.remove(callback); 128 } 129 130 @Override 131 public Collection<QSTile> getTiles() { 132 return mTiles.values(); 133 } 134 135 @Override 136 public void warn(String message, Throwable t) { 137 // already logged 138 } 139 140 @Override 141 public void collapsePanels() { 142 mStatusBar.postAnimateCollapsePanels(); 143 } 144 145 @Override 146 public void forceCollapsePanels() { 147 mStatusBar.postAnimateForceCollapsePanels(); 148 } 149 150 @Override 151 public void openPanels() { 152 mStatusBar.postAnimateOpenPanels(); 153 } 154 155 @Override 156 public Context getContext() { 157 return mContext; 158 } 159 160 161 public TileServices getTileServices() { 162 return mServices; 163 } 164 165 public int indexOf(String spec) { 166 return mTileSpecs.indexOf(spec); 167 } 168 169 @Override 170 public void onTuningChanged(String key, String newValue) { 171 if (!TILES_SETTING.equals(key)) { 172 return; 173 } 174 if (DEBUG) Log.d(TAG, "Recreating tiles"); 175 if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { 176 newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); 177 } 178 final List<String> tileSpecs = loadTileSpecs(mContext, newValue); 179 int currentUser = ActivityManager.getCurrentUser(); 180 if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; 181 mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( 182 tile -> { 183 if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); 184 tile.getValue().destroy(); 185 }); 186 final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>(); 187 for (String tileSpec : tileSpecs) { 188 QSTile tile = mTiles.get(tileSpec); 189 if (tile != null && (!(tile instanceof CustomTile) 190 || ((CustomTile) tile).getUser() == currentUser)) { 191 if (tile.isAvailable()) { 192 if (DEBUG) Log.d(TAG, "Adding " + tile); 193 tile.removeCallbacks(); 194 if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) { 195 tile.userSwitch(currentUser); 196 } 197 newTiles.put(tileSpec, tile); 198 } else { 199 tile.destroy(); 200 } 201 } else { 202 if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); 203 try { 204 tile = createTile(tileSpec); 205 if (tile != null) { 206 if (tile.isAvailable()) { 207 tile.setTileSpec(tileSpec); 208 newTiles.put(tileSpec, tile); 209 } else { 210 tile.destroy(); 211 } 212 } 213 } catch (Throwable t) { 214 Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); 215 } 216 } 217 } 218 mCurrentUser = currentUser; 219 mTileSpecs.clear(); 220 mTileSpecs.addAll(tileSpecs); 221 mTiles.clear(); 222 mTiles.putAll(newTiles); 223 for (int i = 0; i < mCallbacks.size(); i++) { 224 mCallbacks.get(i).onTilesChanged(); 225 } 226 } 227 228 @Override 229 public void removeTile(String tileSpec) { 230 ArrayList<String> specs = new ArrayList<>(mTileSpecs); 231 specs.remove(tileSpec); 232 Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, 233 TextUtils.join(",", specs), ActivityManager.getCurrentUser()); 234 } 235 236 public void addTile(String spec) { 237 final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(), 238 TILES_SETTING, ActivityManager.getCurrentUser()); 239 final List<String> tileSpecs = loadTileSpecs(mContext, setting); 240 if (tileSpecs.contains(spec)) { 241 return; 242 } 243 tileSpecs.add(spec); 244 Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, 245 TextUtils.join(",", tileSpecs), ActivityManager.getCurrentUser()); 246 } 247 248 public void addTile(ComponentName tile) { 249 List<String> newSpecs = new ArrayList<>(mTileSpecs); 250 newSpecs.add(0, CustomTile.toSpec(tile)); 251 changeTiles(mTileSpecs, newSpecs); 252 } 253 254 public void removeTile(ComponentName tile) { 255 List<String> newSpecs = new ArrayList<>(mTileSpecs); 256 newSpecs.remove(CustomTile.toSpec(tile)); 257 changeTiles(mTileSpecs, newSpecs); 258 } 259 260 public void changeTiles(List<String> previousTiles, List<String> newTiles) { 261 final int NP = previousTiles.size(); 262 final int NA = newTiles.size(); 263 for (int i = 0; i < NP; i++) { 264 String tileSpec = previousTiles.get(i); 265 if (!tileSpec.startsWith(CustomTile.PREFIX)) continue; 266 if (!newTiles.contains(tileSpec)) { 267 ComponentName component = CustomTile.getComponentFromSpec(tileSpec); 268 Intent intent = new Intent().setComponent(component); 269 TileLifecycleManager lifecycleManager = new TileLifecycleManager(new Handler(), 270 mContext, mServices, new Tile(), intent, 271 new UserHandle(ActivityManager.getCurrentUser())); 272 lifecycleManager.onStopListening(); 273 lifecycleManager.onTileRemoved(); 274 TileLifecycleManager.setTileAdded(mContext, component, false); 275 lifecycleManager.flushMessagesAndUnbind(); 276 } 277 } 278 if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles); 279 Secure.putStringForUser(getContext().getContentResolver(), QSTileHost.TILES_SETTING, 280 TextUtils.join(",", newTiles), ActivityManager.getCurrentUser()); 281 } 282 283 public QSTile createTile(String tileSpec) { 284 for (int i = 0; i < mQsFactories.size(); i++) { 285 QSTile t = mQsFactories.get(i).createTile(tileSpec); 286 if (t != null) { 287 return t; 288 } 289 } 290 return null; 291 } 292 293 public QSTileView createTileView(QSTile tile, boolean collapsedView) { 294 for (int i = 0; i < mQsFactories.size(); i++) { 295 QSTileView view = mQsFactories.get(i).createTileView(tile, collapsedView); 296 if (view != null) { 297 return view; 298 } 299 } 300 throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); 301 } 302 303 protected List<String> loadTileSpecs(Context context, String tileList) { 304 final Resources res = context.getResources(); 305 final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); 306 if (tileList == null) { 307 tileList = res.getString(R.string.quick_settings_tiles); 308 if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); 309 } else { 310 if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); 311 } 312 final ArrayList<String> tiles = new ArrayList<String>(); 313 boolean addedDefault = false; 314 for (String tile : tileList.split(",")) { 315 tile = tile.trim(); 316 if (tile.isEmpty()) continue; 317 if (tile.equals("default")) { 318 if (!addedDefault) { 319 tiles.addAll(Arrays.asList(defaultTileList.split(","))); 320 addedDefault = true; 321 } 322 } else { 323 tiles.add(tile); 324 } 325 } 326 return tiles; 327 } 328 } 329