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