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