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