• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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