1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.qs.customize;
18 
19 import android.Manifest.permission;
20 import android.app.ActivityManager;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ResolveInfo;
26 import android.graphics.drawable.Drawable;
27 import android.os.Build;
28 import android.os.Handler;
29 import android.provider.Settings;
30 import android.service.quicksettings.Tile;
31 import android.service.quicksettings.TileService;
32 import android.text.TextUtils;
33 import android.util.ArraySet;
34 import android.widget.Button;
35 
36 import com.android.systemui.Dependency;
37 import com.android.systemui.R;
38 import com.android.systemui.plugins.qs.QSTile;
39 import com.android.systemui.plugins.qs.QSTile.State;
40 import com.android.systemui.qs.QSTileHost;
41 import com.android.systemui.qs.external.CustomTile;
42 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
43 import com.android.systemui.util.leak.GarbageMonitor;
44 
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Collection;
48 import java.util.List;
49 
50 public class TileQueryHelper {
51     private static final String TAG = "TileQueryHelper";
52 
53     private final ArrayList<TileInfo> mTiles = new ArrayList<>();
54     private final ArraySet<String> mSpecs = new ArraySet<>();
55     private final Handler mBgHandler;
56     private final Handler mMainHandler;
57     private final Context mContext;
58     private final TileStateListener mListener;
59 
60     private boolean mFinished;
61 
TileQueryHelper(Context context, TileStateListener listener)62     public TileQueryHelper(Context context, TileStateListener listener) {
63         mContext = context;
64         mListener = listener;
65         mBgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
66         mMainHandler = Dependency.get(Dependency.MAIN_HANDLER);
67     }
68 
queryTiles(QSTileHost host)69     public void queryTiles(QSTileHost host) {
70         mTiles.clear();
71         mSpecs.clear();
72         mFinished = false;
73         // Enqueue jobs to fetch every system tile and then ever package tile.
74         addCurrentAndStockTiles(host);
75 
76         addPackageTiles(host);
77     }
78 
isFinished()79     public boolean isFinished() {
80         return mFinished;
81     }
82 
addCurrentAndStockTiles(QSTileHost host)83     private void addCurrentAndStockTiles(QSTileHost host) {
84         String stock = mContext.getString(R.string.quick_settings_tiles_stock);
85         String current = Settings.Secure.getString(mContext.getContentResolver(),
86                 Settings.Secure.QS_TILES);
87         final ArrayList<String> possibleTiles = new ArrayList<>();
88         if (current != null) {
89             // The setting QS_TILES is not populated immediately upon Factory Reset
90             possibleTiles.addAll(Arrays.asList(current.split(",")));
91         } else {
92             current = "";
93         }
94         String[] stockSplit =  stock.split(",");
95         for (String spec : stockSplit) {
96             if (!current.contains(spec)) {
97                 possibleTiles.add(spec);
98             }
99         }
100         if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) {
101             possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
102         }
103 
104         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
105         for (String spec : possibleTiles) {
106             // Only add current and stock tiles that can be created from QSFactoryImpl.
107             // Do not include CustomTile. Those will be created by `addPackageTiles`.
108             if (spec.startsWith(CustomTile.PREFIX)) continue;
109             final QSTile tile = host.createTile(spec);
110             if (tile == null) {
111                 continue;
112             } else if (!tile.isAvailable()) {
113                 tile.destroy();
114                 continue;
115             }
116             tile.setListening(this, true);
117             tile.refreshState();
118             tile.setListening(this, false);
119             tile.setTileSpec(spec);
120             tilesToAdd.add(tile);
121         }
122 
123         mBgHandler.post(() -> {
124             for (QSTile tile : tilesToAdd) {
125                 final QSTile.State state = tile.getState().copy();
126                 // Ignore the current state and get the generic label instead.
127                 state.label = tile.getTileLabel();
128                 tile.destroy();
129                 addTile(tile.getTileSpec(), null, state, true);
130             }
131             notifyTilesChanged(false);
132         });
133     }
134 
addPackageTiles(final QSTileHost host)135     private void addPackageTiles(final QSTileHost host) {
136         mBgHandler.post(() -> {
137             Collection<QSTile> params = host.getTiles();
138             PackageManager pm = mContext.getPackageManager();
139             List<ResolveInfo> services = pm.queryIntentServicesAsUser(
140                     new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
141             String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
142 
143             for (ResolveInfo info : services) {
144                 String packageName = info.serviceInfo.packageName;
145                 ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
146 
147                 // Don't include apps that are a part of the default tile set.
148                 if (stockTiles.contains(componentName.flattenToString())) {
149                     continue;
150                 }
151 
152                 final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
153                 String spec = CustomTile.toSpec(componentName);
154                 State state = getState(params, spec);
155                 if (state != null) {
156                     addTile(spec, appLabel, state, false);
157                     continue;
158                 }
159                 if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
160                     continue;
161                 }
162                 Drawable icon = info.serviceInfo.loadIcon(pm);
163                 if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
164                     continue;
165                 }
166                 if (icon == null) {
167                     continue;
168                 }
169                 icon.mutate();
170                 icon.setTint(mContext.getColor(android.R.color.white));
171                 CharSequence label = info.serviceInfo.loadLabel(pm);
172                 createStateAndAddTile(spec, icon, label != null ? label.toString() : "null",
173                         appLabel);
174             }
175 
176             notifyTilesChanged(true);
177         });
178     }
179 
notifyTilesChanged(final boolean finished)180     private void notifyTilesChanged(final boolean finished) {
181         final ArrayList<TileInfo> tilesToReturn = new ArrayList<>(mTiles);
182         mMainHandler.post(() -> {
183             mListener.onTilesChanged(tilesToReturn);
184             mFinished = finished;
185         });
186     }
187 
getState(Collection<QSTile> tiles, String spec)188     private State getState(Collection<QSTile> tiles, String spec) {
189         for (QSTile tile : tiles) {
190             if (spec.equals(tile.getTileSpec())) {
191                 return tile.getState().copy();
192             }
193         }
194         return null;
195     }
196 
addTile(String spec, CharSequence appLabel, State state, boolean isSystem)197     private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) {
198         if (mSpecs.contains(spec)) {
199             return;
200         }
201         TileInfo info = new TileInfo();
202         info.state = state;
203         info.state.dualTarget = false; // No dual targets in edit.
204         info.state.expandedAccessibilityClassName =
205                 Button.class.getName();
206         info.spec = spec;
207         info.state.secondaryLabel = (isSystem || TextUtils.equals(state.label, appLabel))
208                 ? null : appLabel;
209         info.isSystem = isSystem;
210         mTiles.add(info);
211         mSpecs.add(spec);
212     }
213 
createStateAndAddTile( String spec, Drawable drawable, CharSequence label, CharSequence appLabel)214     private void createStateAndAddTile(
215             String spec, Drawable drawable, CharSequence label, CharSequence appLabel) {
216         QSTile.State state = new QSTile.State();
217         state.state = Tile.STATE_INACTIVE;
218         state.label = label;
219         state.contentDescription = label;
220         state.icon = new DrawableIcon(drawable);
221         addTile(spec, appLabel, state, false);
222     }
223 
224     public static class TileInfo {
225         public String spec;
226         public QSTile.State state;
227         public boolean isSystem;
228     }
229 
230     public interface TileStateListener {
onTilesChanged(List<TileInfo> tiles)231         void onTilesChanged(List<TileInfo> tiles);
232     }
233 }
234