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