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