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