blob: 59b94b7c4bd42a28a24b1222e85ab81d15bb1b9c [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui.qs;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.Dumpable;
import com.android.systemui.ProtoDumpable;
import com.android.systemui.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.nano.SystemUIProtoDump;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.external.CustomTileStatePersister;
import com.android.systemui.qs.external.TileLifecycleManager;
import com.android.systemui.qs.external.TileServiceKey;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.qs.nano.QsTileState;
import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
import com.android.systemui.settings.UserFileManager;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.AutoTileManager;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.settings.SecureSettings;
import org.jetbrains.annotations.NotNull;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Provider;
/** Platform implementation of the quick settings tile host
*
* This class keeps track of the set of current tiles and is the in memory source of truth
* (ground truth is kept in {@link Secure#QS_TILES}). When the ground truth changes,
* {@link #onTuningChanged} will be called and the tiles will be re-created as needed.
*
* This class also provides the interface for adding/removing/changing tiles.
*/
@SysUISingleton
public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, ProtoDumpable,
PanelInteractor, CustomTileAddedRepository {
private static final String TAG = "QSTileHost";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Shared prefs that hold tile lifecycle info.
@VisibleForTesting
static final String TILES = "tiles_prefs";
private final Context mContext;
private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>();
private final ArrayList<String> mTileSpecs = new ArrayList<>();
private final TunerService mTunerService;
private final PluginManager mPluginManager;
private final QSLogger mQSLogger;
private final CustomTileStatePersister mCustomTileStatePersister;
private final Executor mMainExecutor;
private final UserFileManager mUserFileManager;
private final List<Callback> mCallbacks = new ArrayList<>();
@Nullable
private AutoTileManager mAutoTiles;
private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
private int mCurrentUser;
private final Optional<CentralSurfaces> mCentralSurfacesOptional;
private Context mUserContext;
private UserTracker mUserTracker;
private SecureSettings mSecureSettings;
// Keep track of whether mTilesList contains the same information as the Settings value.
// This is a performance optimization to reduce the number of blocking calls to Settings from
// main thread.
// This is enforced by only cleaning the flag at the end of a successful run of #onTuningChanged
private boolean mTilesListDirty = true;
private TileLifecycleManager.Factory mTileLifeCycleManagerFactory;
private final FeatureFlags mFeatureFlags;
@Inject
public QSTileHost(Context context,
QSFactory defaultFactory,
@Main Executor mainExecutor,
PluginManager pluginManager,
TunerService tunerService,
Provider<AutoTileManager> autoTiles,
Optional<CentralSurfaces> centralSurfacesOptional,
QSLogger qsLogger,
UserTracker userTracker,
SecureSettings secureSettings,
CustomTileStatePersister customTileStatePersister,
TileLifecycleManager.Factory tileLifecycleManagerFactory,
UserFileManager userFileManager,
FeatureFlags featureFlags
) {
mContext = context;
mUserContext = context;
mTunerService = tunerService;
mPluginManager = pluginManager;
mQSLogger = qsLogger;
mMainExecutor = mainExecutor;
mTileLifeCycleManagerFactory = tileLifecycleManagerFactory;
mUserFileManager = userFileManager;
mFeatureFlags = featureFlags;
mCentralSurfacesOptional = centralSurfacesOptional;
mQsFactories.add(defaultFactory);
pluginManager.addPluginListener(this, QSFactory.class, true);
mUserTracker = userTracker;
mSecureSettings = secureSettings;
mCustomTileStatePersister = customTileStatePersister;
mainExecutor.execute(() -> {
// This is technically a hack to avoid circular dependency of
// QSTileHost -> XXXTile -> QSTileHost. Posting ensures creation
// finishes before creating any tiles.
tunerService.addTunable(this, TILES_SETTING);
// AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
mAutoTiles = autoTiles.get();
});
}
public void destroy() {
mTiles.values().forEach(tile -> tile.destroy());
mAutoTiles.destroy();
mTunerService.removeTunable(this);
mPluginManager.removePluginListener(this);
}
@Override
public void onPluginConnected(QSFactory plugin, Context pluginContext) {
// Give plugins priority over creation so they can override if they wish.
mQsFactories.add(0, plugin);
String value = mTunerService.getValue(TILES_SETTING);
// Force remove and recreate of all tiles.
onTuningChanged(TILES_SETTING, "");
onTuningChanged(TILES_SETTING, value);
}
@Override
public void onPluginDisconnected(QSFactory plugin) {
mQsFactories.remove(plugin);
// Force remove and recreate of all tiles.
String value = mTunerService.getValue(TILES_SETTING);
onTuningChanged(TILES_SETTING, "");
onTuningChanged(TILES_SETTING, value);
}
@Override
public void addCallback(Callback callback) {
mCallbacks.add(callback);
}
@Override
public void removeCallback(Callback callback) {
mCallbacks.remove(callback);
}
@Override
public Collection<QSTile> getTiles() {
return mTiles.values();
}
@Override
public void collapsePanels() {
mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateCollapsePanels);
}
@Override
public void forceCollapsePanels() {
mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateForceCollapsePanels);
}
@Override
public void openPanels() {
mCentralSurfacesOptional.ifPresent(CentralSurfaces::postAnimateOpenPanels);
}
@Override
public Context getContext() {
return mContext;
}
@Override
public Context getUserContext() {
return mUserContext;
}
@Override
public int getUserId() {
return mCurrentUser;
}
public int indexOf(String spec) {
return mTileSpecs.indexOf(spec);
}
/**
* Whenever the Secure Setting keeping track of the current tiles changes (or upon start) this
* will be called with the new value of the setting.
*
* This method will do the following:
* <ol>
* <li>Destroy any existing tile that's not one of the current tiles (in the setting)</li>
* <li>Create new tiles for those that don't already exist. If this tiles end up being
* not available, they'll also be destroyed.</li>
* <li>Save the resolved list of tiles (current tiles that are available) into the setting.
* This means that after this call ends, the tiles in the Setting, {@link #mTileSpecs},
* and visible tiles ({@link #mTiles}) must match.
* </li>
* </ol>
*
* Additionally, if the user has changed, it'll do the following:
* <ul>
* <li>Change the user for SystemUI tiles: {@link QSTile#userSwitch}.</li>
* <li>Destroy any {@link CustomTile} and recreate it for the new user.</li>
* </ul>
*
* This happens in main thread as {@link com.android.systemui.tuner.TunerServiceImpl} dispatches
* in main thread.
*
* @see QSTile#isAvailable
*/
@MainThread
@Override
public void onTuningChanged(String key, String newValue) {
if (!TILES_SETTING.equals(key)) {
return;
}
// Do not process tiles if the flag is enabled.
if (mFeatureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) {
return;
}
if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
}
final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
int currentUser = mUserTracker.getUserId();
if (currentUser != mCurrentUser) {
mUserContext = mUserTracker.getUserContext();
if (mAutoTiles != null) {
mAutoTiles.changeUser(UserHandle.of(currentUser));
}
}
if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
Log.d(TAG, "Recreating tiles: " + tileSpecs);
mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
tile -> {
Log.d(TAG, "Destroying tile: " + tile.getKey());
mQSLogger.logTileDestroyed(tile.getKey(), "Tile removed");
tile.getValue().destroy();
});
final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
for (String tileSpec : tileSpecs) {
QSTile tile = mTiles.get(tileSpec);
if (tile != null && (!(tile instanceof CustomTile)
|| ((CustomTile) tile).getUser() == currentUser)) {
if (tile.isAvailable()) {
if (DEBUG) Log.d(TAG, "Adding " + tile);
tile.removeCallbacks();
if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
tile.userSwitch(currentUser);
}
newTiles.put(tileSpec, tile);
mQSLogger.logTileAdded(tileSpec);
} else {
tile.destroy();
Log.d(TAG, "Destroying not available tile: " + tileSpec);
mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
}
} else {
// This means that the tile is a CustomTile AND the user is different, so let's
// destroy it
if (tile != null) {
tile.destroy();
Log.d(TAG, "Destroying tile for wrong user: " + tileSpec);
mQSLogger.logTileDestroyed(tileSpec, "Tile for wrong user");
}
Log.d(TAG, "Creating tile: " + tileSpec);
try {
tile = createTile(tileSpec);
if (tile != null) {
tile.setTileSpec(tileSpec);
if (tile.isAvailable()) {
newTiles.put(tileSpec, tile);
mQSLogger.logTileAdded(tileSpec);
} else {
tile.destroy();
Log.d(TAG, "Destroying not available tile: " + tileSpec);
mQSLogger.logTileDestroyed(tileSpec, "Tile not available");
}
} else {
Log.d(TAG, "No factory for a spec: " + tileSpec);
}
} catch (Throwable t) {
Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
}
}
}
mCurrentUser = currentUser;
List<String> currentSpecs = new ArrayList<>(mTileSpecs);
mTileSpecs.clear();
mTileSpecs.addAll(newTiles.keySet()); // Only add the valid (available) tiles.
mTiles.clear();
mTiles.putAll(newTiles);
if (newTiles.isEmpty() && !tileSpecs.isEmpty()) {
// If we didn't manage to create any tiles, set it to empty (default)
Log.d(TAG, "No valid tiles on tuning changed. Setting to default.");
changeTilesByUser(currentSpecs, loadTileSpecs(mContext, ""));
} else {
String resolvedTiles = TextUtils.join(",", mTileSpecs);
if (!resolvedTiles.equals(newValue)) {
// If the resolved tiles (those we actually ended up with) are different than
// the ones that are in the setting, update the Setting.
saveTilesToSettings(mTileSpecs);
}
mTilesListDirty = false;
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onTilesChanged();
}
}
}
/**
* Only use with [CustomTile] if the tile doesn't exist anymore (and therefore doesn't need
* its lifecycle terminated).
*/
@Override
public void removeTile(String spec) {
if (spec.startsWith(CustomTile.PREFIX)) {
// If the tile is removed (due to it not actually existing), mark it as removed. That
// way it will be marked as newly added if it appears in the future.
setTileAdded(CustomTile.getComponentFromSpec(spec), mCurrentUser, false);
}
mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)));
}
/**
* Remove many tiles at once.
*
* It will only save to settings once (as opposed to {@link QSTileHost#removeTileByUser} called
* multiple times).
*/
@Override
public void removeTiles(Collection<String> specs) {
mMainExecutor.execute(() -> changeTileSpecs(tileSpecs -> tileSpecs.removeAll(specs)));
}
/**
* Add a tile to the end
*
* @param spec string matching a pre-defined tilespec
*/
public void addTile(String spec) {
addTile(spec, POSITION_AT_END);
}
@Override
public void addTile(String spec, int requestPosition) {
mMainExecutor.execute(() ->
changeTileSpecs(tileSpecs -> {
if (tileSpecs.contains(spec)) return false;
int size = tileSpecs.size();
if (requestPosition == POSITION_AT_END || requestPosition >= size) {
tileSpecs.add(spec);
} else {
tileSpecs.add(requestPosition, spec);
}
return true;
})
);
}
// When calling this, you may want to modify mTilesListDirty accordingly.
@MainThread
private void saveTilesToSettings(List<String> tileSpecs) {
mSecureSettings.putStringForUser(TILES_SETTING, TextUtils.join(",", tileSpecs),
null /* tag */, false /* default */, mCurrentUser,
true /* overrideable by restore */);
}
@MainThread
private void changeTileSpecs(Predicate<List<String>> changeFunction) {
final List<String> tileSpecs;
if (!mTilesListDirty) {
tileSpecs = new ArrayList<>(mTileSpecs);
} else {
tileSpecs = loadTileSpecs(mContext,
mSecureSettings.getStringForUser(TILES_SETTING, mCurrentUser));
}
if (changeFunction.test(tileSpecs)) {
mTilesListDirty = true;
saveTilesToSettings(tileSpecs);
}
}
@Override
public void addTile(ComponentName tile) {
addTile(tile, /* end */ false);
}
@Override
public void addTile(ComponentName tile, boolean end) {
String spec = CustomTile.toSpec(tile);
addTile(spec, end ? POSITION_AT_END : 0);
}
/**
* This will call through {@link #changeTilesByUser}. It should only be used when a tile is
* removed by a <b>user action</b> like {@code adb}.
*/
@Override
public void removeTileByUser(ComponentName tile) {
mMainExecutor.execute(() -> {
List<String> newSpecs = new ArrayList<>(mTileSpecs);
if (newSpecs.remove(CustomTile.toSpec(tile))) {
changeTilesByUser(mTileSpecs, newSpecs);
}
});
}
/**
* Change the tiles triggered by the user editing.
* <p>
* This is not called on device start, or on user change.
*
* {@link android.service.quicksettings.TileService#onTileRemoved} will be called for tiles
* that are removed.
*/
@MainThread
@Override
public void changeTilesByUser(List<String> previousTiles, List<String> newTiles) {
final List<String> copy = new ArrayList<>(previousTiles);
final int NP = copy.size();
for (int i = 0; i < NP; i++) {
String tileSpec = copy.get(i);
if (!tileSpec.startsWith(CustomTile.PREFIX)) continue;
if (!newTiles.contains(tileSpec)) {
ComponentName component = CustomTile.getComponentFromSpec(tileSpec);
Intent intent = new Intent().setComponent(component);
TileLifecycleManager lifecycleManager = mTileLifeCycleManagerFactory.create(
intent, new UserHandle(mCurrentUser));
lifecycleManager.onStopListening();
lifecycleManager.onTileRemoved();
mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
setTileAdded(component, mCurrentUser, false);
lifecycleManager.flushMessagesAndUnbind();
}
}
if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles);
mTilesListDirty = true;
saveTilesToSettings(newTiles);
}
@Nullable
@Override
public QSTile createTile(String tileSpec) {
for (int i = 0; i < mQsFactories.size(); i++) {
QSTile t = mQsFactories.get(i).createTile(tileSpec);
if (t != null) {
return t;
}
}
return null;
}
@Override
public QSTileView createTileView(Context themedContext, QSTile tile, boolean collapsedView) {
for (int i = 0; i < mQsFactories.size(); i++) {
QSTileView view = mQsFactories.get(i)
.createTileView(themedContext, tile, collapsedView);
if (view != null) {
return view;
}
}
throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec());
}
/**
* Check if a particular {@link CustomTile} has been added for a user and has not been removed
* since.
* @param componentName the {@link ComponentName} of the
* {@link android.service.quicksettings.TileService} associated with the
* tile.
* @param userId the user to check
*/
@Override
public boolean isTileAdded(ComponentName componentName, int userId) {
return mUserFileManager
.getSharedPreferences(TILES, 0, userId)
.getBoolean(componentName.flattenToString(), false);
}
/**
* Persists whether a particular {@link CustomTile} has been added and it's currently in the
* set of selected tiles ({@link #mTiles}.
* @param componentName the {@link ComponentName} of the
* {@link android.service.quicksettings.TileService} associated
* with the tile.
* @param userId the user for this tile
* @param added {@code true} if the tile is being added, {@code false} otherwise
*/
@Override
public void setTileAdded(ComponentName componentName, int userId, boolean added) {
mUserFileManager.getSharedPreferences(TILES, 0, userId)
.edit()
.putBoolean(componentName.flattenToString(), added)
.apply();
}
@Override
public List<String> getSpecs() {
return mTileSpecs;
}
protected static List<String> loadTileSpecs(Context context, String tileList) {
final Resources res = context.getResources();
if (TextUtils.isEmpty(tileList)) {
tileList = res.getString(R.string.quick_settings_tiles);
if (DEBUG) Log.d(TAG, "Loaded tile specs from default config: " + tileList);
} else {
if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
}
final ArrayList<String> tiles = new ArrayList<String>();
boolean addedDefault = false;
Set<String> addedSpecs = new ArraySet<>();
for (String tile : tileList.split(",")) {
tile = tile.trim();
if (tile.isEmpty()) continue;
if (tile.equals("default")) {
if (!addedDefault) {
List<String> defaultSpecs = QSHost.getDefaultSpecs(context.getResources());
for (String spec : defaultSpecs) {
if (!addedSpecs.contains(spec)) {
tiles.add(spec);
addedSpecs.add(spec);
}
}
addedDefault = true;
}
} else {
if (!addedSpecs.contains(tile)) {
tiles.add(tile);
addedSpecs.add(tile);
}
}
}
if (!tiles.contains("internet")) {
if (tiles.contains("wifi")) {
// Replace the WiFi with Internet, and remove the Cell
tiles.set(tiles.indexOf("wifi"), "internet");
tiles.remove("cell");
} else if (tiles.contains("cell")) {
// Replace the Cell with Internet
tiles.set(tiles.indexOf("cell"), "internet");
}
} else {
tiles.remove("wifi");
tiles.remove("cell");
}
return tiles;
}
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("QSTileHost:");
mTiles.values().stream().filter(obj -> obj instanceof Dumpable)
.forEach(o -> ((Dumpable) o).dump(pw, args));
}
@Override
public void dumpProto(@NotNull SystemUIProtoDump systemUIProtoDump, @NotNull String[] args) {
List<QsTileState> data = mTiles.values().stream()
.map(QSTile::getState)
.map(TileStateToProtoKt::toProto)
.filter(Objects::nonNull)
.collect(Collectors.toList());
systemUIProtoDump.tiles = data.toArray(new QsTileState[0]);
}
}