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