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