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