1 /* 2 * Copyright (C) 2018 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.appops; 18 19 import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; 20 import static android.hardware.SensorPrivacyManager.Sensors.MICROPHONE; 21 import static android.media.AudioManager.ACTION_MICROPHONE_MUTE_CHANGED; 22 23 import android.app.AppOpsManager; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.media.AudioManager; 29 import android.media.AudioRecordingConfiguration; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.os.UserHandle; 33 import android.permission.PermissionManager; 34 import android.util.ArraySet; 35 import android.util.Log; 36 import android.util.SparseArray; 37 38 import androidx.annotation.WorkerThread; 39 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.systemui.Dumpable; 43 import com.android.systemui.broadcast.BroadcastDispatcher; 44 import com.android.systemui.dagger.SysUISingleton; 45 import com.android.systemui.dagger.qualifiers.Background; 46 import com.android.systemui.dump.DumpManager; 47 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; 48 import com.android.systemui.util.Assert; 49 import com.android.systemui.util.time.SystemClock; 50 51 import java.io.PrintWriter; 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 import javax.inject.Inject; 58 59 /** 60 * Controller to keep track of applications that have requested access to given App Ops 61 * 62 * It can be subscribed to with callbacks. Additionally, it passes on the information to 63 * NotificationPresenter to be displayed to the user. 64 */ 65 @SysUISingleton 66 public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController, 67 AppOpsManager.OnOpActiveChangedListener, 68 AppOpsManager.OnOpNotedInternalListener, IndividualSensorPrivacyController.Callback, 69 Dumpable { 70 71 // This is the minimum time that we will keep AppOps that are noted on record. If multiple 72 // occurrences of the same (op, package, uid) happen in a shorter interval, they will not be 73 // notified to listeners. 74 private static final long NOTED_OP_TIME_DELAY_MS = 5000; 75 private static final String TAG = "AppOpsControllerImpl"; 76 private static final boolean DEBUG = false; 77 78 private final BroadcastDispatcher mDispatcher; 79 private final Context mContext; 80 private final AppOpsManager mAppOps; 81 private final AudioManager mAudioManager; 82 private final IndividualSensorPrivacyController mSensorPrivacyController; 83 private final SystemClock mClock; 84 85 private H mBGHandler; 86 private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); 87 private final SparseArray<Set<Callback>> mCallbacksByCode = new SparseArray<>(); 88 private boolean mListening; 89 private boolean mMicMuted; 90 private boolean mCameraDisabled; 91 92 @GuardedBy("mActiveItems") 93 private final List<AppOpItem> mActiveItems = new ArrayList<>(); 94 @GuardedBy("mNotedItems") 95 private final List<AppOpItem> mNotedItems = new ArrayList<>(); 96 @GuardedBy("mActiveItems") 97 private final SparseArray<ArrayList<AudioRecordingConfiguration>> mRecordingsByUid = 98 new SparseArray<>(); 99 100 protected static final int[] OPS = new int[] { 101 AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION, 102 AppOpsManager.OP_CAMERA, 103 AppOpsManager.OP_PHONE_CALL_CAMERA, 104 AppOpsManager.OP_SYSTEM_ALERT_WINDOW, 105 AppOpsManager.OP_RECORD_AUDIO, 106 AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, 107 AppOpsManager.OP_RECEIVE_EXPLICIT_USER_INTERACTION_AUDIO, 108 AppOpsManager.OP_PHONE_CALL_MICROPHONE, 109 AppOpsManager.OP_COARSE_LOCATION, 110 AppOpsManager.OP_FINE_LOCATION 111 }; 112 113 @Inject AppOpsControllerImpl( Context context, @Background Looper bgLooper, DumpManager dumpManager, AudioManager audioManager, IndividualSensorPrivacyController sensorPrivacyController, BroadcastDispatcher dispatcher, SystemClock clock )114 public AppOpsControllerImpl( 115 Context context, 116 @Background Looper bgLooper, 117 DumpManager dumpManager, 118 AudioManager audioManager, 119 IndividualSensorPrivacyController sensorPrivacyController, 120 BroadcastDispatcher dispatcher, 121 SystemClock clock 122 ) { 123 mDispatcher = dispatcher; 124 mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 125 mBGHandler = new H(bgLooper); 126 final int numOps = OPS.length; 127 for (int i = 0; i < numOps; i++) { 128 mCallbacksByCode.put(OPS[i], new ArraySet<>()); 129 } 130 mAudioManager = audioManager; 131 mSensorPrivacyController = sensorPrivacyController; 132 mMicMuted = audioManager.isMicrophoneMute() 133 || mSensorPrivacyController.isSensorBlocked(MICROPHONE); 134 mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA); 135 mContext = context; 136 mClock = clock; 137 dumpManager.registerDumpable(TAG, this); 138 } 139 140 @VisibleForTesting setBGHandler(H handler)141 protected void setBGHandler(H handler) { 142 mBGHandler = handler; 143 } 144 145 @VisibleForTesting setListening(boolean listening)146 protected void setListening(boolean listening) { 147 mListening = listening; 148 if (listening) { 149 // System UI could be restarted while ops are active, so fetch the currently active ops 150 // once System UI starts listening again. 151 fetchCurrentActiveOps(); 152 153 mAppOps.startWatchingActive(OPS, this); 154 mAppOps.startWatchingNoted(OPS, this); 155 mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler); 156 mSensorPrivacyController.addCallback(this); 157 158 mMicMuted = mAudioManager.isMicrophoneMute() 159 || mSensorPrivacyController.isSensorBlocked(MICROPHONE); 160 mCameraDisabled = mSensorPrivacyController.isSensorBlocked(CAMERA); 161 162 mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged( 163 mAudioManager.getActiveRecordingConfigurations())); 164 mDispatcher.registerReceiverWithHandler(this, 165 new IntentFilter(ACTION_MICROPHONE_MUTE_CHANGED), mBGHandler); 166 167 } else { 168 mAppOps.stopWatchingActive(this); 169 mAppOps.stopWatchingNoted(this); 170 mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback); 171 mSensorPrivacyController.removeCallback(this); 172 173 mBGHandler.removeCallbacksAndMessages(null); // null removes all 174 mDispatcher.unregisterReceiver(this); 175 synchronized (mActiveItems) { 176 mActiveItems.clear(); 177 mRecordingsByUid.clear(); 178 } 179 synchronized (mNotedItems) { 180 mNotedItems.clear(); 181 } 182 } 183 } 184 fetchCurrentActiveOps()185 private void fetchCurrentActiveOps() { 186 List<AppOpsManager.PackageOps> packageOps = mAppOps.getPackagesForOps(OPS); 187 for (AppOpsManager.PackageOps op : packageOps) { 188 for (AppOpsManager.OpEntry entry : op.getOps()) { 189 for (Map.Entry<String, AppOpsManager.AttributedOpEntry> attributedOpEntry : 190 entry.getAttributedOpEntries().entrySet()) { 191 if (attributedOpEntry.getValue().isRunning()) { 192 onOpActiveChanged( 193 entry.getOpStr(), 194 op.getUid(), 195 op.getPackageName(), 196 /* attributionTag= */ attributedOpEntry.getKey(), 197 /* active= */ true, 198 // AppOpsManager doesn't have a way to fetch attribution flags or 199 // chain ID given an op entry, so default them to none. 200 AppOpsManager.ATTRIBUTION_FLAGS_NONE, 201 AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); 202 } 203 } 204 } 205 } 206 } 207 208 /** 209 * Adds a callback that will get notifified when an AppOp of the type the controller tracks 210 * changes 211 * 212 * @param callback Callback to report changes 213 * @param opsCodes App Ops the callback is interested in checking 214 * 215 * @see #removeCallback(int[], Callback) 216 */ 217 @Override addCallback(int[] opsCodes, AppOpsController.Callback callback)218 public void addCallback(int[] opsCodes, AppOpsController.Callback callback) { 219 boolean added = false; 220 final int numCodes = opsCodes.length; 221 for (int i = 0; i < numCodes; i++) { 222 if (mCallbacksByCode.contains(opsCodes[i])) { 223 mCallbacksByCode.get(opsCodes[i]).add(callback); 224 added = true; 225 } else { 226 if (DEBUG) Log.wtf(TAG, "APP_OP " + opsCodes[i] + " not supported"); 227 } 228 } 229 if (added) mCallbacks.add(callback); 230 if (!mCallbacks.isEmpty()) setListening(true); 231 } 232 233 /** 234 * Removes a callback from those notified when an AppOp of the type the controller tracks 235 * changes 236 * 237 * @param callback Callback to stop reporting changes 238 * @param opsCodes App Ops the callback was interested in checking 239 * 240 * @see #addCallback(int[], Callback) 241 */ 242 @Override removeCallback(int[] opsCodes, AppOpsController.Callback callback)243 public void removeCallback(int[] opsCodes, AppOpsController.Callback callback) { 244 final int numCodes = opsCodes.length; 245 for (int i = 0; i < numCodes; i++) { 246 if (mCallbacksByCode.contains(opsCodes[i])) { 247 mCallbacksByCode.get(opsCodes[i]).remove(callback); 248 } 249 } 250 mCallbacks.remove(callback); 251 if (mCallbacks.isEmpty()) setListening(false); 252 } 253 254 // Find item number in list, only call if the list passed is locked getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, String packageName)255 private AppOpItem getAppOpItemLocked(List<AppOpItem> appOpList, int code, int uid, 256 String packageName) { 257 final int itemsQ = appOpList.size(); 258 for (int i = 0; i < itemsQ; i++) { 259 AppOpItem item = appOpList.get(i); 260 if (item.getCode() == code && item.getUid() == uid 261 && item.getPackageName().equals(packageName)) { 262 return item; 263 } 264 } 265 return null; 266 } 267 updateActives(int code, int uid, String packageName, boolean active)268 private boolean updateActives(int code, int uid, String packageName, boolean active) { 269 synchronized (mActiveItems) { 270 AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); 271 if (item == null && active) { 272 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime()); 273 if (isOpMicrophone(code)) { 274 item.setDisabled(isAnyRecordingPausedLocked(uid)); 275 } else if (isOpCamera(code)) { 276 item.setDisabled(mCameraDisabled); 277 } 278 mActiveItems.add(item); 279 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 280 return !item.isDisabled(); 281 } else if (item != null && !active) { 282 mActiveItems.remove(item); 283 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 284 return true; 285 } 286 return false; 287 } 288 } 289 removeNoted(int code, int uid, String packageName)290 private void removeNoted(int code, int uid, String packageName) { 291 AppOpItem item; 292 synchronized (mNotedItems) { 293 item = getAppOpItemLocked(mNotedItems, code, uid, packageName); 294 if (item == null) return; 295 mNotedItems.remove(item); 296 if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); 297 } 298 boolean active; 299 // Check if the item is also active 300 synchronized (mActiveItems) { 301 active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; 302 } 303 if (!active) { 304 notifySuscribersWorker(code, uid, packageName, false); 305 } 306 } 307 addNoted(int code, int uid, String packageName)308 private boolean addNoted(int code, int uid, String packageName) { 309 AppOpItem item; 310 boolean createdNew = false; 311 synchronized (mNotedItems) { 312 item = getAppOpItemLocked(mNotedItems, code, uid, packageName); 313 if (item == null) { 314 item = new AppOpItem(code, uid, packageName, mClock.elapsedRealtime()); 315 mNotedItems.add(item); 316 if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); 317 createdNew = true; 318 } 319 } 320 // We should keep this so we make sure it cannot time out. 321 mBGHandler.removeCallbacksAndMessages(item); 322 mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS); 323 return createdNew; 324 } 325 isUserVisible(String packageName)326 private boolean isUserVisible(String packageName) { 327 return PermissionManager.shouldShowPackageForIndicatorCached(mContext, packageName); 328 } 329 330 @WorkerThread getActiveAppOps()331 public List<AppOpItem> getActiveAppOps() { 332 return getActiveAppOps(false); 333 } 334 335 /** 336 * Returns a copy of the list containing all the active AppOps that the controller tracks. 337 * 338 * Call from a worker thread as it may perform long operations. 339 * 340 * @return List of active AppOps information 341 */ 342 @WorkerThread getActiveAppOps(boolean showPaused)343 public List<AppOpItem> getActiveAppOps(boolean showPaused) { 344 return getActiveAppOpsForUser(UserHandle.USER_ALL, showPaused); 345 } 346 347 /** 348 * Returns a copy of the list containing all the active AppOps that the controller tracks, for 349 * a given user id. 350 * 351 * Call from a worker thread as it may perform long operations. 352 * 353 * @param userId User id to track, can be {@link UserHandle#USER_ALL} 354 * 355 * @return List of active AppOps information for that user id 356 */ 357 @WorkerThread getActiveAppOpsForUser(int userId, boolean showPaused)358 public List<AppOpItem> getActiveAppOpsForUser(int userId, boolean showPaused) { 359 Assert.isNotMainThread(); 360 List<AppOpItem> list = new ArrayList<>(); 361 synchronized (mActiveItems) { 362 final int numActiveItems = mActiveItems.size(); 363 for (int i = 0; i < numActiveItems; i++) { 364 AppOpItem item = mActiveItems.get(i); 365 if ((userId == UserHandle.USER_ALL 366 || UserHandle.getUserId(item.getUid()) == userId) 367 && isUserVisible(item.getPackageName()) 368 && (showPaused || !item.isDisabled())) { 369 list.add(item); 370 } 371 } 372 } 373 synchronized (mNotedItems) { 374 final int numNotedItems = mNotedItems.size(); 375 for (int i = 0; i < numNotedItems; i++) { 376 AppOpItem item = mNotedItems.get(i); 377 if ((userId == UserHandle.USER_ALL 378 || UserHandle.getUserId(item.getUid()) == userId) 379 && isUserVisible(item.getPackageName())) { 380 list.add(item); 381 } 382 } 383 } 384 return list; 385 } 386 notifySuscribers(int code, int uid, String packageName, boolean active)387 private void notifySuscribers(int code, int uid, String packageName, boolean active) { 388 mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active)); 389 } 390 391 /** 392 * Required to override, delegate to other. Should not be called. 393 */ onOpActiveChanged(String op, int uid, String packageName, boolean active)394 public void onOpActiveChanged(String op, int uid, String packageName, boolean active) { 395 onOpActiveChanged(op, uid, packageName, null, active, 396 AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE); 397 } 398 399 // Get active app ops, and check if their attributions are trusted 400 @Override onOpActiveChanged(String op, int uid, String packageName, String attributionTag, boolean active, int attributionFlags, int attributionChainId)401 public void onOpActiveChanged(String op, int uid, String packageName, String attributionTag, 402 boolean active, int attributionFlags, int attributionChainId) { 403 int code = AppOpsManager.strOpToOp(op); 404 if (DEBUG) { 405 Log.w(TAG, String.format("onActiveChanged(%d,%d,%s,%s,%d,%d)", code, uid, packageName, 406 Boolean.toString(active), attributionChainId, attributionFlags)); 407 } 408 if (active && attributionChainId != AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE 409 && attributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE 410 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_ACCESSOR) == 0 411 && (attributionFlags & AppOpsManager.ATTRIBUTION_FLAG_TRUSTED) == 0) { 412 // if this attribution chain isn't trusted, and this isn't the accessor, do not show it. 413 return; 414 } 415 boolean activeChanged = updateActives(code, uid, packageName, active); 416 if (!activeChanged) return; // early return 417 // Check if the item is also noted, in that case, there's no update. 418 boolean alsoNoted; 419 synchronized (mNotedItems) { 420 alsoNoted = getAppOpItemLocked(mNotedItems, code, uid, packageName) != null; 421 } 422 // If active is true, we only send the update if the op is not actively noted (already true) 423 // If active is false, we only send the update if the op is not actively noted (prevent 424 // early removal) 425 if (!alsoNoted) { 426 notifySuscribers(code, uid, packageName, active); 427 } 428 } 429 430 @Override onOpNoted(int code, int uid, String packageName, String attributionTag, @AppOpsManager.OpFlags int flags, @AppOpsManager.Mode int result)431 public void onOpNoted(int code, int uid, String packageName, 432 String attributionTag, @AppOpsManager.OpFlags int flags, 433 @AppOpsManager.Mode int result) { 434 if (DEBUG) { 435 Log.w(TAG, "Noted op: " + code + " with result " 436 + AppOpsManager.MODE_NAMES[result] + " for package " + packageName); 437 } 438 if (result != AppOpsManager.MODE_ALLOWED) return; 439 boolean notedAdded = addNoted(code, uid, packageName); 440 if (!notedAdded) return; // early return 441 boolean alsoActive; 442 synchronized (mActiveItems) { 443 alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; 444 } 445 if (!alsoActive) { 446 notifySuscribers(code, uid, packageName, true); 447 } 448 } 449 notifySuscribersWorker(int code, int uid, String packageName, boolean active)450 private void notifySuscribersWorker(int code, int uid, String packageName, boolean active) { 451 if (mCallbacksByCode.contains(code) && isUserVisible(packageName)) { 452 if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); 453 for (Callback cb: mCallbacksByCode.get(code)) { 454 cb.onActiveStateChanged(code, uid, packageName, active); 455 } 456 } 457 } 458 459 @Override dump(PrintWriter pw, String[] args)460 public void dump(PrintWriter pw, String[] args) { 461 pw.println("AppOpsController state:"); 462 pw.println(" Listening: " + mListening); 463 pw.println(" Active Items:"); 464 for (int i = 0; i < mActiveItems.size(); i++) { 465 final AppOpItem item = mActiveItems.get(i); 466 pw.print(" "); pw.println(item.toString()); 467 } 468 pw.println(" Noted Items:"); 469 for (int i = 0; i < mNotedItems.size(); i++) { 470 final AppOpItem item = mNotedItems.get(i); 471 pw.print(" "); pw.println(item.toString()); 472 } 473 474 } 475 isAnyRecordingPausedLocked(int uid)476 private boolean isAnyRecordingPausedLocked(int uid) { 477 if (mMicMuted) { 478 return true; 479 } 480 List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid); 481 if (configs == null) return false; 482 int configsNum = configs.size(); 483 for (int i = 0; i < configsNum; i++) { 484 AudioRecordingConfiguration config = configs.get(i); 485 if (config.isClientSilenced()) return true; 486 } 487 return false; 488 } 489 updateSensorDisabledStatus()490 private void updateSensorDisabledStatus() { 491 synchronized (mActiveItems) { 492 int size = mActiveItems.size(); 493 for (int i = 0; i < size; i++) { 494 AppOpItem item = mActiveItems.get(i); 495 496 boolean paused = false; 497 if (isOpMicrophone(item.getCode())) { 498 paused = isAnyRecordingPausedLocked(item.getUid()); 499 } else if (isOpCamera(item.getCode())) { 500 paused = mCameraDisabled; 501 } 502 503 if (item.isDisabled() != paused) { 504 item.setDisabled(paused); 505 notifySuscribers( 506 item.getCode(), 507 item.getUid(), 508 item.getPackageName(), 509 !item.isDisabled() 510 ); 511 } 512 } 513 } 514 } 515 516 private AudioManager.AudioRecordingCallback mAudioRecordingCallback = 517 new AudioManager.AudioRecordingCallback() { 518 @Override 519 public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) { 520 synchronized (mActiveItems) { 521 mRecordingsByUid.clear(); 522 final int recordingsCount = configs.size(); 523 for (int i = 0; i < recordingsCount; i++) { 524 AudioRecordingConfiguration recording = configs.get(i); 525 526 ArrayList<AudioRecordingConfiguration> recordings = mRecordingsByUid.get( 527 recording.getClientUid()); 528 if (recordings == null) { 529 recordings = new ArrayList<>(); 530 mRecordingsByUid.put(recording.getClientUid(), recordings); 531 } 532 recordings.add(recording); 533 } 534 } 535 updateSensorDisabledStatus(); 536 } 537 }; 538 539 @Override onReceive(Context context, Intent intent)540 public void onReceive(Context context, Intent intent) { 541 mMicMuted = mAudioManager.isMicrophoneMute() 542 || mSensorPrivacyController.isSensorBlocked(MICROPHONE); 543 updateSensorDisabledStatus(); 544 } 545 546 @Override onSensorBlockedChanged(int sensor, boolean blocked)547 public void onSensorBlockedChanged(int sensor, boolean blocked) { 548 mBGHandler.post(() -> { 549 if (sensor == CAMERA) { 550 mCameraDisabled = blocked; 551 } else if (sensor == MICROPHONE) { 552 mMicMuted = mAudioManager.isMicrophoneMute() || blocked; 553 } 554 updateSensorDisabledStatus(); 555 }); 556 } 557 558 @Override isMicMuted()559 public boolean isMicMuted() { 560 return mMicMuted; 561 } 562 isOpCamera(int op)563 private boolean isOpCamera(int op) { 564 return op == AppOpsManager.OP_CAMERA || op == AppOpsManager.OP_PHONE_CALL_CAMERA; 565 } 566 isOpMicrophone(int op)567 private boolean isOpMicrophone(int op) { 568 return op == AppOpsManager.OP_RECORD_AUDIO || op == AppOpsManager.OP_PHONE_CALL_MICROPHONE 569 || op == AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; 570 } 571 572 protected class H extends Handler { H(Looper looper)573 H(Looper looper) { 574 super(looper); 575 } 576 scheduleRemoval(AppOpItem item, long timeToRemoval)577 public void scheduleRemoval(AppOpItem item, long timeToRemoval) { 578 removeCallbacksAndMessages(item); 579 postDelayed(new Runnable() { 580 @Override 581 public void run() { 582 removeNoted(item.getCode(), item.getUid(), item.getPackageName()); 583 } 584 }, item, timeToRemoval); 585 } 586 } 587 } 588