blob: 05da9dfe7921db93367938b932b95971dc934838 [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 android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
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.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.RecordingCanvas;
import android.graphics.Rect;
import android.graphics.RenderNode;
import android.hardware.HardwareBuffer;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Pair;
import android.util.Slog;
import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.ThreadedRenderer;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.window.ScreenCapture;
import android.window.SnapshotDrawerUtils;
import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.graphics.ColorUtils;
import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
/**
* Base class for a Snapshot controller
* @param <TYPE> The basic type, either Task or ActivityRecord
* @param <CACHE> The basic cache for either Task or ActivityRecord
*/
abstract class AbsAppSnapshotController<TYPE extends WindowContainer,
CACHE extends SnapshotCache<TYPE>> {
static final String TAG = TAG_WITH_CLASS_NAME ? "SnapshotController" : 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 fake 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;
protected final WindowManagerService mService;
protected final float mHighResSnapshotScale;
/**
* The transition change info of the target to capture screenshot. It is only non-null when
* capturing a snapshot with a given change info. It must be cleared after
* {@link #recordSnapshotInner} is done.
*/
protected Transition.ChangeInfo mCurrentChangeInfo;
/**
* Flag indicating whether we are running on an Android TV device.
*/
protected final boolean mIsRunningOnTv;
/**
* Flag indicating whether we are running on an IoT device.
*/
protected final boolean mIsRunningOnIoT;
protected CACHE mCache;
/**
* Flag indicating if task snapshot is enabled on this device.
*/
private boolean mSnapshotEnabled;
AbsAppSnapshotController(WindowManagerService service) {
mService = service;
mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_LEANBACK);
mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_EMBEDDED);
mHighResSnapshotScale = initSnapshotScale();
}
protected float initSnapshotScale() {
final float config = mService.mContext.getResources().getFloat(
com.android.internal.R.dimen.config_highResTaskSnapshotScale);
return Math.max(Math.min(config, 1f), 0.1f);
}
/**
* Set basic cache to the controller.
*/
protected void initialize(CACHE cache) {
mCache = cache;
}
void setSnapshotEnabled(boolean enabled) {
mSnapshotEnabled = enabled;
}
boolean shouldDisableSnapshots() {
return mIsRunningOnTv || mIsRunningOnIoT || !mSnapshotEnabled;
}
abstract ActivityRecord getTopActivity(TYPE source);
abstract ActivityRecord getTopFullscreenActivity(TYPE source);
abstract ActivityManager.TaskDescription getTaskDescription(TYPE source);
/**
* 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
protected abstract ActivityRecord findAppTokenForSnapshot(TYPE source);
protected abstract boolean use16BitFormat();
/**
* This is different than {@link #recordSnapshotInner(TYPE)} because it doesn't store
* the snapshot to the cache and returns the TaskSnapshot immediately.
*
* This is only used for testing so the snapshot content can be verified.
*/
@VisibleForTesting
TaskSnapshot captureSnapshot(TYPE source) {
final TaskSnapshot snapshot;
switch (getSnapshotMode(source)) {
case SNAPSHOT_MODE_NONE:
return null;
case SNAPSHOT_MODE_APP_THEME:
snapshot = drawAppThemeSnapshot(source);
break;
case SNAPSHOT_MODE_REAL:
snapshot = snapshot(source);
break;
default:
snapshot = null;
break;
}
return snapshot;
}
final TaskSnapshot recordSnapshotInner(TYPE source) {
if (shouldDisableSnapshots()) {
return null;
}
final TaskSnapshot snapshot = captureSnapshot(source);
if (snapshot == null) {
return null;
}
final HardwareBuffer buffer = snapshot.getHardwareBuffer();
if (buffer.getWidth() == 0 || buffer.getHeight() == 0) {
buffer.close();
Slog.e(TAG, "Invalid snapshot dimensions " + buffer.getWidth() + "x"
+ buffer.getHeight());
return null;
} else {
mCache.putSnapshot(source, snapshot);
return snapshot;
}
}
@VisibleForTesting
int getSnapshotMode(TYPE source) {
final int type = source.getActivityType();
if (type == ACTIVITY_TYPE_RECENTS || type == ACTIVITY_TYPE_DREAM) {
return SNAPSHOT_MODE_NONE;
}
if (type == ACTIVITY_TYPE_HOME) {
return SNAPSHOT_MODE_REAL;
}
final ActivityRecord topChild = getTopActivity(source);
if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
return SNAPSHOT_MODE_APP_THEME;
}
return SNAPSHOT_MODE_REAL;
}
@Nullable
TaskSnapshot snapshot(TYPE source) {
TaskSnapshot.Builder builder = new TaskSnapshot.Builder();
final Rect crop = prepareTaskSnapshot(source, builder);
if (crop == null) {
// Failed some pre-req. Has been logged.
return null;
}
Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createSnapshot");
final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = createSnapshot(source,
mHighResSnapshotScale, crop, builder);
Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
if (screenshotBuffer == null) {
// Failed to acquire image. Has been logged.
return null;
}
builder.setCaptureTime(SystemClock.elapsedRealtimeNanos());
builder.setSnapshot(screenshotBuffer.getHardwareBuffer());
builder.setColorSpace(screenshotBuffer.getColorSpace());
return builder.build();
}
@Nullable
ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source,
float scaleFraction, Rect crop, TaskSnapshot.Builder builder) {
if (source.getSurfaceControl() == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + source);
}
return null;
}
SurfaceControl[] excludeLayers;
final WindowState imeWindow = source.getDisplayContent().mInputMethodWindow;
// Exclude IME window snapshot when IME isn't proper to attach to app.
final boolean excludeIme = imeWindow != null && imeWindow.getSurfaceControl() != null
&& !source.getDisplayContent().shouldImeAttachedToApp();
final WindowState navWindow =
source.getDisplayContent().getDisplayPolicy().getNavigationBar();
// If config_attachNavBarToAppDuringTransition is true, the nav bar will be reparent to the
// the swiped app when entering recent app, therefore the task will contain the navigation
// bar and we should exclude it from snapshot.
final boolean excludeNavBar = navWindow != null;
if (excludeIme && excludeNavBar) {
excludeLayers = new SurfaceControl[2];
excludeLayers[0] = imeWindow.getSurfaceControl();
excludeLayers[1] = navWindow.getSurfaceControl();
} else if (excludeIme || excludeNavBar) {
excludeLayers = new SurfaceControl[1];
excludeLayers[0] =
excludeIme ? imeWindow.getSurfaceControl() : navWindow.getSurfaceControl();
} else {
excludeLayers = new SurfaceControl[0];
}
builder.setHasImeSurface(!excludeIme && imeWindow != null && imeWindow.isVisible());
final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
ScreenCapture.captureLayersExcluding(
source.getSurfaceControl(), crop, scaleFraction,
builder.getPixelFormat(), excludeLayers);
final HardwareBuffer buffer = screenshotBuffer == null ? null
: screenshotBuffer.getHardwareBuffer();
if (isInvalidHardwareBuffer(buffer)) {
return null;
}
return screenshotBuffer;
}
static boolean isInvalidHardwareBuffer(HardwareBuffer buffer) {
return buffer == null || buffer.isClosed() // This must be checked before getting size.
|| buffer.getWidth() <= 1 || buffer.getHeight() <= 1;
}
/**
* Validates the state of the Task is appropriate to capture a snapshot, collects
* information from the task and populates the builder.
*
* @param source the window to capture
* @param builder the snapshot builder to populate
*
* @return true if the state of the task is ok to proceed
*/
@VisibleForTesting
@Nullable
Rect prepareTaskSnapshot(TYPE source, TaskSnapshot.Builder builder) {
final Pair<ActivityRecord, WindowState> result = checkIfReadyToSnapshot(source);
if (result == null) {
return null;
}
final ActivityRecord activity = result.first;
final WindowState mainWindow = result.second;
final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(),
mainWindow.getInsetsStateWithVisibilityOverride());
final Rect letterboxInsets = activity.getLetterboxInsets();
InsetUtils.addInsets(contentInsets, letterboxInsets);
builder.setIsRealSnapshot(true);
builder.setId(System.currentTimeMillis());
builder.setContentInsets(contentInsets);
builder.setLetterboxInsets(letterboxInsets);
final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE;
final boolean isShowWallpaper = mainWindow.hasWallpaper();
int pixelFormat = builder.getPixelFormat();
if (pixelFormat == PixelFormat.UNKNOWN) {
pixelFormat = use16BitFormat() && activity.fillsParent()
&& !(isWindowTranslucent && isShowWallpaper)
? PixelFormat.RGB_565
: PixelFormat.RGBA_8888;
}
final boolean isTranslucent = PixelFormat.formatHasAlpha(pixelFormat)
&& (!activity.fillsParent() || isWindowTranslucent);
builder.setTopActivityComponent(activity.mActivityComponent);
builder.setPixelFormat(pixelFormat);
builder.setIsTranslucent(isTranslucent);
builder.setWindowingMode(source.getWindowingMode());
builder.setAppearance(getAppearance(source));
final Configuration taskConfig = activity.getTask().getConfiguration();
final int displayRotation = taskConfig.windowConfiguration.getDisplayRotation();
final Rect outCrop = new Rect();
final Transition.ChangeInfo changeInfo = mCurrentChangeInfo;
if (changeInfo != null && changeInfo.mRotation != displayRotation) {
// For example, the source is closing and display rotation changes at the same time.
// The snapshot should record the state in previous rotation.
outCrop.set(changeInfo.mAbsoluteBounds);
builder.setRotation(changeInfo.mRotation);
builder.setOrientation(changeInfo.mAbsoluteBounds.height()
>= changeInfo.mAbsoluteBounds.width()
? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE);
} else {
outCrop.set(taskConfig.windowConfiguration.getBounds());
builder.setRotation(displayRotation);
builder.setOrientation(taskConfig.orientation);
}
outCrop.offsetTo(0, 0);
builder.setTaskSize(new Point(outCrop.right, outCrop.bottom));
return outCrop;
}
/**
* Check if the state of the Task is appropriate to capture a snapshot, such like the task
* snapshot or the associated IME surface snapshot.
*
* @param source the target object to capture the snapshot
* @return Pair of (the top activity of the task, the main window of the task) if passed the
* state checking. Returns {@code null} if the task state isn't ready to snapshot.
*/
Pair<ActivityRecord, WindowState> checkIfReadyToSnapshot(TYPE source) {
if (!mService.mPolicy.isScreenOn()) {
if (DEBUG_SCREENSHOT) {
Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
}
return null;
}
final ActivityRecord activity = findAppTokenForSnapshot(source);
if (activity == null) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + source);
}
return null;
}
if (activity.hasCommittedReparentToAnimationLeash()) {
if (DEBUG_SCREENSHOT) {
Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + activity);
}
return null;
}
final WindowState mainWindow = activity.findMainWindow();
if (mainWindow == null) {
Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + source);
return null;
}
if (activity.hasFixedRotationTransform()) {
if (DEBUG_SCREENSHOT) {
Slog.i(TAG_WM, "Skip taking screenshot. App has fixed rotation " + activity);
}
// The activity is in a temporal state that it has different rotation than the task.
return null;
}
return new Pair<>(activity, mainWindow);
}
/**
* 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(TYPE source) {
final ActivityRecord topActivity = getTopActivity(source);
if (topActivity == null) {
return null;
}
final WindowState mainWindow = topActivity.findMainWindow();
if (mainWindow == null) {
return null;
}
final ActivityManager.TaskDescription taskDescription = getTaskDescription(source);
final int color = ColorUtils.setAlphaComponent(
taskDescription.getBackgroundColor(), 255);
final WindowManager.LayoutParams attrs = mainWindow.getAttrs();
final Rect taskBounds = source.getBounds();
final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride();
final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState);
final SnapshotDrawerUtils.SystemBarBackgroundPainter
decorPainter = new SnapshotDrawerUtils.SystemBarBackgroundPainter(attrs.flags,
attrs.privateFlags, attrs.insetsFlags.appearance, taskDescription,
mHighResSnapshotScale, mainWindow.getRequestedVisibleTypes());
final int taskWidth = taskBounds.width();
final int taskHeight = taskBounds.height();
final int width = (int) (taskWidth * mHighResSnapshotScale);
final int height = (int) (taskHeight * mHighResSnapshotScale);
final RenderNode node = RenderNode.create("SnapshotController", null);
node.setLeftTopRightBottom(0, 0, width, height);
node.setClipToBounds(false);
final RecordingCanvas c = node.start(width, height);
c.drawColor(color);
decorPainter.setInsets(systemBarInsets);
decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */);
node.end(c);
final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height);
if (hwBitmap == null) {
return null;
}
final Rect contentInsets = new Rect(systemBarInsets);
final Rect letterboxInsets = topActivity.getLetterboxInsets();
InsetUtils.addInsets(contentInsets, letterboxInsets);
// Note, the app theme snapshot is never translucent because we enforce a non-translucent
// color above
return new TaskSnapshot(
System.currentTimeMillis() /* id */,
SystemClock.elapsedRealtimeNanos() /* captureTime */,
topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(),
hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation,
mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight),
contentInsets, letterboxInsets, false /* isLowResolution */,
false /* isRealSnapshot */, source.getWindowingMode(),
getAppearance(source), false /* isTranslucent */, false /* hasImeSurface */);
}
static Rect getSystemBarInsets(Rect frame, InsetsState state) {
return state.calculateInsets(
frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect();
}
/**
* @return The {@link WindowInsetsController.Appearance} flags for the top fullscreen opaque
* window in the given {@param TYPE}.
*/
@WindowInsetsController.Appearance
private int getAppearance(TYPE source) {
final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source);
final WindowState topFullscreenOpaqueWindow = topFullscreenActivity != null
? topFullscreenActivity.getTopFullscreenOpaqueWindow()
: null;
if (topFullscreenOpaqueWindow != null) {
return topFullscreenOpaqueWindow.mAttrs.insetsFlags.appearance;
}
return 0;
}
/**
* Called when an {@link ActivityRecord} has been removed.
*/
void onAppRemoved(ActivityRecord activity) {
mCache.onAppRemoved(activity);
}
/**
* Called when the process of an {@link ActivityRecord} has died.
*/
void onAppDied(ActivityRecord activity) {
mCache.onAppDied(activity);
}
boolean isAnimatingByRecents(@NonNull Task task) {
return task.isAnimatingByRecents();
}
void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "mHighResSnapshotScale=" + mHighResSnapshotScale);
pw.println(prefix + "mSnapshotEnabled=" + mSnapshotEnabled);
mCache.dump(pw, prefix);
}
}