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