1 /* 2 * Copyright (C) 2022 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.server.wm; 18 19 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.app.ActivityManager; 24 import android.os.Environment; 25 import android.os.SystemProperties; 26 import android.os.Trace; 27 import android.util.ArraySet; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 import android.window.TaskSnapshot; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.server.LocalServices; 34 import com.android.server.pm.UserManagerInternal; 35 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; 36 37 import java.io.File; 38 import java.util.ArrayList; 39 40 /** 41 * When an app token becomes invisible, we take a snapshot (bitmap) and put it into our cache. 42 * Internally we use gralloc buffers to be able to draw them wherever we like without any copying. 43 * <p> 44 * System applications may retrieve a snapshot to represent the current state of an activity, and 45 * draw them in their own process. 46 * <p> 47 * Unlike TaskSnapshotController, we only keep one activity snapshot for a visible task in the 48 * cache. Which should largely reduce the memory usage. 49 * <p> 50 * To access this class, acquire the global window manager lock. 51 */ 52 class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord, 53 ActivitySnapshotCache> { 54 private static final boolean DEBUG = false; 55 private static final String TAG = AbsAppSnapshotController.TAG; 56 // Maximum persisted snapshot count on disk. 57 private static final int MAX_PERSIST_SNAPSHOT_COUNT = 20; 58 59 static final String SNAPSHOTS_DIRNAME = "activity_snapshots"; 60 61 /** 62 * The pending activities which should remove snapshot from memory when process transition 63 * finish. 64 */ 65 @VisibleForTesting 66 final ArraySet<ActivityRecord> mPendingRemoveActivity = new ArraySet<>(); 67 68 /** 69 * The pending activities which should delete snapshot files when process transition finish. 70 */ 71 @VisibleForTesting 72 final ArraySet<ActivityRecord> mPendingDeleteActivity = new ArraySet<>(); 73 74 /** 75 * The pending activities which should load snapshot from disk when process transition finish. 76 */ 77 @VisibleForTesting 78 final ArraySet<ActivityRecord> mPendingLoadActivity = new ArraySet<>(); 79 80 private final ArraySet<ActivityRecord> mOnBackPressedActivities = new ArraySet<>(); 81 82 private final ArrayList<ActivityRecord> mTmpBelowActivities = new ArrayList<>(); 83 private final ArrayList<WindowContainer> mTmpTransitionParticipants = new ArrayList<>(); 84 private final SnapshotPersistQueue mSnapshotPersistQueue; 85 private final PersistInfoProvider mPersistInfoProvider; 86 private final AppSnapshotLoader mSnapshotLoader; 87 88 /** 89 * File information holders, to make the sequence align, always update status of 90 * mUserSavedFiles/mSavedFilesInOrder before persist file from mPersister. 91 */ 92 private final SparseArray<SparseArray<UserSavedFile>> mUserSavedFiles = new SparseArray<>(); 93 // Keep sorted with create timeline. 94 private final ArrayList<UserSavedFile> mSavedFilesInOrder = new ArrayList<>(); 95 private final TaskSnapshotPersister mPersister; 96 ActivitySnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue)97 ActivitySnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue) { 98 super(service); 99 mSnapshotPersistQueue = persistQueue; 100 mPersistInfoProvider = createPersistInfoProvider(service, 101 Environment::getDataSystemCeDirectory); 102 mPersister = new TaskSnapshotPersister(persistQueue, mPersistInfoProvider); 103 mSnapshotLoader = new AppSnapshotLoader(mPersistInfoProvider); 104 initialize(new ActivitySnapshotCache(service)); 105 106 final boolean snapshotEnabled = 107 !service.mContext 108 .getResources() 109 .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots) 110 && isSnapshotEnabled() 111 && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go 112 setSnapshotEnabled(snapshotEnabled); 113 } 114 115 @Override initSnapshotScale()116 protected float initSnapshotScale() { 117 final float config = mService.mContext.getResources().getFloat( 118 com.android.internal.R.dimen.config_resActivitySnapshotScale); 119 return Math.max(Math.min(config, 1f), 0.1f); 120 } 121 122 // TODO remove when enabled isSnapshotEnabled()123 static boolean isSnapshotEnabled() { 124 return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0; 125 } 126 createPersistInfoProvider( WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver)127 static PersistInfoProvider createPersistInfoProvider( 128 WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) { 129 // Don't persist reduced file, instead we only persist the "HighRes" bitmap which has 130 // already scaled with #initSnapshotScale 131 final boolean use16BitFormat = service.mContext.getResources().getBoolean( 132 com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat); 133 return new PersistInfoProvider(resolver, SNAPSHOTS_DIRNAME, 134 false /* enableLowResSnapshots */, 0 /* lowResScaleFactor */, use16BitFormat); 135 } 136 137 /** Retrieves a snapshot for an activity from cache. */ 138 @Nullable getSnapshot(ActivityRecord ar)139 TaskSnapshot getSnapshot(ActivityRecord ar) { 140 final int code = getSystemHashCode(ar); 141 return mCache.getSnapshot(code); 142 } 143 cleanUpUserFiles(int userId)144 private void cleanUpUserFiles(int userId) { 145 synchronized (mSnapshotPersistQueue.getLock()) { 146 mSnapshotPersistQueue.sendToQueueLocked( 147 new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) { 148 @Override 149 boolean isReady() { 150 final UserManagerInternal mUserManagerInternal = 151 LocalServices.getService(UserManagerInternal.class); 152 return mUserManagerInternal.isUserUnlocked(userId); 153 } 154 155 @Override 156 void write() { 157 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "cleanUpUserFiles"); 158 final File file = mPersistInfoProvider.getDirectory(userId); 159 if (file.exists()) { 160 final File[] contents = file.listFiles(); 161 if (contents != null) { 162 for (int i = contents.length - 1; i >= 0; i--) { 163 contents[i].delete(); 164 } 165 } 166 } 167 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 168 } 169 }); 170 } 171 } 172 addOnBackPressedActivity(ActivityRecord ar)173 void addOnBackPressedActivity(ActivityRecord ar) { 174 if (shouldDisableSnapshots()) { 175 return; 176 } 177 mOnBackPressedActivities.add(ar); 178 } 179 clearOnBackPressedActivities()180 void clearOnBackPressedActivities() { 181 if (shouldDisableSnapshots()) { 182 return; 183 } 184 mOnBackPressedActivities.clear(); 185 } 186 187 /** 188 * Prepare to collect any change for snapshots processing. Clear all temporary fields. 189 */ beginSnapshotProcess()190 void beginSnapshotProcess() { 191 if (shouldDisableSnapshots()) { 192 return; 193 } 194 resetTmpFields(); 195 } 196 197 /** 198 * End collect any change for snapshots processing, start process data. 199 */ endSnapshotProcess()200 void endSnapshotProcess() { 201 if (shouldDisableSnapshots()) { 202 return; 203 } 204 for (int i = mOnBackPressedActivities.size() - 1; i >= 0; --i) { 205 handleActivityTransition(mOnBackPressedActivities.valueAt(i)); 206 } 207 mOnBackPressedActivities.clear(); 208 mTmpTransitionParticipants.clear(); 209 postProcess(); 210 } 211 212 @VisibleForTesting resetTmpFields()213 void resetTmpFields() { 214 mPendingRemoveActivity.clear(); 215 mPendingDeleteActivity.clear(); 216 mPendingLoadActivity.clear(); 217 } 218 219 /** 220 * Start process all pending activities for a transition. 221 */ postProcess()222 private void postProcess() { 223 if (DEBUG) { 224 Slog.d(TAG, "ActivitySnapshotController#postProcess result:" 225 + " remove " + mPendingRemoveActivity 226 + " delete " + mPendingDeleteActivity 227 + " load " + mPendingLoadActivity); 228 } 229 // load snapshot to cache 230 for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) { 231 final ActivityRecord ar = mPendingLoadActivity.valueAt(i); 232 final int code = getSystemHashCode(ar); 233 final int userId = ar.mUserId; 234 if (mCache.getSnapshot(code) != null) { 235 // already in cache, skip 236 continue; 237 } 238 if (containsFile(code, userId)) { 239 synchronized (mSnapshotPersistQueue.getLock()) { 240 mSnapshotPersistQueue.sendToQueueLocked( 241 new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) { 242 @Override 243 void write() { 244 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, 245 "load_activity_snapshot"); 246 final TaskSnapshot snapshot = mSnapshotLoader.loadTask(code, 247 userId, false /* loadLowResolutionBitmap */); 248 synchronized (mService.getWindowManagerLock()) { 249 if (snapshot != null && !ar.finishing) { 250 mCache.putSnapshot(ar, snapshot); 251 } 252 } 253 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 254 } 255 }); 256 } 257 } 258 } 259 // clear mTmpRemoveActivity from cache 260 for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) { 261 final ActivityRecord ar = mPendingRemoveActivity.valueAt(i); 262 final int code = getSystemHashCode(ar); 263 mCache.onIdRemoved(code); 264 } 265 // clear snapshot on cache and delete files 266 for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) { 267 final ActivityRecord ar = mPendingDeleteActivity.valueAt(i); 268 final int code = getSystemHashCode(ar); 269 mCache.onIdRemoved(code); 270 removeIfUserSavedFileExist(code, ar.mUserId); 271 } 272 // don't keep any reference 273 resetTmpFields(); 274 } 275 recordSnapshot(ActivityRecord activity)276 void recordSnapshot(ActivityRecord activity) { 277 if (shouldDisableSnapshots()) { 278 return; 279 } 280 if (DEBUG) { 281 Slog.d(TAG, "ActivitySnapshotController#recordSnapshot " + activity); 282 } 283 final TaskSnapshot snapshot = recordSnapshotInner(activity, false /* allowSnapshotHome */); 284 if (snapshot != null) { 285 final int code = getSystemHashCode(activity); 286 addUserSavedFile(code, activity.mUserId, snapshot); 287 } 288 } 289 290 /** 291 * Called when the visibility of an app changes outside the regular app transition flow. 292 */ notifyAppVisibilityChanged(ActivityRecord ar, boolean visible)293 void notifyAppVisibilityChanged(ActivityRecord ar, boolean visible) { 294 if (shouldDisableSnapshots()) { 295 return; 296 } 297 final Task task = ar.getTask(); 298 if (task == null) { 299 return; 300 } 301 // Doesn't need to capture activity snapshot when it converts from translucent. 302 if (!visible) { 303 resetTmpFields(); 304 addBelowActivityIfExist(ar, mPendingRemoveActivity, false, 305 "remove-snapshot"); 306 postProcess(); 307 } 308 } 309 getSystemHashCode(ActivityRecord activity)310 private static int getSystemHashCode(ActivityRecord activity) { 311 return System.identityHashCode(activity); 312 } 313 314 @VisibleForTesting handleTransitionFinish(@onNull ArrayList<WindowContainer> windows)315 void handleTransitionFinish(@NonNull ArrayList<WindowContainer> windows) { 316 mTmpTransitionParticipants.clear(); 317 mTmpTransitionParticipants.addAll(windows); 318 for (int i = mTmpTransitionParticipants.size() - 1; i >= 0; --i) { 319 final WindowContainer next = mTmpTransitionParticipants.get(i); 320 if (next.asTask() != null) { 321 handleTaskTransition(next.asTask()); 322 } else if (next.asTaskFragment() != null) { 323 final TaskFragment tf = next.asTaskFragment(); 324 final ActivityRecord ar = tf.getTopMostActivity(); 325 if (ar != null) { 326 handleActivityTransition(ar); 327 } 328 } else if (next.asActivityRecord() != null) { 329 handleActivityTransition(next.asActivityRecord()); 330 } 331 } 332 } 333 handleActivityTransition(@onNull ActivityRecord ar)334 private void handleActivityTransition(@NonNull ActivityRecord ar) { 335 if (shouldDisableSnapshots()) { 336 return; 337 } 338 if (ar.isVisibleRequested()) { 339 mPendingDeleteActivity.add(ar); 340 // load next one if exists. 341 addBelowActivityIfExist(ar, mPendingLoadActivity, true, "load-snapshot"); 342 } else { 343 // remove the snapshot for the one below close 344 addBelowActivityIfExist(ar, mPendingRemoveActivity, true, "remove-snapshot"); 345 } 346 } 347 handleTaskTransition(Task task)348 private void handleTaskTransition(Task task) { 349 if (shouldDisableSnapshots()) { 350 return; 351 } 352 final ActivityRecord topActivity = task.getTopMostActivity(); 353 if (topActivity == null) { 354 return; 355 } 356 if (task.isVisibleRequested()) { 357 // this is open task transition 358 // load the N - 1 to cache 359 addBelowActivityIfExist(topActivity, mPendingLoadActivity, true, "load-snapshot"); 360 // Move the activities to top of mSavedFilesInOrder, so when purge happen, there 361 // will trim the persisted files from the most non-accessed. 362 adjustSavedFileOrder(task); 363 } else { 364 // this is close task transition 365 // remove the N - 1 from cache 366 addBelowActivityIfExist(topActivity, mPendingRemoveActivity, true, "remove-snapshot"); 367 } 368 } 369 370 /** 371 * Add the top -1 activity to a set if it exists. 372 * @param inTransition true if the activity must participant in transition. 373 */ addBelowActivityIfExist(ActivityRecord currentActivity, ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage)374 private void addBelowActivityIfExist(ActivityRecord currentActivity, 375 ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage) { 376 getActivityBelow(currentActivity, inTransition, mTmpBelowActivities); 377 for (int i = mTmpBelowActivities.size() - 1; i >= 0; --i) { 378 set.add(mTmpBelowActivities.get(i)); 379 if (DEBUG) { 380 Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist " 381 + mTmpBelowActivities.get(i) + " from " + debugMessage); 382 } 383 } 384 mTmpBelowActivities.clear(); 385 } 386 getActivityBelow(ActivityRecord currentActivity, boolean inTransition, ArrayList<ActivityRecord> result)387 private void getActivityBelow(ActivityRecord currentActivity, boolean inTransition, 388 ArrayList<ActivityRecord> result) { 389 final Task currentTask = currentActivity.getTask(); 390 if (currentTask == null) { 391 return; 392 } 393 final ActivityRecord initPrev = currentTask.getActivityBelow(currentActivity); 394 if (initPrev == null) { 395 return; 396 } 397 final TaskFragment currTF = currentActivity.getTaskFragment(); 398 final TaskFragment prevTF = initPrev.getTaskFragment(); 399 final TaskFragment prevAdjacentTF = prevTF != null 400 ? prevTF.getAdjacentTaskFragment() : null; 401 if (currTF == prevTF && currTF != null || prevAdjacentTF == null) { 402 // Current activity and previous one is in the same task fragment, or 403 // previous activity is not in a task fragment, or 404 // previous activity's task fragment doesn't adjacent to any others. 405 if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { 406 result.add(initPrev); 407 } 408 return; 409 } 410 411 if (prevAdjacentTF == currTF) { 412 // previous activity A is adjacent to current activity B. 413 // Try to find anyone below previous activityA, which are C and D if exists. 414 // A | B 415 // C (| D) 416 getActivityBelow(initPrev, inTransition, result); 417 } else { 418 // previous activity C isn't adjacent to current activity A. 419 // A 420 // B | C 421 final Task prevAdjacentTask = prevAdjacentTF.getTask(); 422 if (prevAdjacentTask == currentTask) { 423 final int currentIndex = currTF != null 424 ? currentTask.mChildren.indexOf(currTF) 425 : currentTask.mChildren.indexOf(currentActivity); 426 final int prevAdjacentIndex = 427 prevAdjacentTask.mChildren.indexOf(prevAdjacentTF); 428 // prevAdjacentTF already above currentActivity 429 if (prevAdjacentIndex > currentIndex) { 430 return; 431 } 432 } 433 if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) { 434 result.add(initPrev); 435 } 436 // prevAdjacentTF is adjacent to another one 437 final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity(); 438 if (prevAdjacentActivity != null && (!inTransition 439 || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) { 440 result.add(prevAdjacentActivity); 441 } 442 } 443 } 444 isInParticipant(ActivityRecord ar, ArrayList<WindowContainer> transitionParticipants)445 static boolean isInParticipant(ActivityRecord ar, 446 ArrayList<WindowContainer> transitionParticipants) { 447 for (int i = transitionParticipants.size() - 1; i >= 0; --i) { 448 final WindowContainer wc = transitionParticipants.get(i); 449 if (ar == wc || ar.isDescendantOf(wc)) { 450 return true; 451 } 452 } 453 return false; 454 } 455 adjustSavedFileOrder(Task nextTopTask)456 private void adjustSavedFileOrder(Task nextTopTask) { 457 final int userId = nextTopTask.mUserId; 458 nextTopTask.forAllActivities(ar -> { 459 final int code = getSystemHashCode(ar); 460 final UserSavedFile usf = getUserFiles(userId).get(code); 461 if (usf != null) { 462 mSavedFilesInOrder.remove(usf); 463 mSavedFilesInOrder.add(usf); 464 } 465 }, false /* traverseTopToBottom */); 466 } 467 468 @Override onAppRemoved(ActivityRecord activity)469 void onAppRemoved(ActivityRecord activity) { 470 if (shouldDisableSnapshots()) { 471 return; 472 } 473 super.onAppRemoved(activity); 474 final int code = getSystemHashCode(activity); 475 removeIfUserSavedFileExist(code, activity.mUserId); 476 if (DEBUG) { 477 Slog.d(TAG, "ActivitySnapshotController#onAppRemoved delete snapshot " + activity); 478 } 479 } 480 481 @Override onAppDied(ActivityRecord activity)482 void onAppDied(ActivityRecord activity) { 483 if (shouldDisableSnapshots()) { 484 return; 485 } 486 super.onAppDied(activity); 487 final int code = getSystemHashCode(activity); 488 removeIfUserSavedFileExist(code, activity.mUserId); 489 if (DEBUG) { 490 Slog.d(TAG, "ActivitySnapshotController#onAppDied delete snapshot " + activity); 491 } 492 } 493 494 @Override getTopActivity(ActivityRecord activity)495 ActivityRecord getTopActivity(ActivityRecord activity) { 496 return activity; 497 } 498 499 @Override getTopFullscreenActivity(ActivityRecord activity)500 ActivityRecord getTopFullscreenActivity(ActivityRecord activity) { 501 final WindowState win = activity.findMainWindow(); 502 return (win != null && win.mAttrs.isFullscreen()) ? activity : null; 503 } 504 505 @Override getTaskDescription(ActivityRecord object)506 ActivityManager.TaskDescription getTaskDescription(ActivityRecord object) { 507 return object.taskDescription; 508 } 509 510 /** 511 * Find the window for a given activity to take a snapshot. During app transitions, trampoline 512 * activities can appear in the children, but should be ignored. 513 */ 514 @Override findAppTokenForSnapshot(ActivityRecord activity)515 protected ActivityRecord findAppTokenForSnapshot(ActivityRecord activity) { 516 if (activity == null) { 517 return null; 518 } 519 return activity.canCaptureSnapshot() ? activity : null; 520 } 521 522 @Override use16BitFormat()523 protected boolean use16BitFormat() { 524 return mPersistInfoProvider.use16BitFormat(); 525 } 526 527 @NonNull getUserFiles(int userId)528 private SparseArray<UserSavedFile> getUserFiles(int userId) { 529 if (mUserSavedFiles.get(userId) == null) { 530 mUserSavedFiles.put(userId, new SparseArray<>()); 531 // This is the first time this user attempt to access snapshot, clear up the disk. 532 cleanUpUserFiles(userId); 533 } 534 return mUserSavedFiles.get(userId); 535 } 536 removeIfUserSavedFileExist(int code, int userId)537 private void removeIfUserSavedFileExist(int code, int userId) { 538 final UserSavedFile usf = getUserFiles(userId).get(code); 539 if (usf != null) { 540 mUserSavedFiles.get(userId).remove(code); 541 mSavedFilesInOrder.remove(usf); 542 mPersister.removeSnap(code, userId); 543 } 544 } 545 containsFile(int code, int userId)546 private boolean containsFile(int code, int userId) { 547 return getUserFiles(userId).get(code) != null; 548 } 549 addUserSavedFile(int code, int userId, TaskSnapshot snapshot)550 private void addUserSavedFile(int code, int userId, TaskSnapshot snapshot) { 551 final SparseArray<UserSavedFile> savedFiles = getUserFiles(userId); 552 final UserSavedFile savedFile = savedFiles.get(code); 553 if (savedFile == null) { 554 final UserSavedFile usf = new UserSavedFile(code, userId); 555 savedFiles.put(code, usf); 556 mSavedFilesInOrder.add(usf); 557 mPersister.persistSnapshot(code, userId, snapshot); 558 559 if (mSavedFilesInOrder.size() > MAX_PERSIST_SNAPSHOT_COUNT * 2) { 560 purgeSavedFile(); 561 } 562 } 563 } 564 purgeSavedFile()565 private void purgeSavedFile() { 566 final int savedFileCount = mSavedFilesInOrder.size(); 567 final int removeCount = savedFileCount - MAX_PERSIST_SNAPSHOT_COUNT; 568 final ArrayList<UserSavedFile> usfs = new ArrayList<>(); 569 if (removeCount > 0) { 570 final int removeTillIndex = savedFileCount - removeCount; 571 for (int i = savedFileCount - 1; i > removeTillIndex; --i) { 572 final UserSavedFile usf = mSavedFilesInOrder.remove(i); 573 if (usf != null) { 574 mUserSavedFiles.remove(usf.mFileId); 575 usfs.add(usf); 576 } 577 } 578 } 579 if (usfs.size() > 0) { 580 removeSnapshotFiles(usfs); 581 } 582 } 583 removeSnapshotFiles(ArrayList<UserSavedFile> files)584 private void removeSnapshotFiles(ArrayList<UserSavedFile> files) { 585 synchronized (mSnapshotPersistQueue.getLock()) { 586 mSnapshotPersistQueue.sendToQueueLocked( 587 new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) { 588 @Override 589 void write() { 590 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "activity_remove_files"); 591 for (int i = files.size() - 1; i >= 0; --i) { 592 final UserSavedFile usf = files.get(i); 593 mSnapshotPersistQueue.deleteSnapshot( 594 usf.mFileId, usf.mUserId, mPersistInfoProvider); 595 } 596 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); 597 } 598 }); 599 } 600 } 601 602 static class UserSavedFile { 603 int mFileId; 604 int mUserId; UserSavedFile(int fileId, int userId)605 UserSavedFile(int fileId, int userId) { 606 mFileId = fileId; 607 mUserId = userId; 608 } 609 } 610 } 611