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.graphics.Rect;
25 import android.os.Environment;
26 import android.os.SystemProperties;
27 import android.os.Trace;
28 import android.util.ArraySet;
29 import android.util.IntArray;
30 import android.util.Slog;
31 import android.util.SparseArray;
32 import android.window.TaskSnapshot;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
36 import com.android.window.flags.Flags;
37 
38 import java.io.File;
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 
42 /**
43  * When an app token becomes invisible, we take a snapshot (bitmap) and put it into our cache.
44  * Internally we use gralloc buffers to be able to draw them wherever we like without any copying.
45  * <p>
46  * System applications may retrieve a snapshot to represent the current state of an activity, and
47  * draw them in their own process.
48  * <p>
49  * Unlike TaskSnapshotController, we only keep one activity snapshot for a visible task in the
50  * cache. Which should largely reduce the memory usage.
51  * <p>
52  * To access this class, acquire the global window manager lock.
53  */
54 class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord,
55         ActivitySnapshotCache> {
56     private static final boolean DEBUG = false;
57     private static final String TAG = AbsAppSnapshotController.TAG;
58     // Maximum persisted snapshot count on disk.
59     private static final int MAX_PERSIST_SNAPSHOT_COUNT = 20;
60 
61     static final String SNAPSHOTS_DIRNAME = "activity_snapshots";
62 
63     /**
64      * The pending activities which should remove snapshot from memory when process transition
65      * finish.
66      */
67     @VisibleForTesting
68     final ArraySet<ActivityRecord> mPendingRemoveActivity = new ArraySet<>();
69 
70     /**
71      * The pending activities which should delete snapshot files when process transition finish.
72      */
73     @VisibleForTesting
74     final ArraySet<ActivityRecord> mPendingDeleteActivity = new ArraySet<>();
75 
76     /**
77      * The pending activities which should load snapshot from disk when process transition finish.
78      */
79     @VisibleForTesting
80     final ArraySet<ActivityRecord> mPendingLoadActivity = new ArraySet<>();
81 
82     private final ArraySet<ActivityRecord> mOnBackPressedActivities = new ArraySet<>();
83 
84     private final ArrayList<ActivityRecord> mTmpBelowActivities = new ArrayList<>();
85     private final ArrayList<WindowContainer> mTmpTransitionParticipants = new ArrayList<>();
86     private final SnapshotPersistQueue mSnapshotPersistQueue;
87     private final PersistInfoProvider mPersistInfoProvider;
88     private final AppSnapshotLoader mSnapshotLoader;
89 
90     /**
91      * File information holders, to make the sequence align, always update status of
92      * mUserSavedFiles/mSavedFilesInOrder before persist file from mPersister.
93      */
94     private final SparseArray<SparseArray<UserSavedFile>> mUserSavedFiles = new SparseArray<>();
95     // Keep sorted with create timeline.
96     private final ArrayList<UserSavedFile> mSavedFilesInOrder = new ArrayList<>();
97     private final TaskSnapshotPersister mPersister;
98 
ActivitySnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue)99     ActivitySnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue) {
100         super(service);
101         mSnapshotPersistQueue = persistQueue;
102         mPersistInfoProvider = createPersistInfoProvider(service,
103                 Environment::getDataSystemCeDirectory);
104         mPersister = new TaskSnapshotPersister(persistQueue, mPersistInfoProvider);
105         mSnapshotLoader = new AppSnapshotLoader(mPersistInfoProvider);
106         initialize(new ActivitySnapshotCache());
107 
108         final boolean snapshotEnabled =
109                 !service.mContext
110                         .getResources()
111                         .getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots)
112                 && isSnapshotEnabled()
113                 && !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go
114         setSnapshotEnabled(snapshotEnabled);
115     }
116 
117     @Override
initSnapshotScale()118     protected float initSnapshotScale() {
119         final float config = mService.mContext.getResources().getFloat(
120                 com.android.internal.R.dimen.config_resActivitySnapshotScale);
121         return Math.max(Math.min(config, 1f), 0.1f);
122     }
123 
124     // TODO remove when enabled
isSnapshotEnabled()125     static boolean isSnapshotEnabled() {
126         return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0
127                 || Flags.activitySnapshotByDefault();
128     }
129 
createPersistInfoProvider( WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver)130     static PersistInfoProvider createPersistInfoProvider(
131             WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) {
132         // Don't persist reduced file, instead we only persist the "HighRes" bitmap which has
133         // already scaled with #initSnapshotScale
134         final boolean use16BitFormat = service.mContext.getResources().getBoolean(
135                 com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
136         return new PersistInfoProvider(resolver, SNAPSHOTS_DIRNAME,
137                 false /* enableLowResSnapshots */, 0 /* lowResScaleFactor */, use16BitFormat);
138     }
139 
140     /**
141      * Retrieves a snapshot for a set of activities from cache.
142      * This will only return the snapshot IFF input activities exist entirely in the snapshot.
143      * Sample: If the snapshot was captured with activity A and B, here will return null if the
144      * input activity is only [A] or [B], it must be [A, B]
145      */
146     @Nullable
getSnapshot(@onNull ActivityRecord[] activities)147     TaskSnapshot getSnapshot(@NonNull ActivityRecord[] activities) {
148         if (activities.length == 0) {
149             return null;
150         }
151         final UserSavedFile tmpUsf = findSavedFile(activities[0]);
152         if (tmpUsf == null || tmpUsf.mActivityIds.size() != activities.length) {
153             return null;
154         }
155         int fileId = 0;
156         for (int i = activities.length - 1; i >= 0; --i) {
157             fileId ^= getSystemHashCode(activities[i]);
158         }
159         return tmpUsf.mFileId == fileId ? mCache.getSnapshot(tmpUsf.mActivityIds.get(0)) : null;
160     }
161 
cleanUpUserFiles(int userId)162     private void cleanUpUserFiles(int userId) {
163         synchronized (mSnapshotPersistQueue.getLock()) {
164             mSnapshotPersistQueue.sendToQueueLocked(
165                     new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider, userId) {
166 
167                         @Override
168                         void write() {
169                             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "cleanUpUserFiles");
170                             final File file = mPersistInfoProvider.getDirectory(mUserId);
171                             if (file.exists()) {
172                                 final File[] contents = file.listFiles();
173                                 if (contents != null) {
174                                     for (int i = contents.length - 1; i >= 0; i--) {
175                                         contents[i].delete();
176                                     }
177                                 }
178                             }
179                             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
180                         }
181                     });
182         }
183     }
184 
addOnBackPressedActivity(ActivityRecord ar)185     void addOnBackPressedActivity(ActivityRecord ar) {
186         if (shouldDisableSnapshots()) {
187             return;
188         }
189         mOnBackPressedActivities.add(ar);
190     }
191 
clearOnBackPressedActivities()192     void clearOnBackPressedActivities() {
193         if (shouldDisableSnapshots()) {
194             return;
195         }
196         mOnBackPressedActivities.clear();
197     }
198 
199     /**
200      * Prepare to collect any change for snapshots processing. Clear all temporary fields.
201      */
beginSnapshotProcess()202     void beginSnapshotProcess() {
203         if (shouldDisableSnapshots()) {
204             return;
205         }
206         resetTmpFields();
207     }
208 
209     /**
210      * End collect any change for snapshots processing, start process data.
211      */
endSnapshotProcess()212     void endSnapshotProcess() {
213         if (shouldDisableSnapshots()) {
214             return;
215         }
216         for (int i = mOnBackPressedActivities.size() - 1; i >= 0; --i) {
217             handleActivityTransition(mOnBackPressedActivities.valueAt(i));
218         }
219         mOnBackPressedActivities.clear();
220         mTmpTransitionParticipants.clear();
221         postProcess();
222     }
223 
224     @VisibleForTesting
resetTmpFields()225     void resetTmpFields() {
226         mPendingRemoveActivity.clear();
227         mPendingDeleteActivity.clear();
228         mPendingLoadActivity.clear();
229     }
230 
231     /**
232      * Start process all pending activities for a transition.
233      */
postProcess()234     private void postProcess() {
235         if (DEBUG) {
236             Slog.d(TAG, "ActivitySnapshotController#postProcess result:"
237                     + " remove " + mPendingRemoveActivity
238                     + " delete " + mPendingDeleteActivity
239                     + " load " + mPendingLoadActivity);
240         }
241         // load snapshot to cache
242         loadActivitySnapshot();
243         // clear mTmpRemoveActivity from cache
244         for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) {
245             final ActivityRecord ar = mPendingRemoveActivity.valueAt(i);
246             removeCachedFiles(ar);
247         }
248         // clear snapshot on cache and delete files
249         for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) {
250             final ActivityRecord ar = mPendingDeleteActivity.valueAt(i);
251             removeIfUserSavedFileExist(ar);
252         }
253         // don't keep any reference
254         resetTmpFields();
255     }
256 
257     class LoadActivitySnapshotItem extends SnapshotPersistQueue.WriteQueueItem {
258         private final int mCode;
259         private final ActivityRecord[] mActivities;
260 
LoadActivitySnapshotItem(@onNull ActivityRecord[] activities, int code, int userId, @NonNull PersistInfoProvider persistInfoProvider)261         LoadActivitySnapshotItem(@NonNull ActivityRecord[] activities, int code, int userId,
262                 @NonNull PersistInfoProvider persistInfoProvider) {
263             super(persistInfoProvider, userId);
264             mActivities = activities;
265             mCode = code;
266         }
267 
268         @Override
write()269         void write() {
270             try {
271                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
272                         "load_activity_snapshot");
273                 final TaskSnapshot snapshot = mSnapshotLoader.loadTask(mCode,
274                         mUserId, false /* loadLowResolutionBitmap */);
275                 if (snapshot == null) {
276                     return;
277                 }
278                 synchronized (mService.getWindowManagerLock()) {
279                     // Verify the snapshot is still needed, and the activity is not finishing
280                     if (!hasRecord(mActivities[0])) {
281                         return;
282                     }
283                     for (ActivityRecord ar : mActivities) {
284                         mCache.putSnapshot(ar, snapshot);
285                     }
286                 }
287             } finally {
288                 Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
289             }
290         }
291 
292         @Override
equals(Object o)293         public boolean equals(Object o) {
294             if (o == null || getClass() != o.getClass()) return false;
295             final LoadActivitySnapshotItem other = (LoadActivitySnapshotItem) o;
296             return mCode == other.mCode && mUserId == other.mUserId
297                     && mPersistInfoProvider == other.mPersistInfoProvider;
298         }
299 
300         @Override
toString()301         public String toString() {
302             return "LoadActivitySnapshotItem{code=" + mCode + ", UserId=" + mUserId + "}";
303         }
304     }
305 
loadActivitySnapshot()306     void loadActivitySnapshot() {
307         if (mPendingLoadActivity.isEmpty()) {
308             return;
309         }
310         // Only load if saved file exists.
311         final ArraySet<UserSavedFile> loadingFiles = new ArraySet<>();
312         for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) {
313             final ActivityRecord ar = mPendingLoadActivity.valueAt(i);
314             final UserSavedFile usf = findSavedFile(ar);
315             if (usf != null) {
316                 loadingFiles.add(usf);
317             }
318         }
319         // Filter out the activity if the snapshot was removed.
320         for (int i = loadingFiles.size() - 1; i >= 0; i--) {
321             final UserSavedFile usf = loadingFiles.valueAt(i);
322             final ActivityRecord[] activities = usf.filterExistActivities(mPendingLoadActivity);
323             if (activities == null) {
324                 continue;
325             }
326             if (getSnapshot(activities) != null) {
327                 // Found the cache in memory, so skip loading from file.
328                 continue;
329             }
330             loadSnapshotInner(activities, usf);
331         }
332     }
333 
334     @VisibleForTesting
loadSnapshotInner(ActivityRecord[] activities, UserSavedFile usf)335     void loadSnapshotInner(ActivityRecord[] activities, UserSavedFile usf) {
336         synchronized (mSnapshotPersistQueue.getLock()) {
337             mSnapshotPersistQueue.insertQueueAtFirstLocked(new LoadActivitySnapshotItem(
338                     activities, usf.mFileId, usf.mUserId, mPersistInfoProvider));
339         }
340     }
341 
342     /**
343      * Record one or multiple activities within a snapshot where those activities must belong to
344      * the same task.
345      * @param activity If the request activity is more than one, try to record those activities
346      *                 as a single snapshot, so those activities should belong to the same task.
347      */
recordSnapshot(@onNull ArrayList<ActivityRecord> activity)348     void recordSnapshot(@NonNull ArrayList<ActivityRecord> activity) {
349         if (shouldDisableSnapshots() || activity.isEmpty()) {
350             return;
351         }
352         if (DEBUG) {
353             Slog.d(TAG, "ActivitySnapshotController#recordSnapshot " + activity);
354         }
355         final int size = activity.size();
356         final int[] mixedCode = new int[size];
357         if (size == 1) {
358             final ActivityRecord singleActivity = activity.get(0);
359             final TaskSnapshot snapshot = recordSnapshotInner(singleActivity);
360             if (snapshot != null) {
361                 mixedCode[0] = getSystemHashCode(singleActivity);
362                 addUserSavedFile(singleActivity.mUserId, snapshot, mixedCode);
363             }
364             return;
365         }
366 
367         final Task mainTask = activity.get(0).getTask();
368         // Snapshot by task controller with activity's scale.
369         final TaskSnapshot snapshot = mService.mTaskSnapshotController
370                 .snapshot(mainTask, mHighResSnapshotScale);
371         if (snapshot == null) {
372             return;
373         }
374 
375         for (int i = 0; i < activity.size(); ++i) {
376             final ActivityRecord next = activity.get(i);
377             mCache.putSnapshot(next, snapshot);
378             mixedCode[i] = getSystemHashCode(next);
379         }
380         addUserSavedFile(mainTask.mUserId, snapshot, mixedCode);
381     }
382 
383     /**
384      * Called when the visibility of an app changes outside the regular app transition flow.
385      */
notifyAppVisibilityChanged(ActivityRecord ar, boolean visible)386     void notifyAppVisibilityChanged(ActivityRecord ar, boolean visible) {
387         if (shouldDisableSnapshots()) {
388             return;
389         }
390         final Task task = ar.getTask();
391         if (task == null) {
392             return;
393         }
394         // Doesn't need to capture activity snapshot when it converts from translucent.
395         if (!visible) {
396             resetTmpFields();
397             addBelowActivityIfExist(ar, mPendingRemoveActivity, false,
398                     "remove-snapshot");
399             postProcess();
400         }
401     }
402 
403     @VisibleForTesting
getSystemHashCode(ActivityRecord activity)404     static int getSystemHashCode(ActivityRecord activity) {
405         return System.identityHashCode(activity);
406     }
407 
408     @VisibleForTesting
handleTransitionFinish(@onNull ArrayList<WindowContainer> windows)409     void handleTransitionFinish(@NonNull ArrayList<WindowContainer> windows) {
410         mTmpTransitionParticipants.clear();
411         mTmpTransitionParticipants.addAll(windows);
412         for (int i = mTmpTransitionParticipants.size() - 1; i >= 0; --i) {
413             final WindowContainer next = mTmpTransitionParticipants.get(i);
414             if (next.asTask() != null) {
415                 handleTaskTransition(next.asTask());
416             } else if (next.asTaskFragment() != null) {
417                 final TaskFragment tf = next.asTaskFragment();
418                 final ActivityRecord ar = tf.getTopMostActivity();
419                 if (ar != null) {
420                     handleActivityTransition(ar);
421                 }
422             } else if (next.asActivityRecord() != null) {
423                 handleActivityTransition(next.asActivityRecord());
424             }
425         }
426     }
427 
handleActivityTransition(@onNull ActivityRecord ar)428     private void handleActivityTransition(@NonNull ActivityRecord ar) {
429         if (shouldDisableSnapshots()) {
430             return;
431         }
432         if (ar.isVisibleRequested()) {
433             mPendingDeleteActivity.add(ar);
434             // load next one if exists.
435             // Note if this transition is happen between two TaskFragment, the next N - 1 activity
436             // may not participant in this transition.
437             // Sample:
438             //   [TF1] close
439             //   [TF2] open
440             //   Bottom Activity <- Able to load this even it didn't participant the transition.
441             addBelowActivityIfExist(ar, mPendingLoadActivity, false, "load-snapshot");
442         } else {
443             // remove the snapshot for the one below close
444             addBelowActivityIfExist(ar, mPendingRemoveActivity, true, "remove-snapshot");
445         }
446     }
447 
handleTaskTransition(Task task)448     private void handleTaskTransition(Task task) {
449         if (shouldDisableSnapshots()) {
450             return;
451         }
452         final ActivityRecord topActivity = task.getTopMostActivity();
453         if (topActivity == null) {
454             return;
455         }
456         if (task.isVisibleRequested()) {
457             // this is open task transition
458             // load the N - 1 to cache
459             addBelowActivityIfExist(topActivity, mPendingLoadActivity, true, "load-snapshot");
460             // Move the activities to top of mSavedFilesInOrder, so when purge happen, there
461             // will trim the persisted files from the most non-accessed.
462             adjustSavedFileOrder(task);
463         } else {
464             // this is close task transition
465             // remove the N - 1 from cache
466             addBelowActivityIfExist(topActivity, mPendingRemoveActivity, true, "remove-snapshot");
467         }
468     }
469 
470     /**
471      * Add the top -1 activity to a set if it exists.
472      * @param inTransition true if the activity must participant in transition.
473      */
addBelowActivityIfExist(ActivityRecord currentActivity, ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage)474     private void addBelowActivityIfExist(ActivityRecord currentActivity,
475             ArraySet<ActivityRecord> set, boolean inTransition, String debugMessage) {
476         getActivityBelow(currentActivity, inTransition, mTmpBelowActivities);
477         for (int i = mTmpBelowActivities.size() - 1; i >= 0; --i) {
478             set.add(mTmpBelowActivities.get(i));
479             if (DEBUG) {
480                 Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist "
481                         + mTmpBelowActivities.get(i) + " from " + debugMessage);
482             }
483         }
484         mTmpBelowActivities.clear();
485     }
486 
getActivityBelow(ActivityRecord currentActivity, boolean inTransition, ArrayList<ActivityRecord> result)487     private void getActivityBelow(ActivityRecord currentActivity, boolean inTransition,
488             ArrayList<ActivityRecord> result) {
489         final Task currentTask = currentActivity.getTask();
490         if (currentTask == null) {
491             return;
492         }
493         final ActivityRecord initPrev = currentTask.getActivityBelow(currentActivity);
494         if (initPrev == null) {
495             return;
496         }
497         final TaskFragment currTF = currentActivity.getTaskFragment();
498         final TaskFragment prevTF = initPrev.getTaskFragment();
499         final TaskFragment prevAdjacentTF = prevTF != null
500                 ? prevTF.getAdjacentTaskFragment() : null;
501         if (currTF == prevTF && currTF != null || prevAdjacentTF == null) {
502             // Current activity and previous one is in the same task fragment, or
503             // previous activity is not in a task fragment, or
504             // previous activity's task fragment doesn't adjacent to any others.
505             if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
506                 result.add(initPrev);
507             }
508             return;
509         }
510 
511         if (prevAdjacentTF == currTF) {
512             // previous activity A is adjacent to current activity B.
513             // Try to find anyone below previous activityA, which are C and D if exists.
514             // A | B
515             // C (| D)
516             getActivityBelow(initPrev, inTransition, result);
517         } else {
518             // previous activity C isn't adjacent to current activity A.
519             // A
520             // B | C
521             final Task prevAdjacentTask = prevAdjacentTF.getTask();
522             if (prevAdjacentTask == currentTask) {
523                 final int currentIndex = currTF != null
524                         ? currentTask.mChildren.indexOf(currTF)
525                         : currentTask.mChildren.indexOf(currentActivity);
526                 final int prevAdjacentIndex =
527                         prevAdjacentTask.mChildren.indexOf(prevAdjacentTF);
528                 // prevAdjacentTF already above currentActivity
529                 if (prevAdjacentIndex > currentIndex) {
530                     return;
531                 }
532             }
533             if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
534                 result.add(initPrev);
535             }
536             // prevAdjacentTF is adjacent to another one
537             final ActivityRecord prevAdjacentActivity = prevAdjacentTF.getTopMostActivity();
538             if (prevAdjacentActivity != null && (!inTransition
539                     || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) {
540                 result.add(prevAdjacentActivity);
541             }
542         }
543     }
544 
isInParticipant(ActivityRecord ar, ArrayList<WindowContainer> transitionParticipants)545     static boolean isInParticipant(ActivityRecord ar,
546             ArrayList<WindowContainer> transitionParticipants) {
547         for (int i = transitionParticipants.size() - 1; i >= 0; --i) {
548             final WindowContainer wc = transitionParticipants.get(i);
549             if (ar == wc || ar.isDescendantOf(wc)) {
550                 return true;
551             }
552         }
553         return false;
554     }
555 
adjustSavedFileOrder(Task nextTopTask)556     private void adjustSavedFileOrder(Task nextTopTask) {
557         nextTopTask.forAllActivities(ar -> {
558             final UserSavedFile usf = findSavedFile(ar);
559             if (usf != null) {
560                 mSavedFilesInOrder.remove(usf);
561                 mSavedFilesInOrder.add(usf);
562             }
563         }, false /* traverseTopToBottom */);
564     }
565 
566     @Override
onAppRemoved(ActivityRecord activity)567     void onAppRemoved(ActivityRecord activity) {
568         if (shouldDisableSnapshots()) {
569             return;
570         }
571         removeIfUserSavedFileExist(activity);
572         if (DEBUG) {
573             Slog.d(TAG, "ActivitySnapshotController#onAppRemoved delete snapshot " + activity);
574         }
575     }
576 
577     @Override
onAppDied(ActivityRecord activity)578     void onAppDied(ActivityRecord activity) {
579         if (shouldDisableSnapshots()) {
580             return;
581         }
582         removeIfUserSavedFileExist(activity);
583         if (DEBUG) {
584             Slog.d(TAG, "ActivitySnapshotController#onAppDied delete snapshot " + activity);
585         }
586     }
587 
588     @Override
getTopActivity(ActivityRecord activity)589     ActivityRecord getTopActivity(ActivityRecord activity) {
590         return activity;
591     }
592 
593     @Override
getTopFullscreenActivity(ActivityRecord activity)594     ActivityRecord getTopFullscreenActivity(ActivityRecord activity) {
595         final WindowState win = activity.findMainWindow();
596         return (win != null && win.mAttrs.isFullscreen()) ? activity : null;
597     }
598 
599     @Override
getTaskDescription(ActivityRecord object)600     ActivityManager.TaskDescription getTaskDescription(ActivityRecord object) {
601         return object.taskDescription;
602     }
603 
604     /**
605      * Find the window for a given activity to take a snapshot. During app transitions, trampoline
606      * activities can appear in the children, but should be ignored.
607      */
608     @Override
findAppTokenForSnapshot(ActivityRecord activity)609     protected ActivityRecord findAppTokenForSnapshot(ActivityRecord activity) {
610         if (activity == null) {
611             return null;
612         }
613         return activity.canCaptureSnapshot() ? activity : null;
614     }
615 
616     @Override
use16BitFormat()617     protected boolean use16BitFormat() {
618         return mPersistInfoProvider.use16BitFormat();
619     }
620 
621     @Override
getLetterboxInsets(ActivityRecord topActivity)622     protected Rect getLetterboxInsets(ActivityRecord topActivity) {
623         // Do not capture letterbox for ActivityRecord
624         return Letterbox.EMPTY_RECT;
625     }
626 
627     @NonNull
getUserFiles(int userId)628     private SparseArray<UserSavedFile> getUserFiles(int userId) {
629         if (mUserSavedFiles.get(userId) == null) {
630             mUserSavedFiles.put(userId, new SparseArray<>());
631             // This is the first time this user attempt to access snapshot, clear up the disk.
632             cleanUpUserFiles(userId);
633         }
634         return mUserSavedFiles.get(userId);
635     }
636 
findSavedFile(@onNull ActivityRecord ar)637     UserSavedFile findSavedFile(@NonNull ActivityRecord ar) {
638         final int code = getSystemHashCode(ar);
639         return findSavedFile(ar.mUserId, code);
640     }
641 
findSavedFile(int userId, int code)642     UserSavedFile findSavedFile(int userId, int code) {
643         final SparseArray<UserSavedFile> usfs = getUserFiles(userId);
644         return usfs.get(code);
645     }
646 
removeCachedFiles(ActivityRecord ar)647     private void removeCachedFiles(ActivityRecord ar) {
648         final UserSavedFile usf = findSavedFile(ar);
649         if (usf != null) {
650             for (int i = usf.mActivityIds.size() - 1; i >= 0; --i) {
651                 final int activityId = usf.mActivityIds.get(i);
652                 mCache.onIdRemoved(activityId);
653             }
654         }
655     }
656 
removeIfUserSavedFileExist(ActivityRecord ar)657     private void removeIfUserSavedFileExist(ActivityRecord ar) {
658         final UserSavedFile usf = findSavedFile(ar);
659         if (usf != null) {
660             final SparseArray<UserSavedFile> usfs = getUserFiles(ar.mUserId);
661             for (int i = usf.mActivityIds.size() - 1; i >= 0; --i) {
662                 final int activityId = usf.mActivityIds.get(i);
663                 usf.remove(activityId);
664                 mCache.onIdRemoved(activityId);
665                 usfs.remove(activityId);
666             }
667             mSavedFilesInOrder.remove(usf);
668             mPersister.removeSnapshot(usf.mFileId, ar.mUserId);
669         }
670     }
671 
672     @VisibleForTesting
hasRecord(@onNull ActivityRecord ar)673     boolean hasRecord(@NonNull ActivityRecord ar) {
674         return findSavedFile(ar) != null;
675     }
676 
677     @VisibleForTesting
addUserSavedFile(int userId, TaskSnapshot snapshot, @NonNull int[] code)678     void addUserSavedFile(int userId, TaskSnapshot snapshot, @NonNull int[] code) {
679         final UserSavedFile savedFile = findSavedFile(userId, code[0]);
680         if (savedFile != null) {
681             Slog.w(TAG, "Duplicate request for recording activity snapshot " + savedFile);
682             return;
683         }
684         int fileId = 0;
685         for (int i = code.length - 1; i >= 0; --i) {
686             fileId ^= code[i];
687         }
688         final UserSavedFile usf = new UserSavedFile(fileId, userId);
689         SparseArray<UserSavedFile> usfs = getUserFiles(userId);
690         for (int i = code.length - 1; i >= 0; --i) {
691             usfs.put(code[i], usf);
692         }
693         usf.mActivityIds.addAll(code);
694         mSavedFilesInOrder.add(usf);
695         mPersister.persistSnapshot(fileId, userId, snapshot);
696 
697         if (mSavedFilesInOrder.size() > MAX_PERSIST_SNAPSHOT_COUNT * 2) {
698             purgeSavedFile();
699         }
700     }
701 
purgeSavedFile()702     private void purgeSavedFile() {
703         final int savedFileCount = mSavedFilesInOrder.size();
704         final int removeCount = savedFileCount - MAX_PERSIST_SNAPSHOT_COUNT;
705         if (removeCount < 1) {
706             return;
707         }
708 
709         final ArrayList<UserSavedFile> removeTargets = new ArrayList<>();
710         for (int i = removeCount - 1; i >= 0; --i) {
711             final UserSavedFile usf = mSavedFilesInOrder.remove(i);
712             final SparseArray<UserSavedFile> files = mUserSavedFiles.get(usf.mUserId);
713             for (int j = usf.mActivityIds.size() - 1; j >= 0; --j) {
714                 mCache.removeRunningEntry(usf.mActivityIds.get(j));
715                 files.remove(usf.mActivityIds.get(j));
716             }
717             removeTargets.add(usf);
718         }
719         for (int i = removeTargets.size() - 1; i >= 0; --i) {
720             final UserSavedFile usf = removeTargets.get(i);
721             mPersister.removeSnapshot(usf.mFileId, usf.mUserId);
722         }
723     }
724 
725     @Override
dump(PrintWriter pw, String prefix)726     void dump(PrintWriter pw, String prefix) {
727         super.dump(pw, prefix);
728         final String doublePrefix = prefix + "  ";
729         final String triplePrefix = doublePrefix + "  ";
730         for (int i = mUserSavedFiles.size() - 1; i >= 0; --i) {
731             final SparseArray<UserSavedFile> usfs = mUserSavedFiles.valueAt(i);
732             pw.println(doublePrefix + "UserSavedFile userId=" + mUserSavedFiles.keyAt(i));
733             final ArraySet<UserSavedFile> sets = new ArraySet<>();
734             for (int j = usfs.size() - 1; j >= 0; --j) {
735                 sets.add(usfs.valueAt(j));
736             }
737             for (int j = sets.size() - 1; j >= 0; --j) {
738                 pw.println(triplePrefix + "SavedFile=" + sets.valueAt(j));
739             }
740         }
741     }
742 
743     static class UserSavedFile {
744         // The unique id as filename.
745         final int mFileId;
746         final int mUserId;
747 
748         /**
749          * The Id of all activities which are includes in the snapshot.
750          */
751         final IntArray mActivityIds = new IntArray();
752 
UserSavedFile(int fileId, int userId)753         UserSavedFile(int fileId, int userId) {
754             mFileId = fileId;
755             mUserId = userId;
756         }
757 
contains(int code)758         boolean contains(int code) {
759             return mActivityIds.contains(code);
760         }
761 
remove(int code)762         void remove(int code) {
763             final int index = mActivityIds.indexOf(code);
764             if (index >= 0) {
765                 mActivityIds.remove(index);
766             }
767         }
768 
filterExistActivities( @onNull ArraySet<ActivityRecord> pendingLoadActivity)769         ActivityRecord[] filterExistActivities(
770                 @NonNull ArraySet<ActivityRecord> pendingLoadActivity) {
771             ArrayList<ActivityRecord> matchedActivities = null;
772             for (int i = pendingLoadActivity.size() - 1; i >= 0; --i) {
773                 final ActivityRecord ar = pendingLoadActivity.valueAt(i);
774                 if (contains(getSystemHashCode(ar))) {
775                     if (matchedActivities == null) {
776                         matchedActivities = new ArrayList<>();
777                     }
778                     matchedActivities.add(ar);
779                 }
780             }
781             if (matchedActivities == null || matchedActivities.size() != mActivityIds.size()) {
782                 return null;
783             }
784             return matchedActivities.toArray(new ActivityRecord[0]);
785         }
786 
787         @Override
toString()788         public String toString() {
789             StringBuilder sb = new StringBuilder(128);
790             sb.append("UserSavedFile{");
791             sb.append(Integer.toHexString(System.identityHashCode(this)));
792             sb.append(" fileId=");
793             sb.append(Integer.toHexString(mFileId));
794             sb.append(", activityIds=[");
795             for (int i = mActivityIds.size() - 1; i >= 0; --i) {
796                 sb.append(Integer.toHexString(mActivityIds.get(i)));
797                 if (i > 0) {
798                     sb.append(',');
799                 }
800             }
801             sb.append("]");
802             sb.append("}");
803             return sb.toString();
804         }
805     }
806 }
807