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