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.server.display.feature.flags.Flags; 36 import com.android.systemui.dagger.qualifiers.Background; 37 import com.android.systemui.dagger.qualifiers.Main; 38 import com.android.systemui.plugins.qs.QSTile; 39 import com.android.systemui.plugins.qs.QSTile.State; 40 import com.android.systemui.qs.QSHost; 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.res.R; 45 import com.android.systemui.settings.UserTracker; 46 import com.android.systemui.shade.ShadeDisplayAware; 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( @hadeDisplayAware Context context, UserTracker userTracker, @Main Executor mainExecutor, @Background Executor bgExecutor )72 public TileQueryHelper( 73 @ShadeDisplayAware 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 118 final ArrayList<QSTile> tilesToAdd = new ArrayList<>(); 119 possibleTiles.remove("cell"); 120 possibleTiles.remove("wifi"); 121 if (Flags.evenDimmer() && mContext.getResources().getBoolean( 122 com.android.internal.R.bool.config_evenDimmerEnabled)) { 123 possibleTiles.remove("reduce_brightness"); 124 } 125 126 for (String spec : possibleTiles) { 127 // Only add current and stock tiles that can be created from QSFactoryImpl. 128 // Do not include CustomTile. Those will be created by `addPackageTiles`. 129 if (spec.startsWith(CustomTile.PREFIX)) continue; 130 final QSTile tile = host.createTile(spec); 131 if (tile == null) { 132 continue; 133 } else if (!tile.isAvailable()) { 134 tile.destroy(); 135 continue; 136 } 137 tilesToAdd.add(tile); 138 } 139 140 new TileCollector(tilesToAdd, host).startListening(); 141 } 142 143 private static class TilePair { TilePair(QSTile tile)144 private TilePair(QSTile tile) { 145 mTile = tile; 146 } 147 148 QSTile mTile; 149 boolean mReady = false; 150 } 151 152 private class TileCollector implements QSTile.Callback { 153 154 private final List<TilePair> mQSTileList = new ArrayList<>(); 155 private final QSHost mQSHost; 156 TileCollector(List<QSTile> tilesToAdd, QSHost host)157 TileCollector(List<QSTile> tilesToAdd, QSHost host) { 158 for (QSTile tile: tilesToAdd) { 159 TilePair pair = new TilePair(tile); 160 mQSTileList.add(pair); 161 } 162 mQSHost = host; 163 if (tilesToAdd.isEmpty()) { 164 mBgExecutor.execute(this::finished); 165 } 166 } 167 finished()168 private void finished() { 169 notifyTilesChanged(false); 170 addPackageTiles(mQSHost); 171 } 172 startListening()173 private void startListening() { 174 for (TilePair pair: mQSTileList) { 175 pair.mTile.addCallback(this); 176 pair.mTile.setListening(this, true); 177 // Make sure that at least one refresh state happens 178 pair.mTile.refreshState(); 179 } 180 } 181 182 // This is called in the Bg thread 183 @Override onStateChanged(State s)184 public void onStateChanged(State s) { 185 boolean allReady = true; 186 for (TilePair pair: mQSTileList) { 187 if (!pair.mReady && pair.mTile.isTileReady()) { 188 pair.mTile.removeCallback(this); 189 pair.mTile.setListening(this, false); 190 pair.mReady = true; 191 } else if (!pair.mReady) { 192 allReady = false; 193 } 194 } 195 if (allReady) { 196 for (TilePair pair : mQSTileList) { 197 QSTile tile = pair.mTile; 198 final QSTile.State state = tile.getState().copy(); 199 // Ignore the current state and get the generic label instead. 200 state.label = tile.getTileLabel(); 201 tile.destroy(); 202 addTile(tile.getTileSpec(), null, state, true); 203 } 204 finished(); 205 } 206 } 207 } 208 addPackageTiles(final QSHost host)209 private void addPackageTiles(final QSHost host) { 210 mBgExecutor.execute(() -> { 211 Collection<QSTile> params = host.getTiles(); 212 PackageManager pm = mContext.getPackageManager(); 213 List<ResolveInfo> services = pm.queryIntentServicesAsUser( 214 new Intent(TileService.ACTION_QS_TILE), 0, mUserTracker.getUserId()); 215 String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock); 216 217 for (ResolveInfo info : services) { 218 String packageName = info.serviceInfo.packageName; 219 ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name); 220 221 // Don't include apps that are a part of the default tile set. 222 if (stockTiles.contains(componentName.flattenToString())) { 223 continue; 224 } 225 226 final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm); 227 String spec = CustomTile.toSpec(componentName); 228 State state = getState(params, spec); 229 if (state != null) { 230 addTile(spec, appLabel, state, false); 231 continue; 232 } 233 if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) { 234 continue; 235 } 236 Drawable icon = info.serviceInfo.loadIcon(pm); 237 if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) { 238 continue; 239 } 240 if (icon == null) { 241 continue; 242 } 243 icon.mutate(); 244 icon.setTint(mContext.getColor(android.R.color.white)); 245 CharSequence label = info.serviceInfo.loadLabel(pm); 246 createStateAndAddTile(spec, icon, label != null ? label.toString() : "null", 247 appLabel); 248 } 249 250 notifyTilesChanged(true); 251 }); 252 } 253 notifyTilesChanged(final boolean finished)254 private void notifyTilesChanged(final boolean finished) { 255 final ArrayList<TileInfo> tilesToReturn = new ArrayList<>(mTiles); 256 mMainExecutor.execute(() -> { 257 if (mListener != null) { 258 mListener.onTilesChanged(tilesToReturn); 259 } 260 mFinished = finished; 261 }); 262 } 263 264 @Nullable getState(Collection<QSTile> tiles, String spec)265 private State getState(Collection<QSTile> tiles, String spec) { 266 for (QSTile tile : tiles) { 267 if (spec.equals(tile.getTileSpec())) { 268 if (tile.isTileReady()) { 269 return tile.getState().copy(); 270 } else { 271 return null; 272 } 273 } 274 } 275 return null; 276 } 277 addTile( String spec, @Nullable CharSequence appLabel, State state, boolean isSystem)278 private void addTile( 279 String spec, @Nullable CharSequence appLabel, State state, boolean isSystem) { 280 if (mSpecs.contains(spec)) { 281 return; 282 } 283 state.dualTarget = false; // No dual targets in edit. 284 state.expandedAccessibilityClassName = Button.class.getName(); 285 state.secondaryLabel = (isSystem || TextUtils.equals(state.label, appLabel)) 286 ? null : appLabel; 287 TileInfo info = new TileInfo(spec, state, isSystem); 288 mTiles.add(info); 289 mSpecs.add(spec); 290 } 291 createStateAndAddTile( String spec, Drawable drawable, CharSequence label, CharSequence appLabel)292 private void createStateAndAddTile( 293 String spec, Drawable drawable, CharSequence label, CharSequence appLabel) { 294 QSTile.State state = new QSTile.State(); 295 state.state = Tile.STATE_INACTIVE; 296 state.label = label; 297 state.contentDescription = label; 298 state.icon = new DrawableIcon(drawable); 299 addTile(spec, appLabel, state, false); 300 } 301 302 public static class TileInfo { TileInfo(String spec, QSTile.State state, boolean isSystem)303 public TileInfo(String spec, QSTile.State state, boolean isSystem) { 304 this.spec = spec; 305 this.state = state; 306 this.isSystem = isSystem; 307 } 308 309 public String spec; 310 public QSTile.State state; 311 public boolean isSystem; 312 } 313 314 public interface TileStateListener { onTilesChanged(List<TileInfo> tiles)315 void onTilesChanged(List<TileInfo> tiles); 316 } 317 } 318