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
15package com.android.systemui.qs;
16
17import android.app.ActivityManager;
18import android.content.ComponentName;
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.os.Handler;
23import android.os.UserHandle;
24import android.os.UserManager;
25import android.provider.Settings;
26import android.provider.Settings.Secure;
27import android.service.quicksettings.Tile;
28import android.text.TextUtils;
29import android.util.Log;
30
31import com.android.systemui.Dependency;
32import com.android.systemui.R;
33import com.android.systemui.plugins.PluginListener;
34import com.android.systemui.plugins.PluginManager;
35import com.android.systemui.plugins.qs.QSFactory;
36import com.android.systemui.plugins.qs.QSTileView;
37import com.android.systemui.plugins.qs.QSTile;
38import com.android.systemui.qs.external.CustomTile;
39import com.android.systemui.qs.external.TileLifecycleManager;
40import com.android.systemui.qs.external.TileServices;
41import com.android.systemui.qs.tileimpl.QSFactoryImpl;
42import com.android.systemui.statusbar.phone.AutoTileManager;
43import com.android.systemui.statusbar.phone.StatusBar;
44import com.android.systemui.statusbar.phone.StatusBarIconController;
45import com.android.systemui.tuner.TunerService;
46import com.android.systemui.tuner.TunerService.Tunable;
47
48import java.util.ArrayList;
49import java.util.Arrays;
50import java.util.Collection;
51import java.util.LinkedHashMap;
52import java.util.List;
53
54/** Platform implementation of the quick settings tile host **/
55public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory> {
56    private static final String TAG = "QSTileHost";
57    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
58
59    public static final String TILES_SETTING = Secure.QS_TILES;
60
61    private final Context mContext;
62    private final StatusBar mStatusBar;
63    private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>();
64    protected final ArrayList<String> mTileSpecs = new ArrayList<>();
65    private final TileServices mServices;
66
67    private final List<Callback> mCallbacks = new ArrayList<>();
68    private final AutoTileManager mAutoTiles;
69    private final StatusBarIconController mIconController;
70    private final ArrayList<QSFactory> mQsFactories = new ArrayList<>();
71    private int mCurrentUser;
72
73    public QSTileHost(Context context, StatusBar statusBar,
74            StatusBarIconController iconController) {
75        mIconController = iconController;
76        mContext = context;
77        mStatusBar = statusBar;
78
79        mServices = new TileServices(this, Dependency.get(Dependency.BG_LOOPER));
80
81        mQsFactories.add(new QSFactoryImpl(this));
82        Dependency.get(PluginManager.class).addPluginListener(this, QSFactory.class, true);
83
84        Dependency.get(TunerService.class).addTunable(this, TILES_SETTING);
85        // AutoTileManager can modify mTiles so make sure mTiles has already been initialized.
86        mAutoTiles = new AutoTileManager(context, this);
87    }
88
89    public StatusBarIconController getIconController() {
90        return mIconController;
91    }
92
93    public void destroy() {
94        mTiles.values().forEach(tile -> tile.destroy());
95        mAutoTiles.destroy();
96        Dependency.get(TunerService.class).removeTunable(this);
97        mServices.destroy();
98        Dependency.get(PluginManager.class).removePluginListener(this);
99    }
100
101    @Override
102    public void onPluginConnected(QSFactory plugin, Context pluginContext) {
103        // Give plugins priority over creation so they can override if they wish.
104        mQsFactories.add(0, plugin);
105        String value = Dependency.get(TunerService.class).getValue(TILES_SETTING);
106        // Force remove and recreate of all tiles.
107        onTuningChanged(TILES_SETTING, "");
108        onTuningChanged(TILES_SETTING, value);
109    }
110
111    @Override
112    public void onPluginDisconnected(QSFactory plugin) {
113        mQsFactories.remove(plugin);
114        // Force remove and recreate of all tiles.
115        String value = Dependency.get(TunerService.class).getValue(TILES_SETTING);
116        onTuningChanged(TILES_SETTING, "");
117        onTuningChanged(TILES_SETTING, value);
118    }
119
120    @Override
121    public void addCallback(Callback callback) {
122        mCallbacks.add(callback);
123    }
124
125    @Override
126    public void removeCallback(Callback callback) {
127        mCallbacks.remove(callback);
128    }
129
130    @Override
131    public Collection<QSTile> getTiles() {
132        return mTiles.values();
133    }
134
135    @Override
136    public void warn(String message, Throwable t) {
137        // already logged
138    }
139
140    @Override
141    public void collapsePanels() {
142        mStatusBar.postAnimateCollapsePanels();
143    }
144
145    @Override
146    public void forceCollapsePanels() {
147        mStatusBar.postAnimateForceCollapsePanels();
148    }
149
150    @Override
151    public void openPanels() {
152        mStatusBar.postAnimateOpenPanels();
153    }
154
155    @Override
156    public Context getContext() {
157        return mContext;
158    }
159
160
161    public TileServices getTileServices() {
162        return mServices;
163    }
164
165    public int indexOf(String spec) {
166        return mTileSpecs.indexOf(spec);
167    }
168
169    @Override
170    public void onTuningChanged(String key, String newValue) {
171        if (!TILES_SETTING.equals(key)) {
172            return;
173        }
174        if (DEBUG) Log.d(TAG, "Recreating tiles");
175        if (newValue == null && UserManager.isDeviceInDemoMode(mContext)) {
176            newValue = mContext.getResources().getString(R.string.quick_settings_tiles_retail_mode);
177        }
178        final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
179        int currentUser = ActivityManager.getCurrentUser();
180        if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
181        mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
182                tile -> {
183                    if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
184                    tile.getValue().destroy();
185                });
186        final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
187        for (String tileSpec : tileSpecs) {
188            QSTile tile = mTiles.get(tileSpec);
189            if (tile != null && (!(tile instanceof CustomTile)
190                    || ((CustomTile) tile).getUser() == currentUser)) {
191                if (tile.isAvailable()) {
192                    if (DEBUG) Log.d(TAG, "Adding " + tile);
193                    tile.removeCallbacks();
194                    if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
195                        tile.userSwitch(currentUser);
196                    }
197                    newTiles.put(tileSpec, tile);
198                } else {
199                    tile.destroy();
200                }
201            } else {
202                if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
203                try {
204                    tile = createTile(tileSpec);
205                    if (tile != null) {
206                        if (tile.isAvailable()) {
207                            tile.setTileSpec(tileSpec);
208                            newTiles.put(tileSpec, tile);
209                        } else {
210                            tile.destroy();
211                        }
212                    }
213                } catch (Throwable t) {
214                    Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
215                }
216            }
217        }
218        mCurrentUser = currentUser;
219        mTileSpecs.clear();
220        mTileSpecs.addAll(tileSpecs);
221        mTiles.clear();
222        mTiles.putAll(newTiles);
223        for (int i = 0; i < mCallbacks.size(); i++) {
224            mCallbacks.get(i).onTilesChanged();
225        }
226    }
227
228    @Override
229    public void removeTile(String tileSpec) {
230        ArrayList<String> specs = new ArrayList<>(mTileSpecs);
231        specs.remove(tileSpec);
232        Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING,
233                TextUtils.join(",", specs), ActivityManager.getCurrentUser());
234    }
235
236    public void addTile(String spec) {
237        final String setting = Settings.Secure.getStringForUser(mContext.getContentResolver(),
238                TILES_SETTING, ActivityManager.getCurrentUser());
239        final List<String> tileSpecs = loadTileSpecs(mContext, setting);
240        if (tileSpecs.contains(spec)) {
241            return;
242        }
243        tileSpecs.add(spec);
244        Settings.Secure.putStringForUser(mContext.getContentResolver(), TILES_SETTING,
245                TextUtils.join(",", tileSpecs), ActivityManager.getCurrentUser());
246    }
247
248    public void addTile(ComponentName tile) {
249        List<String> newSpecs = new ArrayList<>(mTileSpecs);
250        newSpecs.add(0, CustomTile.toSpec(tile));
251        changeTiles(mTileSpecs, newSpecs);
252    }
253
254    public void removeTile(ComponentName tile) {
255        List<String> newSpecs = new ArrayList<>(mTileSpecs);
256        newSpecs.remove(CustomTile.toSpec(tile));
257        changeTiles(mTileSpecs, newSpecs);
258    }
259
260    public void changeTiles(List<String> previousTiles, List<String> newTiles) {
261        final int NP = previousTiles.size();
262        final int NA = newTiles.size();
263        for (int i = 0; i < NP; i++) {
264            String tileSpec = previousTiles.get(i);
265            if (!tileSpec.startsWith(CustomTile.PREFIX)) continue;
266            if (!newTiles.contains(tileSpec)) {
267                ComponentName component = CustomTile.getComponentFromSpec(tileSpec);
268                Intent intent = new Intent().setComponent(component);
269                TileLifecycleManager lifecycleManager = new TileLifecycleManager(new Handler(),
270                        mContext, mServices, new Tile(), intent,
271                        new UserHandle(ActivityManager.getCurrentUser()));
272                lifecycleManager.onStopListening();
273                lifecycleManager.onTileRemoved();
274                TileLifecycleManager.setTileAdded(mContext, component, false);
275                lifecycleManager.flushMessagesAndUnbind();
276            }
277        }
278        if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles);
279        Secure.putStringForUser(getContext().getContentResolver(), QSTileHost.TILES_SETTING,
280                TextUtils.join(",", newTiles), ActivityManager.getCurrentUser());
281    }
282
283    public QSTile createTile(String tileSpec) {
284        for (int i = 0; i < mQsFactories.size(); i++) {
285            QSTile t = mQsFactories.get(i).createTile(tileSpec);
286            if (t != null) {
287                return t;
288            }
289        }
290        return null;
291    }
292
293    public QSTileView createTileView(QSTile tile, boolean collapsedView) {
294        for (int i = 0; i < mQsFactories.size(); i++) {
295            QSTileView view = mQsFactories.get(i).createTileView(tile, collapsedView);
296            if (view != null) {
297                return view;
298            }
299        }
300        throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec());
301    }
302
303    protected List<String> loadTileSpecs(Context context, String tileList) {
304        final Resources res = context.getResources();
305        final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
306        if (tileList == null) {
307            tileList = res.getString(R.string.quick_settings_tiles);
308            if (DEBUG) Log.d(TAG, "Loaded tile specs from config: " + tileList);
309        } else {
310            if (DEBUG) Log.d(TAG, "Loaded tile specs from setting: " + tileList);
311        }
312        final ArrayList<String> tiles = new ArrayList<String>();
313        boolean addedDefault = false;
314        for (String tile : tileList.split(",")) {
315            tile = tile.trim();
316            if (tile.isEmpty()) continue;
317            if (tile.equals("default")) {
318                if (!addedDefault) {
319                    tiles.addAll(Arrays.asList(defaultTileList.split(",")));
320                    addedDefault = true;
321                }
322            } else {
323                tiles.add(tile);
324            }
325        }
326        return tiles;
327    }
328}
329