• 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.UserHandle;
22 import android.os.UserManager;
23 import android.provider.Settings.Secure;
24 import android.text.TextUtils;
25 import android.util.ArraySet;
26 import android.util.Log;
27 
28 import androidx.annotation.MainThread;
29 import androidx.annotation.Nullable;
30 
31 import com.android.internal.annotations.VisibleForTesting;
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.ProtoDumpable;
37 import com.android.systemui.R;
38 import com.android.systemui.dagger.SysUISingleton;
39 import com.android.systemui.dagger.qualifiers.Main;
40 import com.android.systemui.dump.DumpManager;
41 import com.android.systemui.dump.nano.SystemUIProtoDump;
42 import com.android.systemui.plugins.PluginListener;
43 import com.android.systemui.plugins.PluginManager;
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.TileServiceRequestController;
52 import com.android.systemui.qs.logging.QSLogger;
53 import com.android.systemui.qs.nano.QsTileState;
54 import com.android.systemui.settings.UserFileManager;
55 import com.android.systemui.settings.UserTracker;
56 import com.android.systemui.statusbar.phone.AutoTileManager;
57 import com.android.systemui.statusbar.phone.CentralSurfaces;
58 import com.android.systemui.tuner.TunerService;
59 import com.android.systemui.tuner.TunerService.Tunable;
60 import com.android.systemui.util.settings.SecureSettings;
61 
62 import org.jetbrains.annotations.NotNull;
63 
64 import java.io.PrintWriter;
65 import java.util.ArrayList;
66 import java.util.Collection;
67 import java.util.LinkedHashMap;
68 import java.util.List;
69 import java.util.Objects;
70 import java.util.Optional;
71 import java.util.Set;
72 import java.util.concurrent.Executor;
73 import java.util.function.Predicate;
74 import java.util.stream.Collectors;
75 
76 import javax.inject.Inject;
77 import javax.inject.Provider;
78 
79 /** Platform implementation of the quick settings tile host
80  *
81  * This class keeps track of the set of current tiles and is the in memory source of truth
82  * (ground truth is kept in {@link Secure#QS_TILES}). When the ground truth changes,
83  * {@link #onTuningChanged} will be called and the tiles will be re-created as needed.
84  *
85  * This class also provides the interface for adding/removing/changing tiles.
86  */
87 @SysUISingleton
88 public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable {
89     private static final String TAG = "QSTileHost";
90     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
91     private static final int MAX_QS_INSTANCE_ID = 1 << 20;
92 
93     // Shared prefs that hold tile lifecycle info.
94     @VisibleForTesting
95     static final String TILES = "tiles_prefs";
96 
97     private final Context mContext;
98     private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>();
99     private final ArrayList<String> mTileSpecs = new ArrayList<>();
100     private final TunerService mTunerService;
101     private final PluginManager mPluginManager;
102     private final DumpManager mDumpManager;
103     private final QSLogger mQSLogger;
104     private final UiEventLogger mUiEventLogger;
105     private final InstanceIdSequence mInstanceIdSequence;
106     private final CustomTileStatePersister mCustomTileStatePersister;
107     private final Executor mMainExecutor;
108     private final UserFileManager mUserFileManager;
109 
110     private final List<Callback> mCallbacks = new ArrayList<>();
111     @Nullable
112     private AutoTileManager mAutoTiles;
113     private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
114     private int mCurrentUser;
115     private final Optional<CentralSurfaces> mCentralSurfacesOptional;
116     private Context mUserContext;
117     private UserTracker mUserTracker;
118     private SecureSettings mSecureSettings;
119     // Keep track of whether mTilesList contains the same information as the Settings value.
120     // This is a performance optimization to reduce the number of blocking calls to Settings from
121     // main thread.
122     // This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged
123     private boolean mTilesListDirty = true;
124 
125     private final TileServiceRequestController mTileServiceRequestController;
126     private TileLifecycleManager.Factory mTileLifeCycleManagerFactory;
127 
128     @Inject
QSTileHost(Context context, QSFactory defaultFactory, @Main Executor mainExecutor, PluginManager pluginManager, TunerService tunerService, Provider<AutoTileManager> autoTiles, DumpManager dumpManager, Optional<CentralSurfaces> centralSurfacesOptional, QSLogger qsLogger, UiEventLogger uiEventLogger, UserTracker userTracker, SecureSettings secureSettings, CustomTileStatePersister customTileStatePersister, TileServiceRequestController.Builder tileServiceRequestControllerBuilder, TileLifecycleManager.Factory tileLifecycleManagerFactory, UserFileManager userFileManager )129     public QSTileHost(Context context,
130             QSFactory defaultFactory,
131             @Main Executor mainExecutor,
132             PluginManager pluginManager,
133             TunerService tunerService,
134             Provider<AutoTileManager> autoTiles,
135             DumpManager dumpManager,
136             Optional<CentralSurfaces> centralSurfacesOptional,
137             QSLogger qsLogger,
138             UiEventLogger uiEventLogger,
139             UserTracker userTracker,
140             SecureSettings secureSettings,
141             CustomTileStatePersister customTileStatePersister,
142             TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
143             TileLifecycleManager.Factory tileLifecycleManagerFactory,
144             UserFileManager userFileManager
145     ) {
146         mContext = context;
147         mUserContext = context;
148         mTunerService = tunerService;
149         mPluginManager = pluginManager;
150         mDumpManager = dumpManager;
151         mQSLogger = qsLogger;
152         mUiEventLogger = uiEventLogger;
153         mMainExecutor = mainExecutor;
154         mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this);
155         mTileLifeCycleManagerFactory = tileLifecycleManagerFactory;
156         mUserFileManager = userFileManager;
157 
158         mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID);
159         mCentralSurfacesOptional = centralSurfacesOptional;
160 
161         mQsFactories.add(defaultFactory);
162         pluginManager.addPluginListener(this, QSFactory.class, true);
163         mDumpManager.registerDumpable(TAG, this);
164         mUserTracker = userTracker;
165         mSecureSettings = secureSettings;
166         mCustomTileStatePersister = customTileStatePersister;
167 
168         mainExecutor.execute(() -> {
169             // This is technically a hack to avoid circular dependency of
170             // QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation
171             // finishes before creating any tiles.
172             tunerService.addTunable(this, TILES_SETTING);
173             // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
174             mAutoTiles = autoTiles.get();
175             mTileServiceRequestController.init();
176         });
177     }
178 
179     @Override
getNewInstanceId()180     public InstanceId getNewInstanceId() {
181         return mInstanceIdSequence.newInstanceId();
182     }
183 
destroy()184     public void destroy() {
185         mTiles.values().forEach(tile -> tile.destroy());
186         mAutoTiles.destroy();
187         mTunerService.removeTunable(this);
188         mPluginManager.removePluginListener(this);
189         mDumpManager.unregisterDumpable(TAG);
190         mTileServiceRequestController.destroy();
191     }
192 
193     @Override
onPluginConnected(QSFactory plugin, Context pluginContext)194     public void onPluginConnected(QSFactory plugin, Context pluginContext) {
195         // Give plugins priority over creation so they can override if they wish.
196         mQsFactories.add(0, plugin);
197         String value = mTunerService.getValue(TILES_SETTING);
198         // Force remove and recreate of all tiles.
199         onTuningChanged(TILES_SETTING, "");
200         onTuningChanged(TILES_SETTING, value);
201     }
202 
203     @Override
onPluginDisconnected(QSFactory plugin)204     public void onPluginDisconnected(QSFactory plugin) {
205         mQsFactories.remove(plugin);
206         // Force remove and recreate of all tiles.
207         String value = mTunerService.getValue(TILES_SETTING);
208         onTuningChanged(TILES_SETTING, "");
209         onTuningChanged(TILES_SETTING, value);
210     }
211 
212     @Override
getUiEventLogger()213     public UiEventLogger getUiEventLogger() {
214         return mUiEventLogger;
215     }
216 
217     @Override
addCallback(Callback callback)218     public void addCallback(Callback callback) {
219         mCallbacks.add(callback);
220     }
221 
222     @Override
removeCallback(Callback callback)223     public void removeCallback(Callback callback) {
224         mCallbacks.remove(callback);
225     }
226 
227     @Override
getTiles()228     public Collection<QSTile> getTiles() {
229         return mTiles.values();
230     }
231 
232     @Override
warn(String message, Throwable t)233     public void warn(String message, Throwable t) {
234         // already logged
235     }
236 
237     @Override
collapsePanels()238     public void collapsePanels() {
239         mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateCollapsePanels);
240     }
241 
242     @Override
forceCollapsePanels()243     public void forceCollapsePanels() {
244         mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateForceCollapsePanels);
245     }
246 
247     @Override
openPanels()248     public void openPanels() {
249         mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateOpenPanels);
250     }
251 
252     @Override
getContext()253     public Context getContext() {
254         return mContext;
255     }
256 
257     @Override
getUserContext()258     public Context getUserContext() {
259         return mUserContext;
260     }
261 
262     @Override
getUserId()263     public int getUserId() {
264         return mCurrentUser;
265     }
266 
indexOf(String spec)267     public int indexOf(String spec) {
268         return mTileSpecs.indexOf(spec);
269     }
270 
271     /**
272      * Whenever the Secure Setting keeping track of the current tiles changes (or upon start) this
273      * will be called with the new value of the setting.
274      *
275      * This method will do the following:
276      * <ol>
277      *     <li>Destroy any existing tile that's not one of the current tiles (in the setting)</li>
278      *     <li>Create new tiles for those that don't already exist. If this tiles end up being
279      *         not available, they'll also be destroyed.</li>
280      *     <li>Save the resolved list of tiles (current tiles that are available) into the setting.
281      *         This means that after this call ends, the tiles in the Setting, {@link #mTileSpecs},
282      *         and visible tiles ({@link #mTiles}) must match.
283      *         </li>
284      * </ol>
285      *
286      * Additionally, if the user has changed, it'll do the following:
287      * <ul>
288      *     <li>Change the user for SystemUI tiles: {@link QSTile#userSwitch}.</li>
289      *     <li>Destroy any {@link CustomTile} and recreate it for the new user.</li>
290      * </ul>
291      *
292      * This happens in main thread as {@link com.android.systemui.tuner.TunerServiceImpl} dispatches
293      * in main thread.
294      *
295      * @see QSTile#isAvailable
296      */
297     @MainThread
298     @Override
onTuningChanged(String key, String newValue)299     public void onTuningChanged(String key, String newValue) {
300         if (!TILES_SETTING.equals(key)) {
301             return;
302         }
303         if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
304             newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
305         }
306         final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
307         int currentUser = mUserTracker.getUserId();
308         if (currentUser != mCurrentUser) {
309             mUserContext = mUserTracker.getUserContext();
310             if (mAutoTiles != null) {
311                 mAutoTiles.changeUser(UserHandle.of(currentUser));
312             }
313         }
314         if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
315         Log.d(TAG, "Recreating tiles: " + tileSpecs);
316         mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
317                 tile -> {
318                     Log.d(TAG, "Destroying tile: " + tile.getKey());
319                     mQSLogger.logTileDestroyed(tile.getKey(), "Tile removed");
320                     tile.getValue().destroy();
321                 });
322         final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
323         for (String tileSpec : tileSpecs) {
324             QSTile tile = mTiles.get(tileSpec);
325             if (tile != null && (!(tile instanceof CustomTile)
326                     || ((CustomTile) tile).getUser() == currentUser)) {
327                 if (tile.isAvailable()) {
328                     if (DEBUG) Log.d(TAG, "Adding " + tile);
329                     tile.removeCallbacks();
330                     if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
331                         tile.userSwitch(currentUser);
332                     }
333                     newTiles.put(tileSpec, tile);
334                     mQSLogger.logTileAdded(tileSpec);
335                 } else {
336                     tile.destroy();
337                     Log.d(TAG, "Destroying not available tile: " + tileSpec);
338                     mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
339                 }
340             } else {
341                 // This means that the tile is a CustomTile AND the user is different, so let's
342                 // destroy it
343                 if (tile != null) {
344                     tile.destroy();
345                     Log.d(TAG, "Destroying tile for wrong user: " + tileSpec);
346                     mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user");
347                 }
348                 Log.d(TAG, "Creating tile: " + tileSpec);
349                 try {
350                     tile = createTile(tileSpec);
351                     if (tile != null) {
352                         tile.setTileSpec(tileSpec);
353                         if (tile.isAvailable()) {
354                             newTiles.put(tileSpec, tile);
355                             mQSLogger.logTileAdded(tileSpec);
356                         } else {
357                             tile.destroy();
358                             Log.d(TAG, "Destroying not available tile: " + tileSpec);
359                             mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
360                         }
361                     } else {
362                         Log.d(TAG, "No factory for a spec: " + tileSpec);
363                     }
364                 } catch (Throwable t) {
365                     Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
366                 }
367             }
368         }
369         mCurrentUser = currentUser;
370         List<String> currentSpecs = new ArrayList<>(mTileSpecs);
371         mTileSpecs.clear();
372         mTileSpecs.addAll(newTiles.keySet()); // Only add the valid (available) tiles.
373         mTiles.clear();
374         mTiles.putAll(newTiles);
375         if (newTiles.isEmpty() && !tileSpecs.isEmpty()) {
376             // If we didn't manage to create any tiles, set it to empty (default)
377             Log.d(TAG, "No valid tiles on tuning changed. Setting to default.");
378             changeTilesByUser(currentSpecs, loadTileSpecs(mContext, ""));
379         } else {
380             String resolvedTiles = TextUtils.join(",", mTileSpecs);
381             if (!resolvedTiles.equals(newValue)) {
382                 // If the resolved tiles (those we actually ended up with) are different than
383                 // the ones that are in the setting, update the Setting.
384                 saveTilesToSettings(mTileSpecs);
385             }
386             mTilesListDirty = false;
387             for (int i = 0; i < mCallbacks.size(); i++) {
388                 mCallbacks.get(i).onTilesChanged();
389             }
390         }
391     }
392 
393     /**
394      * Only use with [CustomTile] if the tile doesn't exist anymore (and therefore doesn't need
395      * its lifecycle terminated).
396      */
397     @Override
removeTile(String spec)398     public void removeTile(String spec) {
399         if (spec.startsWith(CustomTile.PREFIX)) {
400             // If the tile is removed (due to it not actually existing), mark it as removed. That
401             // way it will be marked as newly added if it appears in the future.
402             setTileAdded(CustomTile.getComponentFromSpec(spec), mCurrentUser, false);
403         }
404         mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)));
405     }
406 
407     /**
408      * Remove many tiles at once.
409      *
410      * It will only save to settings once (as opposed to {@link QSTileHost#removeTileByUser} called
411      * multiple times).
412      */
413     @Override
removeTiles(Collection<String> specs)414     public void removeTiles(Collection<String> specs) {
415         mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs)));
416     }
417 
418     /**
419      * Add a tile to the end
420      *
421      * @param spec string matching a pre-defined tilespec
422      */
addTile(String spec)423     public void addTile(String spec) {
424         addTile(spec, POSITION_AT_END);
425     }
426 
427     @Override
addTile(String spec, int requestPosition)428     public void addTile(String spec, int requestPosition) {
429         mMainExecutor.execute(() ->
430                 changeTileSpecs(tileSpecs -> {
431                     if (tileSpecs.contains(spec)) return false;
432 
433                     int size = tileSpecs.size();
434                     if (requestPosition == POSITION_AT_END || requestPosition >= size) {
435                         tileSpecs.add(spec);
436                     } else {
437                         tileSpecs.add(requestPosition, spec);
438                     }
439                     return true;
440                 })
441         );
442     }
443 
444     // When calling this, you may want to modify mTilesListDirty accordingly.
445     @MainThread
saveTilesToSettings(List<String> tileSpecs)446     private void saveTilesToSettings(List<String> tileSpecs) {
447         mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
448                 null /* tag */, false /* default */, mCurrentUser,
449                 true /* overrideable by restore */);
450     }
451 
452     @MainThread
changeTileSpecs(Predicate<List<String>> changeFunction)453     private void changeTileSpecs(Predicate<List<String>> changeFunction) {
454         final List<String> tileSpecs;
455         if (!mTilesListDirty) {
456             tileSpecs = new ArrayList<>(mTileSpecs);
457         } else {
458             tileSpecs = loadTileSpecs(mContext,
459                     mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser));
460         }
461         if (changeFunction.test(tileSpecs)) {
462             mTilesListDirty = true;
463             saveTilesToSettings(tileSpecs);
464         }
465     }
466 
467     @Override
addTile(ComponentName tile)468     public void addTile(ComponentName tile) {
469         addTile(tile, /* end */ false);
470     }
471 
472     @Override
addTile(ComponentName tile, boolean end)473     public void addTile(ComponentName tile, boolean end) {
474         String spec = CustomTile.toSpec(tile);
475         addTile(spec, end ? POSITION_AT_END : 0);
476     }
477 
478     /**
479      * This will call through {@link #changeTilesByUser}. It should only be used when a tile is
480      * removed by a <b>user action</b> like {@code adb}.
481      */
482     @Override
removeTileByUser(ComponentName tile)483     public void removeTileByUser(ComponentName tile) {
484         mMainExecutor.execute(() -> {
485             List<String> newSpecs = new ArrayList<>(mTileSpecs);
486             if (newSpecs.remove(CustomTile.toSpec(tile))) {
487                 changeTilesByUser(mTileSpecs, newSpecs);
488             }
489         });
490     }
491 
492     /**
493      * Change the tiles triggered by the user editing.
494      * <p>
495      * This is not called on device start, or on user change.
496      *
497      * {@link android.service.quicksettings.TileService#onTileRemoved} will be called for tiles
498      * that are removed.
499      */
500     @MainThread
501     @Override
changeTilesByUser(List<String> previousTiles, List<String> newTiles)502     public void changeTilesByUser(List<String> previousTiles, List<String> newTiles) {
503         final List<String> copy = new ArrayList<>(previousTiles);
504         final int NP = copy.size();
505         for (int i = 0; i < NP; i++) {
506             String tileSpec = copy.get(i);
507             if (!tileSpec.startsWith(CustomTile.PREFIX)) continue;
508             if (!newTiles.contains(tileSpec)) {
509                 ComponentName component = CustomTile.getComponentFromSpec(tileSpec);
510                 Intent intent = new Intent().setComponent(component);
511                 TileLifecycleManager lifecycleManager = mTileLifeCycleManagerFactory.create(
512                         intent, new UserHandle(mCurrentUser));
513                 lifecycleManager.onStopListening();
514                 lifecycleManager.onTileRemoved();
515                 mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
516                 setTileAdded(component, mCurrentUser, false);
517                 lifecycleManager.flushMessagesAndUnbind();
518             }
519         }
520         if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles);
521         mTilesListDirty = true;
522         saveTilesToSettings(newTiles);
523     }
524 
525     @Nullable
526     @Override
createTile(String tileSpec)527     public QSTile createTile(String tileSpec) {
528         for (int i = 0; i < mQsFactories.size(); i++) {
529             QSTile t = mQsFactories.get(i).createTile(tileSpec);
530             if (t != null) {
531                 return t;
532             }
533         }
534         return null;
535     }
536 
537     @Override
createTileView(Context themedContext, QSTile tile, boolean collapsedView)538     public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) {
539         for (int i = 0; i < mQsFactories.size(); i++) {
540             QSTileView view = mQsFactories.get(i)
541                     .createTileView(themedContext, tile, collapsedView);
542             if (view != null) {
543                 return view;
544             }
545         }
546         throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec());
547     }
548 
549     /**
550      * Check if a particular {@link CustomTile} has been added for a user and has not been removed
551      * since.
552      * @param componentName the {@link ComponentName} of the
553      *                      {@link android.service.quicksettings.TileService} associated with the
554      *                      tile.
555      * @param userId the user to check
556      */
557     @Override
isTileAdded(ComponentName componentName, int userId)558     public boolean isTileAdded(ComponentName componentName, int userId) {
559         return mUserFileManager
560                 .getSharedPreferences(TILES, 0, userId)
561                 .getBoolean(componentName.flattenToString(), false);
562     }
563 
564     /**
565      * Persists whether a particular {@link CustomTile} has been added and it's currently in the
566      * set of selected tiles ({@link #mTiles}.
567      * @param componentName the {@link ComponentName} of the
568      *                      {@link android.service.quicksettings.TileService} associated
569      *                      with the tile.
570      * @param userId the user for this tile
571      * @param added {@code true} if the tile is being added, {@code false} otherwise
572      */
573     @Override
setTileAdded(ComponentName componentName, int userId, boolean added)574     public void setTileAdded(ComponentName componentName, int userId, boolean added) {
575         mUserFileManager.getSharedPreferences(TILES, 0, userId)
576                 .edit()
577                 .putBoolean(componentName.flattenToString(), added)
578                 .apply();
579     }
580 
581     @Override
getSpecs()582     public List<String> getSpecs() {
583         return mTileSpecs;
584     }
585 
loadTileSpecs(Context context, String tileList)586     protected static List<String> loadTileSpecs(Context context, String tileList) {
587         final Resources res = context.getResources();
588 
589         if (TextUtils.isEmpty(tileList)) {
590             tileList = res.getString(R.string.quick_settings_tiles);
591             if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList);
592         } else {
593             if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
594         }
595         final ArrayList<String> tiles = new ArrayList<String>();
596         boolean addedDefault = false;
597         Set<String> addedSpecs = new ArraySet<>();
598         for (String tile : tileList.split(",")) {
599             tile = tile.trim();
600             if (tile.isEmpty()) continue;
601             if (tile.equals("default")) {
602                 if (!addedDefault) {
603                     List<String> defaultSpecs = QSHost.getDefaultSpecs(context);
604                     for (String spec : defaultSpecs) {
605                         if (!addedSpecs.contains(spec)) {
606                             tiles.add(spec);
607                             addedSpecs.add(spec);
608                         }
609                     }
610                     addedDefault = true;
611                 }
612             } else {
613                 if (!addedSpecs.contains(tile)) {
614                     tiles.add(tile);
615                     addedSpecs.add(tile);
616                 }
617             }
618         }
619 
620         if (!tiles.contains("internet")) {
621             if (tiles.contains("wifi")) {
622                 // Replace the WiFi with Internet, and remove the Cell
623                 tiles.set(tiles.indexOf("wifi"), "internet");
624                 tiles.remove("cell");
625             } else if (tiles.contains("cell")) {
626                 // Replace the Cell with Internet
627                 tiles.set(tiles.indexOf("cell"), "internet");
628             }
629         } else {
630             tiles.remove("wifi");
631             tiles.remove("cell");
632         }
633         return tiles;
634     }
635 
636     @Override
dump(PrintWriter pw, String[] args)637     public void dump(PrintWriter pw, String[] args) {
638         pw.println("QSTileHost:");
639         mTiles.values().stream().filter(obj -> obj instanceof Dumpable)
640                 .forEach(o -> ((Dumpable) o).dump(pw, args));
641     }
642 
643     @Override
dumpProto(@otNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args)644     public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) {
645         List<QsTileState> data = mTiles.values().stream()
646                 .map(QSTile::getState)
647                 .map(TileStateToProtoKt::toProto)
648                 .filter(Objects::nonNull)
649                 .collect(Collectors.toList());
650 
651         systemUIProtoDump.tiles = data.toArray(new QsTileState[0]);
652     }
653 }
654