/* * 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.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.provider.Settings.Secure; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.Log; import com.android.systemui.Dependency; import com.android.systemui.R; 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.QSTileView; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.external.TileLifecycleManager; import com.android.systemui.qs.external.TileServices; import com.android.systemui.qs.tileimpl.QSFactoryImpl; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; /** Platform implementation of the quick settings tile host **/ public class QSTileHost implements QSHost, Tunable, PluginListener { private static final String TAG = "QSTileHost"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); public static final String TILES_SETTING = Secure.QS_TILES; private final Context mContext; private final StatusBar mStatusBar; private final LinkedHashMap mTiles = new LinkedHashMap<>(); protected final ArrayList mTileSpecs = new ArrayList<>(); private final TileServices mServices; private final List mCallbacks = new ArrayList<>(); private final AutoTileManager mAutoTiles; private final StatusBarIconController mIconController; private final ArrayList mQsFactories = new ArrayList<>(); private int mCurrentUser; public QSTileHost(Context context, StatusBar statusBar, StatusBarIconController iconController) { mIconController = iconController; mContext = context; mStatusBar = statusBar; mServices = new TileServices(this, Dependency.get(Dependency.BG_LOOPER)); mQsFactories.add(new QSFactoryImpl(this)); Dependency.get(PluginManager.class).addPluginListener(this, QSFactory.class, true); Dependency.get(TunerService.class).addTunable(this, TILES_SETTING); // AutoTileManager can modify mTiles so make sure mTiles has already been initialized. mAutoTiles = new AutoTileManager(context, this); } public StatusBarIconController getIconController() { return mIconController; } public void destroy() { mTiles.values().forEach(tile -> tile.destroy()); mAutoTiles.destroy(); Dependency.get(TunerService.class).removeTunable(this); mServices.destroy(); Dependency.get(PluginManager.class).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 = Dependency.get(TunerService.class).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 = Dependency.get(TunerService.class).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 getTiles() { return mTiles.values(); } @Override public void warn(String message, Throwable t) { // already logged } @Override public void collapsePanels() { mStatusBar.postAnimateCollapsePanels(); } @Override public void forceCollapsePanels() { mStatusBar.postAnimateForceCollapsePanels(); } @Override public void openPanels() { mStatusBar.postAnimateOpenPanels(); } @Override public Context getContext() { return mContext; } public TileServices getTileServices() { return mServices; } public int indexOf(String spec) { return mTileSpecs.indexOf(spec); } @Override public void onTuningChanged(String key, String newValue) { if (!TILES_SETTING.equals(key)) { return; } if (DEBUG) Log.d(TAG, "Recreating tiles"); if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) { newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode); } final List tileSpecs = loadTileSpecs(mContext, newValue); int currentUser = ActivityManager.getCurrentUser(); if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return; mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach( tile -> { if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey()); tile.getValue().destroy(); }); final LinkedHashMap 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); } else { tile.destroy(); } } else { if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec); try { tile = createTile(tileSpec); if (tile != null) { if (tile.isAvailable()) { tile.setTileSpec(tileSpec); newTiles.put(tileSpec, tile); } else { tile.destroy(); } } } catch (Throwable t) { Log.w(TAG, "Error creating tile for spec: " + tileSpec, t); } } } mCurrentUser = currentUser; mTileSpecs.clear(); mTileSpecs.addAll(tileSpecs); mTiles.clear(); mTiles.putAll(newTiles); for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onTilesChanged(); } } @Override public void removeTile(String tileSpec) { ArrayList specs = new ArrayList<>(mTileSpecs); specs.remove(tileSpec); Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, TextUtils.join(",", specs), ActivityManager.getCurrentUser()); } public void addTile(String spec) { final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(), TILES_SETTING, ActivityManager.getCurrentUser()); final List tileSpecs = loadTileSpecs(mContext, setting); if (tileSpecs.contains(spec)) { return; } tileSpecs.add(spec); Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING, TextUtils.join(",", tileSpecs), ActivityManager.getCurrentUser()); } public void addTile(ComponentName tile) { List newSpecs = new ArrayList<>(mTileSpecs); newSpecs.add(0, CustomTile.toSpec(tile)); changeTiles(mTileSpecs, newSpecs); } public void removeTile(ComponentName tile) { List newSpecs = new ArrayList<>(mTileSpecs); newSpecs.remove(CustomTile.toSpec(tile)); changeTiles(mTileSpecs, newSpecs); } public void changeTiles(List previousTiles, List newTiles) { final int NP = previousTiles.size(); final int NA = newTiles.size(); for (int i = 0; i < NP; i++) { String tileSpec = previousTiles.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 = new TileLifecycleManager(new Handler(), mContext, mServices, new Tile(), intent, new UserHandle(ActivityManager.getCurrentUser())); lifecycleManager.onStopListening(); lifecycleManager.onTileRemoved(); TileLifecycleManager.setTileAdded(mContext, component, false); lifecycleManager.flushMessagesAndUnbind(); } } if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles); Secure.putStringForUser(getContext().getContentResolver(), QSTileHost.TILES_SETTING, TextUtils.join(",", newTiles), ActivityManager.getCurrentUser()); } 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; } public QSTileView createTileView(QSTile tile, boolean collapsedView) { for (int i = 0; i < mQsFactories.size(); i++) { QSTileView view = mQsFactories.get(i).createTileView(tile, collapsedView); if (view != null) { return view; } } throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec()); } protected List loadTileSpecs(Context context, String tileList) { final Resources res = context.getResources(); final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); if (tileList == null) { tileList = res.getString(R.string.quick_settings_tiles); if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList); } else { if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList); } final ArrayList tiles = new ArrayList(); boolean addedDefault = false; for (String tile : tileList.split(",")) { tile = tile.trim(); if (tile.isEmpty()) continue; if (tile.equals("default")) { if (!addedDefault) { tiles.addAll(Arrays.asList(defaultTileList.split(","))); addedDefault = true; } } else { tiles.add(tile); } } return tiles; } }