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