blob: 105b2bb09cf8c1b70fdf381ce1c9f37538e8c8e4 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wm;
import static com.android.server.wm.SnapshotController.ACTIVITY_CLOSE;
import static com.android.server.wm.SnapshotController.ACTIVITY_OPEN;
import static com.android.server.wm.SnapshotController.TASK_CLOSE;
import static com.android.server.wm.SnapshotController.TASK_OPEN;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.os.Environment;
import android.os.SystemProperties;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
import com.android.server.wm.SnapshotController.TransitionState;
import java.io.File;
import java.util.ArrayList;
/**
* When an app token becomes invisible, we take a snapshot (bitmap) and put it into our cache.
* Internally we use gralloc buffers to be able to draw them wherever we like without any copying.
* <p>
* System applications may retrieve a snapshot to represent the current state of an activity, and
* draw them in their own process.
* <p>
* Unlike TaskSnapshotController, we only keep one activity snapshot for a visible task in the
* cache. Which should largely reduce the memory usage.
* <p>
* To access this class, acquire the global window manager lock.
*/
class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord,
ActivitySnapshotCache> {
private static final boolean DEBUG = false;
private static final String TAG = AbsAppSnapshotController.TAG;
// Maximum persisted snapshot count on disk.
private static final int MAX_PERSIST_SNAPSHOT_COUNT = 20;
static final String SNAPSHOTS_DIRNAME = "activity_snapshots";
/**
* The pending activities which should capture snapshot when process transition finish.
*/
@VisibleForTesting
final ArraySet<ActivityRecord> mPendingCaptureActivity = new ArraySet<>();
/**
* The pending activities which should remove snapshot from memory when process transition
* finish.
*/
@VisibleForTesting
final ArraySet<ActivityRecord> mPendingRemoveActivity = new ArraySet<>();
/**
* The pending activities which should delete snapshot files when process transition finish.
*/
@VisibleForTesting
final ArraySet<ActivityRecord> mPendingDeleteActivity = new ArraySet<>();
/**
* The pending activities which should load snapshot from disk when process transition finish.
*/
@VisibleForTesting
final ArraySet<ActivityRecord> mPendingLoadActivity = new ArraySet<>();
private final SnapshotPersistQueue mSnapshotPersistQueue;
private final PersistInfoProvider mPersistInfoProvider;
private final AppSnapshotLoader mSnapshotLoader;
/**
* File information holders, to make the sequence align, always update status of
* mUserSavedFiles/mSavedFilesInOrder before persist file from mPersister.
*/
private final SparseArray<SparseArray<UserSavedFile>> mUserSavedFiles = new SparseArray<>();
// Keep sorted with create timeline.
private final ArrayList<UserSavedFile> mSavedFilesInOrder = new ArrayList<>();
private final TaskSnapshotPersister mPersister;
ActivitySnapshotController(WindowManagerService service, SnapshotPersistQueue persistQueue) {
super(service);
mSnapshotPersistQueue = persistQueue;
mPersistInfoProvider = createPersistInfoProvider(service,
Environment::getDataSystemCeDirectory);
mPersister = new TaskSnapshotPersister(persistQueue, mPersistInfoProvider);
mSnapshotLoader = new AppSnapshotLoader(mPersistInfoProvider);
initialize(new ActivitySnapshotCache(service));
final boolean snapshotEnabled =
!service.mContext
.getResources()
.getBoolean(com.android.internal.R.bool.config_disableTaskSnapshots)
&& isSnapshotEnabled()
&& !ActivityManager.isLowRamDeviceStatic(); // Don't support Android Go
setSnapshotEnabled(snapshotEnabled);
}
void systemReady() {
if (shouldDisableSnapshots()) {
return;
}
mService.mSnapshotController.registerTransitionStateConsumer(
ACTIVITY_OPEN, this::handleOpenActivityTransition);
mService.mSnapshotController.registerTransitionStateConsumer(
ACTIVITY_CLOSE, this::handleCloseActivityTransition);
mService.mSnapshotController.registerTransitionStateConsumer(
TASK_OPEN, this::handleOpenTaskTransition);
mService.mSnapshotController.registerTransitionStateConsumer(
TASK_CLOSE, this::handleCloseTaskTransition);
}
@Override
protected float initSnapshotScale() {
final float config = mService.mContext.getResources().getFloat(
com.android.internal.R.dimen.config_resActivitySnapshotScale);
return Math.max(Math.min(config, 1f), 0.1f);
}
// TODO remove when enabled
static boolean isSnapshotEnabled() {
return SystemProperties.getInt("persist.wm.debug.activity_screenshot", 0) != 0;
}
static PersistInfoProvider createPersistInfoProvider(
WindowManagerService service, BaseAppSnapshotPersister.DirectoryResolver resolver) {
// Don't persist reduced file, instead we only persist the "HighRes" bitmap which has
// already scaled with #initSnapshotScale
final boolean use16BitFormat = service.mContext.getResources().getBoolean(
com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
return new PersistInfoProvider(resolver, SNAPSHOTS_DIRNAME,
false /* enableLowResSnapshots */, 0 /* lowResScaleFactor */, use16BitFormat);
}
/** Retrieves a snapshot for an activity from cache. */
@Nullable
TaskSnapshot getSnapshot(ActivityRecord ar) {
final int code = getSystemHashCode(ar);
return mCache.getSnapshot(code);
}
private void cleanUpUserFiles(int userId) {
synchronized (mSnapshotPersistQueue.getLock()) {
mSnapshotPersistQueue.sendToQueueLocked(
new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
@Override
boolean isReady() {
final UserManagerInternal mUserManagerInternal =
LocalServices.getService(UserManagerInternal.class);
return mUserManagerInternal.isUserUnlocked(userId);
}
@Override
void write() {
final File file = mPersistInfoProvider.getDirectory(userId);
if (file.exists()) {
final File[] contents = file.listFiles();
if (contents != null) {
for (int i = contents.length - 1; i >= 0; i--) {
contents[i].delete();
}
}
}
}
});
}
}
/**
* Prepare to handle on transition start. Clear all temporary fields.
*/
void preTransitionStart() {
if (shouldDisableSnapshots()) {
return;
}
resetTmpFields();
}
/**
* on transition start has notified, start process data.
*/
void postTransitionStart() {
if (shouldDisableSnapshots()) {
return;
}
onCommitTransition();
}
@VisibleForTesting
void resetTmpFields() {
mPendingCaptureActivity.clear();
mPendingRemoveActivity.clear();
mPendingDeleteActivity.clear();
mPendingLoadActivity.clear();
}
/**
* Start process all pending activities for a transition.
*/
private void onCommitTransition() {
if (DEBUG) {
Slog.d(TAG, "ActivitySnapshotController#onCommitTransition result:"
+ " capture " + mPendingCaptureActivity
+ " remove " + mPendingRemoveActivity
+ " delete " + mPendingDeleteActivity
+ " load " + mPendingLoadActivity);
}
// task snapshots
for (int i = mPendingCaptureActivity.size() - 1; i >= 0; i--) {
recordSnapshot(mPendingCaptureActivity.valueAt(i));
}
// clear mTmpRemoveActivity from cache
for (int i = mPendingRemoveActivity.size() - 1; i >= 0; i--) {
final ActivityRecord ar = mPendingRemoveActivity.valueAt(i);
final int code = getSystemHashCode(ar);
mCache.onIdRemoved(code);
}
// clear snapshot on cache and delete files
for (int i = mPendingDeleteActivity.size() - 1; i >= 0; i--) {
final ActivityRecord ar = mPendingDeleteActivity.valueAt(i);
final int code = getSystemHashCode(ar);
mCache.onIdRemoved(code);
removeIfUserSavedFileExist(code, ar.mUserId);
}
// load snapshot to cache
for (int i = mPendingLoadActivity.size() - 1; i >= 0; i--) {
final ActivityRecord ar = mPendingLoadActivity.valueAt(i);
final int code = getSystemHashCode(ar);
final int userId = ar.mUserId;
if (mCache.getSnapshot(code) != null) {
// already in cache, skip
continue;
}
if (containsFile(code, userId)) {
synchronized (mSnapshotPersistQueue.getLock()) {
mSnapshotPersistQueue.sendToQueueLocked(
new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
@Override
void write() {
final TaskSnapshot snapshot = mSnapshotLoader.loadTask(code,
userId, false /* loadLowResolutionBitmap */);
synchronized (mService.getWindowManagerLock()) {
if (snapshot != null && !ar.finishing) {
mCache.putSnapshot(ar, snapshot);
}
}
}
});
}
}
}
// don't keep any reference
resetTmpFields();
}
private void recordSnapshot(ActivityRecord activity) {
final TaskSnapshot snapshot = recordSnapshotInner(activity, false /* allowSnapshotHome */);
if (snapshot != null) {
final int code = getSystemHashCode(activity);
addUserSavedFile(code, activity.mUserId, snapshot);
}
}
/**
* Called when the visibility of an app changes outside the regular app transition flow.
*/
void notifyAppVisibilityChanged(ActivityRecord appWindowToken, boolean visible) {
if (shouldDisableSnapshots()) {
return;
}
if (!visible) {
resetTmpFields();
addBelowTopActivityIfExist(appWindowToken.getTask(), mPendingRemoveActivity,
"remove-snapshot");
onCommitTransition();
}
}
private static int getSystemHashCode(ActivityRecord activity) {
return System.identityHashCode(activity);
}
void handleOpenActivityTransition(TransitionState<ActivityRecord> transitionState) {
ArraySet<ActivityRecord> participant = transitionState.getParticipant(false /* open */);
for (ActivityRecord ar : participant) {
mPendingCaptureActivity.add(ar);
// remove the snapshot for the one below close
final ActivityRecord below = ar.getTask().getActivityBelow(ar);
if (below != null) {
mPendingRemoveActivity.add(below);
}
}
}
void handleCloseActivityTransition(TransitionState<ActivityRecord> transitionState) {
ArraySet<ActivityRecord> participant = transitionState.getParticipant(true /* open */);
for (ActivityRecord ar : participant) {
mPendingDeleteActivity.add(ar);
// load next one if exists.
final ActivityRecord below = ar.getTask().getActivityBelow(ar);
if (below != null) {
mPendingLoadActivity.add(below);
}
}
}
void handleCloseTaskTransition(TransitionState<Task> closeTaskTransitionRecord) {
ArraySet<Task> participant = closeTaskTransitionRecord.getParticipant(false /* open */);
for (Task close : participant) {
// this is close task transition
// remove the N - 1 from cache
addBelowTopActivityIfExist(close, mPendingRemoveActivity, "remove-snapshot");
}
}
void handleOpenTaskTransition(TransitionState<Task> openTaskTransitionRecord) {
ArraySet<Task> participant = openTaskTransitionRecord.getParticipant(true /* open */);
for (Task open : participant) {
// this is close task transition
// remove the N - 1 from cache
addBelowTopActivityIfExist(open, mPendingLoadActivity, "load-snapshot");
// Move the activities to top of mSavedFilesInOrder, so when purge happen, there
// will trim the persisted files from the most non-accessed.
adjustSavedFileOrder(open);
}
}
// Add the top -1 activity to a set if it exists.
private void addBelowTopActivityIfExist(Task task, ArraySet<ActivityRecord> set,
String debugMessage) {
final ActivityRecord topActivity = task.getTopMostActivity();
if (topActivity != null) {
final ActivityRecord below = task.getActivityBelow(topActivity);
if (below != null) {
set.add(below);
if (DEBUG) {
Slog.d(TAG, "ActivitySnapshotController#addBelowTopActivityIfExist "
+ below + " from " + debugMessage);
}
}
}
}
private void adjustSavedFileOrder(Task nextTopTask) {
final int userId = nextTopTask.mUserId;
nextTopTask.forAllActivities(ar -> {
final int code = getSystemHashCode(ar);
final UserSavedFile usf = getUserFiles(userId).get(code);
if (usf != null) {
mSavedFilesInOrder.remove(usf);
mSavedFilesInOrder.add(usf);
}
}, false /* traverseTopToBottom */);
}
@Override
void onAppRemoved(ActivityRecord activity) {
super.onAppRemoved(activity);
final int code = getSystemHashCode(activity);
removeIfUserSavedFileExist(code, activity.mUserId);
if (DEBUG) {
Slog.d(TAG, "ActivitySnapshotController#onAppRemoved delete snapshot " + activity);
}
}
@Override
void onAppDied(ActivityRecord activity) {
super.onAppDied(activity);
final int code = getSystemHashCode(activity);
removeIfUserSavedFileExist(code, activity.mUserId);
if (DEBUG) {
Slog.d(TAG, "ActivitySnapshotController#onAppDied delete snapshot " + activity);
}
}
@Override
ActivityRecord getTopActivity(ActivityRecord activity) {
return activity;
}
@Override
ActivityRecord getTopFullscreenActivity(ActivityRecord activity) {
final WindowState win = activity.findMainWindow();
return (win != null && win.mAttrs.isFullscreen()) ? activity : null;
}
@Override
ActivityManager.TaskDescription getTaskDescription(ActivityRecord object) {
return object.taskDescription;
}
/**
* Find the window for a given activity to take a snapshot. During app transitions, trampoline
* activities can appear in the children, but should be ignored.
*/
@Override
protected ActivityRecord findAppTokenForSnapshot(ActivityRecord activity) {
if (activity == null) {
return null;
}
return activity.canCaptureSnapshot() ? activity : null;
}
@Override
protected boolean use16BitFormat() {
return mPersistInfoProvider.use16BitFormat();
}
@NonNull
private SparseArray<UserSavedFile> getUserFiles(int userId) {
if (mUserSavedFiles.get(userId) == null) {
mUserSavedFiles.put(userId, new SparseArray<>());
// This is the first time this user attempt to access snapshot, clear up the disk.
cleanUpUserFiles(userId);
}
return mUserSavedFiles.get(userId);
}
private void removeIfUserSavedFileExist(int code, int userId) {
final UserSavedFile usf = getUserFiles(userId).get(code);
if (usf != null) {
mUserSavedFiles.remove(code);
mSavedFilesInOrder.remove(usf);
mPersister.removeSnap(code, userId);
}
}
private boolean containsFile(int code, int userId) {
return getUserFiles(userId).get(code) != null;
}
private void addUserSavedFile(int code, int userId, TaskSnapshot snapshot) {
final SparseArray<UserSavedFile> savedFiles = getUserFiles(userId);
final UserSavedFile savedFile = savedFiles.get(code);
if (savedFile == null) {
final UserSavedFile usf = new UserSavedFile(code, userId);
savedFiles.put(code, usf);
mSavedFilesInOrder.add(usf);
mPersister.persistSnapshot(code, userId, snapshot);
if (mSavedFilesInOrder.size() > MAX_PERSIST_SNAPSHOT_COUNT * 2) {
purgeSavedFile();
}
}
}
private void purgeSavedFile() {
final int savedFileCount = mSavedFilesInOrder.size();
final int removeCount = savedFileCount - MAX_PERSIST_SNAPSHOT_COUNT;
final ArrayList<UserSavedFile> usfs = new ArrayList<>();
if (removeCount > 0) {
final int removeTillIndex = savedFileCount - removeCount;
for (int i = savedFileCount - 1; i > removeTillIndex; --i) {
final UserSavedFile usf = mSavedFilesInOrder.remove(i);
if (usf != null) {
mUserSavedFiles.remove(usf.mFileId);
usfs.add(usf);
}
}
}
if (usfs.size() > 0) {
removeSnapshotFiles(usfs);
}
}
private void removeSnapshotFiles(ArrayList<UserSavedFile> files) {
synchronized (mSnapshotPersistQueue.getLock()) {
mSnapshotPersistQueue.sendToQueueLocked(
new SnapshotPersistQueue.WriteQueueItem(mPersistInfoProvider) {
@Override
void write() {
for (int i = files.size() - 1; i >= 0; --i) {
final UserSavedFile usf = files.get(i);
mSnapshotPersistQueue.deleteSnapshot(
usf.mFileId, usf.mUserId, mPersistInfoProvider);
}
}
});
}
}
static class UserSavedFile {
int mFileId;
int mUserId;
UserSavedFile(int fileId, int userId) {
mFileId = fileId;
mUserId = userId;
}
}
}