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.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.graphics.drawable.Drawable;
26 import android.os.Build;
27 import android.provider.Settings;
28 import android.service.quicksettings.Tile;
29 import android.service.quicksettings.TileService;
30 import android.text.TextUtils;
31 import android.util.ArraySet;
32 import android.widget.Button;
33 
34 import androidx.annotation.Nullable;
35 
36 import com.android.systemui.R;
37 import com.android.systemui.dagger.qualifiers.Background;
38 import com.android.systemui.dagger.qualifiers.Main;
39 import com.android.systemui.plugins.qs.QSTile;
40 import com.android.systemui.plugins.qs.QSTile.State;
41 import com.android.systemui.qs.QSHost;
42 import com.android.systemui.qs.dagger.QSScope;
43 import com.android.systemui.qs.external.CustomTile;
44 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
45 import com.android.systemui.settings.UserTracker;
46 import com.android.systemui.util.leak.GarbageMonitor;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.concurrent.Executor;
53 
54 import javax.inject.Inject;
55 
56 /** */
57 @QSScope
58 public class TileQueryHelper {
59     private static final String TAG = "TileQueryHelper";
60 
61     private final ArrayList<TileInfo> mTiles = new ArrayList<>();
62     private final ArraySet<String> mSpecs = new ArraySet<>();
63     private final Executor mMainExecutor;
64     private final Executor mBgExecutor;
65     private final Context mContext;
66     private final UserTracker mUserTracker;
67     private TileStateListener mListener;
68 
69     private boolean mFinished;
70 
71     @Inject
TileQueryHelper( Context context, UserTracker userTracker, @Main Executor mainExecutor, @Background Executor bgExecutor )72     public TileQueryHelper(
73             Context context,
74             UserTracker userTracker,
75             @Main Executor mainExecutor,
76             @Background Executor bgExecutor
77     ) {
78         mContext = context;
79         mMainExecutor = mainExecutor;
80         mBgExecutor = bgExecutor;
81         mUserTracker = userTracker;
82     }
83 
setListener(@ullable TileStateListener listener)84     public void setListener(@Nullable TileStateListener listener) {
85         mListener = listener;
86     }
87 
queryTiles(QSHost host)88     public void queryTiles(QSHost host) {
89         mTiles.clear();
90         mSpecs.clear();
91         mFinished = false;
92         // Enqueue jobs to fetch every system tile and then ever package tile.
93         addCurrentAndStockTiles(host);
94     }
95 
isFinished()96     public boolean isFinished() {
97         return mFinished;
98     }
99 
addCurrentAndStockTiles(QSHost host)100     private void addCurrentAndStockTiles(QSHost host) {
101         String stock = mContext.getString(R.string.quick_settings_tiles_stock);
102         String current = Settings.Secure.getString(mContext.getContentResolver(),
103                 Settings.Secure.QS_TILES);
104         final ArrayList<String> possibleTiles = new ArrayList<>();
105         if (current != null) {
106             // The setting QS_TILES is not populated immediately upon Factory Reset
107             possibleTiles.addAll(Arrays.asList(current.split(",")));
108         } else {
109             current = "";
110         }
111         String[] stockSplit =  stock.split(",");
112         for (String spec : stockSplit) {
113             if (!current.contains(spec)) {
114                 possibleTiles.add(spec);
115             }
116         }
117         if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) {
118             possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
119         }
120 
121         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
122         possibleTiles.remove("cell");
123         possibleTiles.remove("wifi");
124 
125         for (String spec : possibleTiles) {
126             // Only add current and stock tiles that can be created from QSFactoryImpl.
127             // Do not include CustomTile. Those will be created by `addPackageTiles`.
128             if (spec.startsWith(CustomTile.PREFIX)) continue;
129             final QSTile tile = host.createTile(spec);
130             if (tile == null) {
131                 continue;
132             } else if (!tile.isAvailable()) {
133                 tile.setTileSpec(spec);
134                 tile.destroy();
135                 continue;
136             }
137             tile.setTileSpec(spec);
138             tilesToAdd.add(tile);
139         }
140 
141         new TileCollector(tilesToAdd, host).startListening();
142     }
143 
144     private static class TilePair {
TilePair(QSTile tile)145         private TilePair(QSTile tile) {
146             mTile = tile;
147         }
148 
149         QSTile mTile;
150         boolean mReady = false;
151     }
152 
153     private class TileCollector implements QSTile.Callback {
154 
155         private final List<TilePair> mQSTileList = new ArrayList<>();
156         private final QSHost mQSHost;
157 
TileCollector(List<QSTile> tilesToAdd, QSHost host)158         TileCollector(List<QSTile> tilesToAdd, QSHost host) {
159             for (QSTile tile: tilesToAdd) {
160                 TilePair pair = new TilePair(tile);
161                 mQSTileList.add(pair);
162             }
163             mQSHost = host;
164             if (tilesToAdd.isEmpty()) {
165                 mBgExecutor.execute(this::finished);
166             }
167         }
168 
finished()169         private void finished() {
170             notifyTilesChanged(false);
171             addPackageTiles(mQSHost);
172         }
173 
startListening()174         private void startListening() {
175             for (TilePair pair: mQSTileList) {
176                 pair.mTile.addCallback(this);
177                 pair.mTile.setListening(this, true);
178                 // Make sure that at least one refresh state happens
179                 pair.mTile.refreshState();
180             }
181         }
182 
183         // This is called in the Bg thread
184         @Override
onStateChanged(State s)185         public void onStateChanged(State s) {
186             boolean allReady = true;
187             for (TilePair pair: mQSTileList) {
188                 if (!pair.mReady && pair.mTile.isTileReady()) {
189                     pair.mTile.removeCallback(this);
190                     pair.mTile.setListening(this, false);
191                     pair.mReady = true;
192                 } else if (!pair.mReady) {
193                     allReady = false;
194                 }
195             }
196             if (allReady) {
197                 for (TilePair pair : mQSTileList) {
198                     QSTile tile = pair.mTile;
199                     final QSTile.State state = tile.getState().copy();
200                     // Ignore the current state and get the generic label instead.
201                     state.label = tile.getTileLabel();
202                     tile.destroy();
203                     addTile(tile.getTileSpec(), null, state, true);
204                 }
205                 finished();
206             }
207         }
208     }
209 
addPackageTiles(final QSHost host)210     private void addPackageTiles(final QSHost host) {
211         mBgExecutor.execute(() -> {
212             Collection<QSTile> params = host.getTiles();
213             PackageManager pm = mContext.getPackageManager();
214             List<ResolveInfo> services = pm.queryIntentServicesAsUser(
215                     new Intent(TileService.ACTION_QS_TILE), 0, mUserTracker.getUserId());
216             String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
217 
218             for (ResolveInfo info : services) {
219                 String packageName = info.serviceInfo.packageName;
220                 ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
221 
222                 // Don't include apps that are a part of the default tile set.
223                 if (stockTiles.contains(componentName.flattenToString())) {
224                     continue;
225                 }
226 
227                 final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
228                 String spec = CustomTile.toSpec(componentName);
229                 State state = getState(params, spec);
230                 if (state != null) {
231                     addTile(spec, appLabel, state, false);
232                     continue;
233                 }
234                 if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
235                     continue;
236                 }
237                 Drawable icon = info.serviceInfo.loadIcon(pm);
238                 if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
239                     continue;
240                 }
241                 if (icon == null) {
242                     continue;
243                 }
244                 icon.mutate();
245                 icon.setTint(mContext.getColor(android.R.color.white));
246                 CharSequence label = info.serviceInfo.loadLabel(pm);
247                 createStateAndAddTile(spec, icon, label != null ? label.toString() : "null",
248                         appLabel);
249             }
250 
251             notifyTilesChanged(true);
252         });
253     }
254 
notifyTilesChanged(final boolean finished)255     private void notifyTilesChanged(final boolean finished) {
256         final ArrayList<TileInfo> tilesToReturn = new ArrayList<>(mTiles);
257         mMainExecutor.execute(() -> {
258             if (mListener != null) {
259                 mListener.onTilesChanged(tilesToReturn);
260             }
261             mFinished = finished;
262         });
263     }
264 
265     @Nullable
getState(Collection<QSTile> tiles, String spec)266     private State getState(Collection<QSTile> tiles, String spec) {
267         for (QSTile tile : tiles) {
268             if (spec.equals(tile.getTileSpec())) {
269                 return tile.getState().copy();
270             }
271         }
272         return null;
273     }
274 
addTile( String spec, @Nullable CharSequence appLabel, State state, boolean isSystem)275     private void addTile(
276             String spec, @Nullable CharSequence appLabel, State state, boolean isSystem) {
277         if (mSpecs.contains(spec)) {
278             return;
279         }
280         state.dualTarget = false; // No dual targets in edit.
281         state.expandedAccessibilityClassName = Button.class.getName();
282         state.secondaryLabel = (isSystem || TextUtils.equals(state.label, appLabel))
283                 ? null : appLabel;
284         TileInfo info = new TileInfo(spec, state, isSystem);
285         mTiles.add(info);
286         mSpecs.add(spec);
287     }
288 
createStateAndAddTile( String spec, Drawable drawable, CharSequence label, CharSequence appLabel)289     private void createStateAndAddTile(
290             String spec, Drawable drawable, CharSequence label, CharSequence appLabel) {
291         QSTile.State state = new QSTile.State();
292         state.state = Tile.STATE_INACTIVE;
293         state.label = label;
294         state.contentDescription = label;
295         state.icon = new DrawableIcon(drawable);
296         addTile(spec, appLabel, state, false);
297     }
298 
299     public static class TileInfo {
TileInfo(String spec, QSTile.State state, boolean isSystem)300         public TileInfo(String spec, QSTile.State state, boolean isSystem) {
301             this.spec = spec;
302             this.state = state;
303             this.isSystem = isSystem;
304         }
305 
306         public String spec;
307         public QSTile.State state;
308         public boolean isSystem;
309     }
310 
311     public interface TileStateListener {
onTilesChanged(List<TileInfo> tiles)312         void onTilesChanged(List<TileInfo> tiles);
313     }
314 }
315