blob: 9fdc0453424e373d35d3db1682b36a0e32c57120 [file] [log] [blame]
/*
* Copyright (C) 2016 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.TaskSnapshotPersister.DISABLE_FULL_SIZED_BITMAPS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskSnapshot;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
import android.graphics.PixelFormat;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.os.Environment;
import android.os.Handler;
import android.util.ArraySet;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.ThreadedRenderer;
import android.view.WindowManager.LayoutParams;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.policy.WindowManagerPolicy.StartingSurface;
import com.android.server.wm.TaskSnapshotSurface.SystemBarBackgroundPainter;
import com.android.server.wm.utils.InsetUtils;
import com.google.android.collect.Sets;
import java.io.PrintWriter;
/**
* When an app token becomes invisible, we take a snapshot (bitmap) of the corresponding task 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 a task, and draw
* them in their own process.
* <p>
* When we task becomes visible again, we show a starting window with the snapshot as the content to
* make app transitions more responsive.
* <p>
* To access this class, acquire the global window manager lock.
*/
class TaskSnapshotController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotController" : TAG_WM;
/**
* Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be
* used as the snapshot.
*/
@VisibleForTesting
static final int SNAPSHOT_MODE_REAL = 0;
/**
* Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but
* we should try to use the app theme to create a dummy representation of the app.
*/
@VisibleForTesting
static final int SNAPSHOT_MODE_APP_THEME = 1;
/**
* Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot.
*/
@VisibleForTesting
static final int SNAPSHOT_MODE_NONE = 2;
private final WindowManagerService mService;
private final TaskSnapshotCache mCache;
private final TaskSnapshotPersister mPersister;
private final TaskSnapshotLoader mLoader;
private final ArraySet<Task> mSkipClosingAppSnapshotTasks = new ArraySet<>();
private final ArraySet<Task> mTmpTasks = new ArraySet<>();
private final Handler mHandler = new Handler();
private final float mFullSnapshotScale;
private final Rect mTmpRect = new Rect();
/**
* Flag indicating whether we are running on an Android TV device.
*/
private final boolean mIsRunningOnTv;
/**
* Flag indicating whether we are running on an IoT device.
*/
private final boolean mIsRunningOnIoT;
/**
* Flag indicating whether we are running on an Android Wear device.
*/
private final boolean mIsRunningOnWear;
TaskSnapshotController(WindowManagerService service) {
mService = service;
mPersister = new TaskSnapshotPersister(mService, Environment::getDataSystemCeDirectory);
mLoader = new TaskSnapshotLoader(mPersister);
mCache = new TaskSnapshotCache(mService, mLoader);
mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_LEANBACK);
mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_EMBEDDED);
mIsRunningOnWear = mService.mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WATCH);
mFullSnapshotScale = mService.mContext.getResources().getFloat(
com.android.internal.R.dimen.config_fullTaskSnapshotScale);
}
void systemReady() {
mPersister.start();
}
void onTransitionStarting(DisplayContent displayContent) {
handleClosingApps(displayContent.mClosingApps);
}
/**
* Called when the visibility of an app changes outside of the regular app transition flow.
*/
void notifyAppVisibilityChanged(AppWindowToken appWindowToken, boolean visible) {
if (!visible) {
handleClosingApps(Sets.newArraySet(appWindowToken));
}
}
private void handleClosingApps(ArraySet<AppWindowToken> closingApps) {
if (shouldDisableSnapshots()) {
return;
}
// We need to take a snapshot of the task if and only if all activities of the task are
// either closing or hidden.
getClosingTasks(closingApps, mTmpTasks);
snapshotTasks(mTmpTasks);
mSkipClosingAppSnapshotTasks.clear();
}
/**
* Adds the given {@param tasks} to the list of tasks which should not have their snapshots
* taken upon the next processing of the set of closing apps. The caller is responsible for
* calling {@link #snapshotTasks} to ensure that the task has an up-to-date snapshot.
*/
@VisibleForTesting
void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
mSkipClosingAppSnapshotTasks.addAll(tasks);
}
void snapshotTasks(ArraySet<Task> tasks) {
for (int i = tasks.size() - 1; i >= 0; i--) {
final Task task = tasks.valueAt(i);
final int mode = getSnapshotMode(task);
final TaskSnapshot snapshot;
switch (mode) {
case SNAPSHOT_MODE_NONE:
continue;
case SNAPSHOT_MODE_APP_THEME:
snapshot = drawAppThemeSnapshot(task);
break;
case SNAPSHOT_MODE_REAL:
snapshot = snapshotTask(task);
break;
default:
snapshot = null;
break;
}
if (snapshot != null) {
final GraphicBuffer buffer = snapshot.getSnapshot();
if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
buffer.destroy();
Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x"
+ buffer.getHeight());
} else {
mCache.putSnapshot(task, snapshot);
mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
task.onSnapshotChanged(snapshot);
}
}
}
}
/**
* Retrieves a snapshot. If {@param restoreFromDisk} equals {@code true}, DO HOLD THE WINDOW
* MANAGER LOCK WHEN CALLING THIS METHOD!
*/
@Nullable TaskSnapshot getSnapshot(int taskId, int userId, boolean restoreFromDisk,
boolean reducedResolution) {
return mCache.getSnapshot(taskId, userId, restoreFromDisk, reducedResolution
|| DISABLE_FULL_SIZED_BITMAPS);
}
/**
* Creates a starting surface for {@param token} with {@param snapshot}. DO NOT HOLD THE WINDOW
* MANAGER LOCK WHEN CALLING THIS METHOD!
*/
StartingSurface createStartingSurface(AppWindowToken token,
TaskSnapshot snapshot) {
return TaskSnapshotSurface.create(mService, token, snapshot);
}
/**
* Find the window for a given task to take a snapshot. Top child of the task is usually the one
* we're looking for, but during app transitions, trampoline activities can appear in the
* children, which should be ignored.
*/
@Nullable private AppWindowToken findAppTokenForSnapshot(Task task) {
for (int i = task.getChildCount() - 1; i >= 0; --i) {
final AppWindowToken appWindowToken = task.getChildAt(i);
if (appWindowToken == null || !appWindowToken.isSurfaceShowing()
|| appWindowToken.findMainWindow() == null) {
continue;
}
final boolean hasVisibleChild = appWindowToken.forAllWindows(
// Ensure at least one window for the top app is visible before attempting to
// take a screenshot. Visible here means that the WSA surface is shown and has
// an alpha greater than 0.
ws -> ws.mWinAnimator != null && ws.mWinAnimator.getShown()
&& ws.mWinAnimator.mLastAlpha > 0f, true /* traverseTopToBottom */);
if (hasVisibleChild) {
return appWindowToken;
}
}
return null;
}
@Nullable
SurfaceControl.ScreenshotGraphicBuffer createTaskSnapshot(@NonNull Task task,
float scaleFraction) {
if (task.getSurfaceControl() == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task);
}
return null;
}
task.getBounds(mTmpRect);
mTmpRect.offsetTo(0, 0);
final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
SurfaceControl.captureLayers(
task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction);
final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer()
: null;
if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
return null;
}
return screenshotBuffer;
}
@Nullable private TaskSnapshot snapshotTask(Task task) {
if (!mService.mPolicy.isScreenOn()) {
if (DEBUG_SCREENSHOT) {
Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
}
return null;
}
final AppWindowToken appWindowToken = findAppTokenForSnapshot(task);
if (appWindowToken == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + task);
}
return null;
}
if (appWindowToken.hasCommittedReparentToAnimationLeash()) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + appWindowToken);
}
return null;
}
final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
final float scaleFraction = isLowRamDevice
? mPersister.getReducedScale()
: mFullSnapshotScale;
final WindowState mainWindow = appWindowToken.findMainWindow();
if (mainWindow == null) {
Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + task);
return null;
}
final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer =
createTaskSnapshot(task, scaleFraction);
if (screenshotBuffer == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot for " + task);
}
return null;
}
final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
return new TaskSnapshot(
appWindowToken.mActivityComponent, screenshotBuffer.getGraphicBuffer(),
screenshotBuffer.getColorSpace(),
appWindowToken.getTask().getConfiguration().orientation,
getInsets(mainWindow), isLowRamDevice /* reduced */, scaleFraction /* scale */,
true /* isRealSnapshot */, task.getWindowingMode(), getSystemUiVisibility(task),
!appWindowToken.fillsParent() || isWindowTranslucent);
}
private boolean shouldDisableSnapshots() {
return mIsRunningOnWear || mIsRunningOnTv || mIsRunningOnIoT;
}
private Rect getInsets(WindowState state) {
// XXX(b/72757033): These are insets relative to the window frame, but we're really
// interested in the insets relative to the task bounds.
final Rect insets = minRect(state.getContentInsets(), state.getStableInsets());
InsetUtils.addInsets(insets, state.mAppToken.getLetterboxInsets());
return insets;
}
private Rect minRect(Rect rect1, Rect rect2) {
return new Rect(Math.min(rect1.left, rect2.left),
Math.min(rect1.top, rect2.top),
Math.min(rect1.right, rect2.right),
Math.min(rect1.bottom, rect2.bottom));
}
/**
* Retrieves all closing tasks based on the list of closing apps during an app transition.
*/
@VisibleForTesting
void getClosingTasks(ArraySet<AppWindowToken> closingApps, ArraySet<Task> outClosingTasks) {
outClosingTasks.clear();
for (int i = closingApps.size() - 1; i >= 0; i--) {
final AppWindowToken atoken = closingApps.valueAt(i);
final Task task = atoken.getTask();
// If the task of the app is not visible anymore, it means no other app in that task
// is opening. Thus, the task is closing.
if (task != null && !task.isVisible() && !mSkipClosingAppSnapshotTasks.contains(task)) {
outClosingTasks.add(task);
}
}
}
@VisibleForTesting
int getSnapshotMode(Task task) {
final AppWindowToken topChild = task.getTopChild();
if (!task.isActivityTypeStandardOrUndefined() && !task.isActivityTypeAssistant()) {
return SNAPSHOT_MODE_NONE;
} else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
return SNAPSHOT_MODE_APP_THEME;
} else {
return SNAPSHOT_MODE_REAL;
}
}
/**
* If we are not allowed to take a real screenshot, this attempts to represent the app as best
* as possible by using the theme's window background.
*/
private TaskSnapshot drawAppThemeSnapshot(Task task) {
final AppWindowToken topChild = task.getTopChild();
if (topChild == null) {
return null;
}
final WindowState mainWindow = topChild.findMainWindow();
if (mainWindow == null) {
return null;
}
final int color = ColorUtils.setAlphaComponent(
task.getTaskDescription().getBackgroundColor(), 255);
final LayoutParams attrs = mainWindow.getAttrs();
final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags,
attrs.privateFlags, attrs.systemUiVisibility, task.getTaskDescription(),
mFullSnapshotScale);
final int width = (int) (task.getBounds().width() * mFullSnapshotScale);
final int height = (int) (task.getBounds().height() * mFullSnapshotScale);
final RenderNode node = RenderNode.create("TaskSnapshotController", null);
node.setLeftTopRightBottom(0, 0, width, height);
node.setClipToBounds(false);
final RecordingCanvas c = node.start(width, height);
c.drawColor(color);
decorPainter.setInsets(mainWindow.getContentInsets(), mainWindow.getStableInsets());
decorPainter.drawDecors(c, null /* statusBarExcludeFrame */);
node.end(c);
final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
if (hwBitmap == null) {
return null;
}
// Note, the app theme snapshot is never translucent because we enforce a non-translucent
// color above
return new TaskSnapshot(topChild.mActivityComponent, hwBitmap.createGraphicBufferHandle(),
hwBitmap.getColorSpace(), topChild.getTask().getConfiguration().orientation,
getInsets(mainWindow), ActivityManager.isLowRamDeviceStatic() /* reduced */,
mFullSnapshotScale, false /* isRealSnapshot */, task.getWindowingMode(),
getSystemUiVisibility(task), false);
}
/**
* Called when an {@link AppWindowToken} has been removed.
*/
void onAppRemoved(AppWindowToken wtoken) {
mCache.onAppRemoved(wtoken);
}
/**
* Called when the process of an {@link AppWindowToken} has died.
*/
void onAppDied(AppWindowToken wtoken) {
mCache.onAppDied(wtoken);
}
void notifyTaskRemovedFromRecents(int taskId, int userId) {
mCache.onTaskRemoved(taskId);
mPersister.onTaskRemovedFromRecents(taskId, userId);
}
/**
* See {@link TaskSnapshotPersister#removeObsoleteFiles}
*/
void removeObsoleteTaskFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
mPersister.removeObsoleteFiles(persistentTaskIds, runningUserIds);
}
/**
* Temporarily pauses/unpauses persisting of task snapshots.
*
* @param paused Whether task snapshot persisting should be paused.
*/
void setPersisterPaused(boolean paused) {
mPersister.setPaused(paused);
}
/**
* Called when screen is being turned off.
*/
void screenTurningOff(ScreenOffListener listener) {
if (shouldDisableSnapshots()) {
listener.onScreenOff();
return;
}
// We can't take a snapshot when screen is off, so take a snapshot now!
mHandler.post(() -> {
try {
synchronized (mService.mGlobalLock) {
mTmpTasks.clear();
mService.mRoot.forAllTasks(task -> {
if (task.isVisible()) {
mTmpTasks.add(task);
}
});
snapshotTasks(mTmpTasks);
}
} finally {
listener.onScreenOff();
}
});
}
/**
* @return The SystemUI visibility flags for the top fullscreen window in the given
* {@param task}.
*/
private int getSystemUiVisibility(Task task) {
final AppWindowToken topFullscreenToken = task.getTopFullscreenAppToken();
final WindowState topFullscreenWindow = topFullscreenToken != null
? topFullscreenToken.getTopFullscreenWindow()
: null;
if (topFullscreenWindow != null) {
return topFullscreenWindow.getSystemUiVisibility();
}
return 0;
}
void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "mFullSnapshotScale=" + mFullSnapshotScale);
mCache.dump(pw, prefix);
}
}