1 /* 2 * Copyright (C) 2015 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 package com.android.systemui.qs.external; 17 18 import android.app.PendingIntent; 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.pm.PackageInfo; 22 import android.content.pm.PackageManager; 23 import android.graphics.drawable.Icon; 24 import android.os.Binder; 25 import android.os.Handler; 26 import android.os.IBinder; 27 import android.os.RemoteException; 28 import android.os.UserHandle; 29 import android.service.quicksettings.IQSService; 30 import android.service.quicksettings.Tile; 31 import android.util.ArrayMap; 32 import android.util.Log; 33 import android.util.SparseArrayMap; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 39 import com.android.internal.statusbar.StatusBarIcon; 40 import com.android.systemui.broadcast.BroadcastDispatcher; 41 import com.android.systemui.dagger.SysUISingleton; 42 import com.android.systemui.dagger.qualifiers.Background; 43 import com.android.systemui.dagger.qualifiers.Main; 44 import com.android.systemui.qs.QSHost; 45 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; 46 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; 47 import com.android.systemui.settings.UserTracker; 48 import com.android.systemui.statusbar.CommandQueue; 49 import com.android.systemui.statusbar.phone.StatusBarIconController; 50 import com.android.systemui.statusbar.policy.KeyguardStateController; 51 import com.android.systemui.util.concurrency.DelayableExecutor; 52 53 import java.util.ArrayList; 54 import java.util.Collections; 55 import java.util.Comparator; 56 import java.util.Objects; 57 58 import javax.inject.Inject; 59 import javax.inject.Provider; 60 61 /** 62 * Runs the day-to-day operations of which tiles should be bound and when. 63 */ 64 @SysUISingleton 65 public class TileServices extends IQSService.Stub { 66 static final int DEFAULT_MAX_BOUND = 3; 67 static final int REDUCED_MAX_BOUND = 1; 68 private static final String TAG = "TileServices"; 69 70 private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>(); 71 private final SparseArrayMap<ComponentName, CustomTile> mTiles = new SparseArrayMap<>(); 72 private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>(); 73 private final Context mContext; 74 private final Handler mMainHandler; 75 private final Provider<Handler> mHandlerProvider; 76 private final QSHost mHost; 77 private final KeyguardStateController mKeyguardStateController; 78 private final BroadcastDispatcher mBroadcastDispatcher; 79 private final CommandQueue mCommandQueue; 80 private final UserTracker mUserTracker; 81 private final StatusBarIconController mStatusBarIconController; 82 private final PanelInteractor mPanelInteractor; 83 private final CustomTileAddedRepository mCustomTileAddedRepository; 84 private final DelayableExecutor mBackgroundExecutor; 85 86 private int mMaxBound = DEFAULT_MAX_BOUND; 87 88 @Inject TileServices( QSHost host, @Main Provider<Handler> handlerProvider, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, KeyguardStateController keyguardStateController, CommandQueue commandQueue, StatusBarIconController statusBarIconController, PanelInteractor panelInteractor, CustomTileAddedRepository customTileAddedRepository, @Background DelayableExecutor backgroundExecutor)89 public TileServices( 90 QSHost host, 91 @Main Provider<Handler> handlerProvider, 92 BroadcastDispatcher broadcastDispatcher, 93 UserTracker userTracker, 94 KeyguardStateController keyguardStateController, 95 CommandQueue commandQueue, 96 StatusBarIconController statusBarIconController, 97 PanelInteractor panelInteractor, 98 CustomTileAddedRepository customTileAddedRepository, 99 @Background DelayableExecutor backgroundExecutor) { 100 mHost = host; 101 mKeyguardStateController = keyguardStateController; 102 mContext = mHost.getContext(); 103 mBroadcastDispatcher = broadcastDispatcher; 104 mHandlerProvider = handlerProvider; 105 mMainHandler = mHandlerProvider.get(); 106 mUserTracker = userTracker; 107 mCommandQueue = commandQueue; 108 mStatusBarIconController = statusBarIconController; 109 mCommandQueue.addCallback(mRequestListeningCallback); 110 mPanelInteractor = panelInteractor; 111 mCustomTileAddedRepository = customTileAddedRepository; 112 mBackgroundExecutor = backgroundExecutor; 113 } 114 getContext()115 public Context getContext() { 116 return mContext; 117 } 118 getHost()119 public QSHost getHost() { 120 return mHost; 121 } 122 getTileWrapper(CustomTile tile)123 public TileServiceManager getTileWrapper(CustomTile tile) { 124 ComponentName component = tile.getComponent(); 125 int userId = tile.getUser(); 126 TileServiceManager service = onCreateTileService(component, mBroadcastDispatcher); 127 synchronized (mServices) { 128 mServices.put(tile, service); 129 mTiles.add(userId, component, tile); 130 mTokenMap.put(service.getToken(), tile); 131 } 132 // Makes sure binding only happens after the maps have been populated 133 service.startLifecycleManagerAndAddTile(); 134 return service; 135 } 136 onCreateTileService(ComponentName component, BroadcastDispatcher broadcastDispatcher)137 protected TileServiceManager onCreateTileService(ComponentName component, 138 BroadcastDispatcher broadcastDispatcher) { 139 return new TileServiceManager(this, mHandlerProvider.get(), component, 140 broadcastDispatcher, mUserTracker, mCustomTileAddedRepository, mBackgroundExecutor); 141 } 142 freeService(CustomTile tile, TileServiceManager service)143 public void freeService(CustomTile tile, TileServiceManager service) { 144 synchronized (mServices) { 145 service.setBindAllowed(false); 146 service.handleDestroy(); 147 mServices.remove(tile); 148 mTokenMap.remove(service.getToken()); 149 mTiles.delete(tile.getUser(), tile.getComponent()); 150 final String slot = getStatusBarIconSlotName(tile.getComponent()); 151 mMainHandler.post(() -> mStatusBarIconController.removeIconForTile(slot)); 152 } 153 } 154 setMemoryPressure(boolean memoryPressure)155 public void setMemoryPressure(boolean memoryPressure) { 156 mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND; 157 recalculateBindAllowance(); 158 } 159 recalculateBindAllowance()160 public void recalculateBindAllowance() { 161 final ArrayList<TileServiceManager> services; 162 synchronized (mServices) { 163 services = new ArrayList<>(mServices.values()); 164 } 165 final int N = services.size(); 166 if (N > mMaxBound) { 167 long currentTime = System.currentTimeMillis(); 168 // Precalculate the priority of services for binding. 169 for (int i = 0; i < N; i++) { 170 services.get(i).calculateBindPriority(currentTime); 171 } 172 // Sort them so we can bind the most important first. 173 Collections.sort(services, SERVICE_SORT); 174 } 175 int i; 176 // Allow mMaxBound items to bind. 177 for (i = 0; i < mMaxBound && i < N; i++) { 178 services.get(i).setBindAllowed(true); 179 } 180 // The rest aren't allowed to bind for now. 181 while (i < N) { 182 services.get(i).setBindAllowed(false); 183 i++; 184 } 185 } 186 verifyCaller(CustomTile tile)187 private void verifyCaller(CustomTile tile) { 188 try { 189 String packageName = tile.getComponent().getPackageName(); 190 int uid = mContext.getPackageManager().getPackageUidAsUser(packageName, 191 Binder.getCallingUserHandle().getIdentifier()); 192 if (Binder.getCallingUid() != uid) { 193 throw new SecurityException("Component outside caller's uid"); 194 } 195 } catch (PackageManager.NameNotFoundException e) { 196 throw new SecurityException(e); 197 } 198 } 199 requestListening(ComponentName component)200 private void requestListening(ComponentName component) { 201 synchronized (mServices) { 202 int userId = mUserTracker.getUserId(); 203 CustomTile customTile = getTileForUserAndComponent(userId, component); 204 if (customTile == null) { 205 Log.d(TAG, "Couldn't find tile for " + component + "(" + userId + ")"); 206 return; 207 } 208 TileServiceManager service = mServices.get(customTile); 209 if (service == null) { 210 Log.e( 211 TAG, 212 "No TileServiceManager found in requestListening for tile " 213 + customTile.getTileSpec()); 214 return; 215 } 216 if (!service.isActiveTile()) { 217 return; 218 } 219 service.setBindRequested(true); 220 try { 221 service.getTileService().onStartListening(); 222 } catch (RemoteException e) { 223 } 224 } 225 } 226 227 @Override updateQsTile(Tile tile, IBinder token)228 public void updateQsTile(Tile tile, IBinder token) { 229 CustomTile customTile = getTileForToken(token); 230 if (customTile != null) { 231 verifyCaller(customTile); 232 synchronized (mServices) { 233 final TileServiceManager tileServiceManager = mServices.get(customTile); 234 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) { 235 Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(), 236 new IllegalStateException()); 237 return; 238 } 239 tileServiceManager.clearPendingBind(); 240 tileServiceManager.setLastUpdate(System.currentTimeMillis()); 241 } 242 customTile.updateTileState(tile); 243 customTile.refreshState(); 244 } 245 } 246 247 @Override onStartSuccessful(IBinder token)248 public void onStartSuccessful(IBinder token) { 249 CustomTile customTile = getTileForToken(token); 250 if (customTile != null) { 251 verifyCaller(customTile); 252 synchronized (mServices) { 253 final TileServiceManager tileServiceManager = mServices.get(customTile); 254 // This should not happen as the TileServiceManager should have been started for the 255 // first bind to happen. 256 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) { 257 Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(), 258 new IllegalStateException()); 259 return; 260 } 261 tileServiceManager.clearPendingBind(); 262 } 263 customTile.refreshState(); 264 } 265 } 266 267 @Override onShowDialog(IBinder token)268 public void onShowDialog(IBinder token) { 269 CustomTile customTile = getTileForToken(token); 270 if (customTile != null) { 271 verifyCaller(customTile); 272 customTile.onDialogShown(); 273 mPanelInteractor.forceCollapsePanels(); 274 Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(true); 275 } 276 } 277 278 @Override onDialogHidden(IBinder token)279 public void onDialogHidden(IBinder token) { 280 CustomTile customTile = getTileForToken(token); 281 if (customTile != null) { 282 verifyCaller(customTile); 283 Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(false); 284 customTile.onDialogHidden(); 285 } 286 } 287 288 @Override onStartActivity(IBinder token)289 public void onStartActivity(IBinder token) { 290 CustomTile customTile = getTileForToken(token); 291 if (customTile != null) { 292 verifyCaller(customTile); 293 mPanelInteractor.forceCollapsePanels(); 294 } 295 } 296 297 @Override startActivity(IBinder token, PendingIntent pendingIntent)298 public void startActivity(IBinder token, PendingIntent pendingIntent) { 299 startActivity(getTileForToken(token), pendingIntent); 300 } 301 302 @VisibleForTesting startActivity(CustomTile customTile, PendingIntent pendingIntent)303 protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) { 304 if (customTile != null) { 305 verifyCaller(customTile); 306 customTile.startActivityAndCollapse(pendingIntent); 307 } 308 } 309 310 @Override updateStatusIcon(IBinder token, Icon icon, String contentDescription)311 public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) { 312 CustomTile customTile = getTileForToken(token); 313 if (customTile != null) { 314 verifyCaller(customTile); 315 try { 316 ComponentName componentName = customTile.getComponent(); 317 String packageName = componentName.getPackageName(); 318 UserHandle userHandle = getCallingUserHandle(); 319 PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0, 320 userHandle.getIdentifier()); 321 if (info.applicationInfo.isSystemApp()) { 322 final StatusBarIcon statusIcon = icon != null 323 ? new StatusBarIcon(userHandle, packageName, icon, 0, 0, 324 contentDescription) 325 : null; 326 final String slot = getStatusBarIconSlotName(componentName); 327 mMainHandler.post(new Runnable() { 328 @Override 329 public void run() { 330 mStatusBarIconController.setIconFromTile(slot, statusIcon); 331 } 332 }); 333 } 334 } catch (PackageManager.NameNotFoundException e) { 335 } 336 } 337 } 338 339 @Nullable 340 @Override getTile(IBinder token)341 public Tile getTile(IBinder token) { 342 CustomTile customTile = getTileForToken(token); 343 if (customTile != null) { 344 verifyCaller(customTile); 345 return customTile.getQsTile(); 346 } 347 Log.e(TAG, "Tile for token " + token + "not found. " 348 + "Tiles in map: " + availableTileComponents()); 349 return null; 350 } 351 availableTileComponents()352 private String availableTileComponents() { 353 StringBuilder sb = new StringBuilder("["); 354 synchronized (mServices) { 355 mTokenMap.forEach((iBinder, customTile) -> 356 sb.append(iBinder.toString()) 357 .append(":") 358 .append(customTile.getComponent().flattenToShortString()) 359 .append(":") 360 .append(customTile.getUser()) 361 .append(",")); 362 } 363 sb.append("]"); 364 return sb.toString(); 365 } 366 367 @Override startUnlockAndRun(IBinder token)368 public void startUnlockAndRun(IBinder token) { 369 CustomTile customTile = getTileForToken(token); 370 if (customTile != null) { 371 verifyCaller(customTile); 372 customTile.startUnlockAndRun(); 373 } 374 } 375 376 @Override isLocked()377 public boolean isLocked() { 378 return mKeyguardStateController.isShowing(); 379 } 380 381 @Override isSecure()382 public boolean isSecure() { 383 return mKeyguardStateController.isMethodSecure() && mKeyguardStateController.isShowing(); 384 } 385 386 @Nullable getTileForToken(IBinder token)387 public CustomTile getTileForToken(IBinder token) { 388 synchronized (mServices) { 389 return mTokenMap.get(token); 390 } 391 } 392 393 @Nullable getTileForUserAndComponent(int userId, ComponentName component)394 private CustomTile getTileForUserAndComponent(int userId, ComponentName component) { 395 synchronized (mServices) { 396 return mTiles.get(userId, component); 397 } 398 } 399 destroy()400 public void destroy() { 401 synchronized (mServices) { 402 mServices.values().forEach(service -> service.handleDestroy()); 403 } 404 mCommandQueue.removeCallback(mRequestListeningCallback); 405 } 406 407 /** Returns the slot name that should be used when adding or removing status bar icons. */ getStatusBarIconSlotName(ComponentName componentName)408 private String getStatusBarIconSlotName(ComponentName componentName) { 409 return componentName.getClassName(); 410 } 411 412 413 private final CommandQueue.Callbacks mRequestListeningCallback = new CommandQueue.Callbacks() { 414 @Override 415 public void requestTileServiceListeningState(@NonNull ComponentName componentName) { 416 mMainHandler.post(() -> requestListening(componentName)); 417 } 418 }; 419 420 private static final Comparator<TileServiceManager> SERVICE_SORT = 421 new Comparator<TileServiceManager>() { 422 @Override 423 public int compare(TileServiceManager left, TileServiceManager right) { 424 return -Integer.compare(left.getBindPriority(), right.getBindPriority()); 425 } 426 }; 427 428 } 429