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