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 package com.android.server.wm; 17 18 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT; 19 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; 20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.ActivityManager; 25 import android.content.pm.PackageManager; 26 import android.graphics.Bitmap; 27 import android.graphics.PixelFormat; 28 import android.graphics.Point; 29 import android.graphics.RecordingCanvas; 30 import android.graphics.Rect; 31 import android.graphics.RenderNode; 32 import android.hardware.HardwareBuffer; 33 import android.os.SystemClock; 34 import android.os.Trace; 35 import android.util.Pair; 36 import android.util.Slog; 37 import android.view.InsetsState; 38 import android.view.SurfaceControl; 39 import android.view.ThreadedRenderer; 40 import android.view.WindowInsets; 41 import android.view.WindowInsetsController; 42 import android.view.WindowManager; 43 import android.window.ScreenCapture; 44 import android.window.SnapshotDrawerUtils; 45 import android.window.TaskSnapshot; 46 47 import com.android.internal.annotations.VisibleForTesting; 48 import com.android.internal.graphics.ColorUtils; 49 import com.android.server.wm.utils.InsetUtils; 50 51 import java.io.PrintWriter; 52 53 /** 54 * Base class for a Snapshot controller 55 * @param <TYPE> The basic type, either Task or ActivityRecord 56 * @param <CACHE> The basic cache for either Task or ActivityRecord 57 */ 58 abstract class AbsAppSnapshotController<TYPE extends WindowContainer, 59 CACHE extends SnapshotCache<TYPE>> { 60 static final String TAG = TAG_WITH_CLASS_NAME ? "SnapshotController" : TAG_WM; 61 /** 62 * Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be 63 * used as the snapshot. 64 */ 65 @VisibleForTesting 66 static final int SNAPSHOT_MODE_REAL = 0; 67 /** 68 * Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but 69 * we should try to use the app theme to create a fake representation of the app. 70 */ 71 @VisibleForTesting 72 static final int SNAPSHOT_MODE_APP_THEME = 1; 73 /** 74 * Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot. 75 */ 76 @VisibleForTesting 77 static final int SNAPSHOT_MODE_NONE = 2; 78 79 protected final WindowManagerService mService; 80 protected final float mHighResSnapshotScale; 81 private final Rect mTmpRect = new Rect(); 82 /** 83 * Flag indicating whether we are running on an Android TV device. 84 */ 85 protected final boolean mIsRunningOnTv; 86 /** 87 * Flag indicating whether we are running on an IoT device. 88 */ 89 protected final boolean mIsRunningOnIoT; 90 91 protected CACHE mCache; 92 /** 93 * Flag indicating if task snapshot is enabled on this device. 94 */ 95 private boolean mSnapshotEnabled; 96 AbsAppSnapshotController(WindowManagerService service)97 AbsAppSnapshotController(WindowManagerService service) { 98 mService = service; 99 mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature( 100 PackageManager.FEATURE_LEANBACK); 101 mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature( 102 PackageManager.FEATURE_EMBEDDED); 103 mHighResSnapshotScale = initSnapshotScale(); 104 } 105 initSnapshotScale()106 protected float initSnapshotScale() { 107 final float config = mService.mContext.getResources().getFloat( 108 com.android.internal.R.dimen.config_highResTaskSnapshotScale); 109 return Math.max(Math.min(config, 1f), 0.1f); 110 } 111 112 /** 113 * Set basic cache to the controller. 114 */ initialize(CACHE cache)115 protected void initialize(CACHE cache) { 116 mCache = cache; 117 } 118 setSnapshotEnabled(boolean enabled)119 void setSnapshotEnabled(boolean enabled) { 120 mSnapshotEnabled = enabled; 121 } 122 shouldDisableSnapshots()123 boolean shouldDisableSnapshots() { 124 return mIsRunningOnTv || mIsRunningOnIoT || !mSnapshotEnabled; 125 } 126 getTopActivity(TYPE source)127 abstract ActivityRecord getTopActivity(TYPE source); getTopFullscreenActivity(TYPE source)128 abstract ActivityRecord getTopFullscreenActivity(TYPE source); getTaskDescription(TYPE source)129 abstract ActivityManager.TaskDescription getTaskDescription(TYPE source); 130 /** 131 * Find the window for a given task to take a snapshot. Top child of the task is usually the one 132 * we're looking for, but during app transitions, trampoline activities can appear in the 133 * children, which should be ignored. 134 */ 135 @Nullable findAppTokenForSnapshot(TYPE source)136 protected abstract ActivityRecord findAppTokenForSnapshot(TYPE source); use16BitFormat()137 protected abstract boolean use16BitFormat(); 138 139 /** 140 * This is different than {@link #recordSnapshotInner(TYPE, boolean)} because it doesn't store 141 * the snapshot to the cache and returns the TaskSnapshot immediately. 142 * 143 * This is only used for testing so the snapshot content can be verified. 144 */ 145 // TODO(b/264551777): clean up the "snapshotHome" argument 146 @VisibleForTesting captureSnapshot(TYPE source, boolean snapshotHome)147 TaskSnapshot captureSnapshot(TYPE source, boolean snapshotHome) { 148 final TaskSnapshot snapshot; 149 if (snapshotHome) { 150 snapshot = snapshot(source); 151 } else { 152 switch (getSnapshotMode(source)) { 153 case SNAPSHOT_MODE_NONE: 154 return null; 155 case SNAPSHOT_MODE_APP_THEME: 156 snapshot = drawAppThemeSnapshot(source); 157 break; 158 case SNAPSHOT_MODE_REAL: 159 snapshot = snapshot(source); 160 break; 161 default: 162 snapshot = null; 163 break; 164 } 165 } 166 return snapshot; 167 } 168 recordSnapshotInner(TYPE source, boolean allowSnapshotHome)169 final TaskSnapshot recordSnapshotInner(TYPE source, boolean allowSnapshotHome) { 170 if (shouldDisableSnapshots()) { 171 return null; 172 } 173 final boolean snapshotHome = allowSnapshotHome && source.isActivityTypeHome(); 174 final TaskSnapshot snapshot = captureSnapshot(source, snapshotHome); 175 if (snapshot == null) { 176 return null; 177 } 178 final HardwareBuffer buffer = snapshot.getHardwareBuffer(); 179 if (buffer.getWidth() == 0 || buffer.getHeight() == 0) { 180 buffer.close(); 181 Slog.e(TAG, "Invalid snapshot dimensions " + buffer.getWidth() + "x" 182 + buffer.getHeight()); 183 return null; 184 } else { 185 mCache.putSnapshot(source, snapshot); 186 return snapshot; 187 } 188 } 189 190 @VisibleForTesting getSnapshotMode(TYPE source)191 int getSnapshotMode(TYPE source) { 192 final ActivityRecord topChild = getTopActivity(source); 193 if (!source.isActivityTypeStandardOrUndefined() && !source.isActivityTypeAssistant()) { 194 return SNAPSHOT_MODE_NONE; 195 } else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) { 196 return SNAPSHOT_MODE_APP_THEME; 197 } else { 198 return SNAPSHOT_MODE_REAL; 199 } 200 } 201 202 @Nullable snapshot(TYPE source)203 TaskSnapshot snapshot(TYPE source) { 204 return snapshot(source, PixelFormat.UNKNOWN); 205 } 206 207 @Nullable snapshot(TYPE source, int pixelFormat)208 TaskSnapshot snapshot(TYPE source, int pixelFormat) { 209 TaskSnapshot.Builder builder = new TaskSnapshot.Builder(); 210 if (!prepareTaskSnapshot(source, pixelFormat, builder)) { 211 // Failed some pre-req. Has been logged. 212 return null; 213 } 214 final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = 215 createSnapshot(source, builder); 216 if (screenshotBuffer == null) { 217 // Failed to acquire image. Has been logged. 218 return null; 219 } 220 builder.setCaptureTime(SystemClock.elapsedRealtimeNanos()); 221 builder.setSnapshot(screenshotBuffer.getHardwareBuffer()); 222 builder.setColorSpace(screenshotBuffer.getColorSpace()); 223 return builder.build(); 224 } 225 226 @Nullable createSnapshot(@onNull TYPE source, TaskSnapshot.Builder builder)227 ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source, 228 TaskSnapshot.Builder builder) { 229 Point taskSize = new Point(); 230 Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "createSnapshot"); 231 final ScreenCapture.ScreenshotHardwareBuffer taskSnapshot = createSnapshot(source, 232 mHighResSnapshotScale, builder.getPixelFormat(), taskSize, builder); 233 Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); 234 builder.setTaskSize(taskSize); 235 return taskSnapshot; 236 } 237 238 @Nullable createSnapshot(@onNull TYPE source, float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder)239 ScreenCapture.ScreenshotHardwareBuffer createSnapshot(@NonNull TYPE source, 240 float scaleFraction, int pixelFormat, Point outTaskSize, TaskSnapshot.Builder builder) { 241 if (source.getSurfaceControl() == null) { 242 if (DEBUG_SCREENSHOT) { 243 Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + source); 244 } 245 return null; 246 } 247 mTmpRect.setEmpty(); 248 if (source.mTransitionController.inFinishingTransition(source)) { 249 final Transition.ChangeInfo changeInfo = source.mTransitionController 250 .mFinishingTransition.mChanges.get(source); 251 if (changeInfo != null) { 252 mTmpRect.set(changeInfo.mAbsoluteBounds); 253 } 254 } 255 if (mTmpRect.isEmpty()) { 256 source.getBounds(mTmpRect); 257 } 258 mTmpRect.offsetTo(0, 0); 259 SurfaceControl[] excludeLayers; 260 final WindowState imeWindow = source.getDisplayContent().mInputMethodWindow; 261 // Exclude IME window snapshot when IME isn't proper to attach to app. 262 final boolean excludeIme = imeWindow != null && imeWindow.getSurfaceControl() != null 263 && !source.getDisplayContent().shouldImeAttachedToApp(); 264 final WindowState navWindow = 265 source.getDisplayContent().getDisplayPolicy().getNavigationBar(); 266 // If config_attachNavBarToAppDuringTransition is true, the nav bar will be reparent to the 267 // the swiped app when entering recent app, therefore the task will contain the navigation 268 // bar and we should exclude it from snapshot. 269 final boolean excludeNavBar = navWindow != null; 270 if (excludeIme && excludeNavBar) { 271 excludeLayers = new SurfaceControl[2]; 272 excludeLayers[0] = imeWindow.getSurfaceControl(); 273 excludeLayers[1] = navWindow.getSurfaceControl(); 274 } else if (excludeIme || excludeNavBar) { 275 excludeLayers = new SurfaceControl[1]; 276 excludeLayers[0] = 277 excludeIme ? imeWindow.getSurfaceControl() : navWindow.getSurfaceControl(); 278 } else { 279 excludeLayers = new SurfaceControl[0]; 280 } 281 builder.setHasImeSurface(!excludeIme && imeWindow != null && imeWindow.isVisible()); 282 final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = 283 ScreenCapture.captureLayersExcluding( 284 source.getSurfaceControl(), mTmpRect, scaleFraction, 285 pixelFormat, excludeLayers); 286 if (outTaskSize != null) { 287 outTaskSize.x = mTmpRect.width(); 288 outTaskSize.y = mTmpRect.height(); 289 } 290 final HardwareBuffer buffer = screenshotBuffer == null ? null 291 : screenshotBuffer.getHardwareBuffer(); 292 if (isInvalidHardwareBuffer(buffer)) { 293 return null; 294 } 295 return screenshotBuffer; 296 } 297 isInvalidHardwareBuffer(HardwareBuffer buffer)298 static boolean isInvalidHardwareBuffer(HardwareBuffer buffer) { 299 return buffer == null || buffer.isClosed() // This must be checked before getting size. 300 || buffer.getWidth() <= 1 || buffer.getHeight() <= 1; 301 } 302 303 /** 304 * Validates the state of the Task is appropriate to capture a snapshot, collects 305 * information from the task and populates the builder. 306 * 307 * @param source the window to capture 308 * @param pixelFormat the desired pixel format, or {@link PixelFormat#UNKNOWN} to 309 * automatically select 310 * @param builder the snapshot builder to populate 311 * 312 * @return true if the state of the task is ok to proceed 313 */ 314 @VisibleForTesting prepareTaskSnapshot(TYPE source, int pixelFormat, TaskSnapshot.Builder builder)315 boolean prepareTaskSnapshot(TYPE source, int pixelFormat, TaskSnapshot.Builder builder) { 316 final Pair<ActivityRecord, WindowState> result = checkIfReadyToSnapshot(source); 317 if (result == null) { 318 return false; 319 } 320 final ActivityRecord activity = result.first; 321 final WindowState mainWindow = result.second; 322 final Rect contentInsets = getSystemBarInsets(mainWindow.getFrame(), 323 mainWindow.getInsetsStateWithVisibilityOverride()); 324 final Rect letterboxInsets = activity.getLetterboxInsets(); 325 InsetUtils.addInsets(contentInsets, letterboxInsets); 326 builder.setIsRealSnapshot(true); 327 builder.setId(System.currentTimeMillis()); 328 builder.setContentInsets(contentInsets); 329 builder.setLetterboxInsets(letterboxInsets); 330 final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE; 331 final boolean isShowWallpaper = mainWindow.hasWallpaper(); 332 if (pixelFormat == PixelFormat.UNKNOWN) { 333 pixelFormat = use16BitFormat() && activity.fillsParent() 334 && !(isWindowTranslucent && isShowWallpaper) 335 ? PixelFormat.RGB_565 336 : PixelFormat.RGBA_8888; 337 } 338 final boolean isTranslucent = PixelFormat.formatHasAlpha(pixelFormat) 339 && (!activity.fillsParent() || isWindowTranslucent); 340 builder.setTopActivityComponent(activity.mActivityComponent); 341 builder.setPixelFormat(pixelFormat); 342 builder.setIsTranslucent(isTranslucent); 343 builder.setOrientation(activity.getTask().getConfiguration().orientation); 344 builder.setRotation(activity.getTask().getDisplayContent().getRotation()); 345 builder.setWindowingMode(source.getWindowingMode()); 346 builder.setAppearance(getAppearance(source)); 347 return true; 348 } 349 350 /** 351 * Check if the state of the Task is appropriate to capture a snapshot, such like the task 352 * snapshot or the associated IME surface snapshot. 353 * 354 * @param source the target object to capture the snapshot 355 * @return Pair of (the top activity of the task, the main window of the task) if passed the 356 * state checking. Returns {@code null} if the task state isn't ready to snapshot. 357 */ checkIfReadyToSnapshot(TYPE source)358 Pair<ActivityRecord, WindowState> checkIfReadyToSnapshot(TYPE source) { 359 if (!mService.mPolicy.isScreenOn()) { 360 if (DEBUG_SCREENSHOT) { 361 Slog.i(TAG_WM, "Attempted to take screenshot while display was off."); 362 } 363 return null; 364 } 365 final ActivityRecord activity = findAppTokenForSnapshot(source); 366 if (activity == null) { 367 if (DEBUG_SCREENSHOT) { 368 Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + source); 369 } 370 return null; 371 } 372 if (activity.hasCommittedReparentToAnimationLeash()) { 373 if (DEBUG_SCREENSHOT) { 374 Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + activity); 375 } 376 return null; 377 } 378 final WindowState mainWindow = activity.findMainWindow(); 379 if (mainWindow == null) { 380 Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + source); 381 return null; 382 } 383 if (activity.hasFixedRotationTransform()) { 384 if (DEBUG_SCREENSHOT) { 385 Slog.i(TAG_WM, "Skip taking screenshot. App has fixed rotation " + activity); 386 } 387 // The activity is in a temporal state that it has different rotation than the task. 388 return null; 389 } 390 return new Pair<>(activity, mainWindow); 391 } 392 393 /** 394 * If we are not allowed to take a real screenshot, this attempts to represent the app as best 395 * as possible by using the theme's window background. 396 */ drawAppThemeSnapshot(TYPE source)397 private TaskSnapshot drawAppThemeSnapshot(TYPE source) { 398 final ActivityRecord topActivity = getTopActivity(source); 399 if (topActivity == null) { 400 return null; 401 } 402 final WindowState mainWindow = topActivity.findMainWindow(); 403 if (mainWindow == null) { 404 return null; 405 } 406 final ActivityManager.TaskDescription taskDescription = getTaskDescription(source); 407 final int color = ColorUtils.setAlphaComponent( 408 taskDescription.getBackgroundColor(), 255); 409 final WindowManager.LayoutParams attrs = mainWindow.getAttrs(); 410 final Rect taskBounds = source.getBounds(); 411 final InsetsState insetsState = mainWindow.getInsetsStateWithVisibilityOverride(); 412 final Rect systemBarInsets = getSystemBarInsets(mainWindow.getFrame(), insetsState); 413 final SnapshotDrawerUtils.SystemBarBackgroundPainter 414 decorPainter = new SnapshotDrawerUtils.SystemBarBackgroundPainter(attrs.flags, 415 attrs.privateFlags, attrs.insetsFlags.appearance, taskDescription, 416 mHighResSnapshotScale, mainWindow.getRequestedVisibleTypes()); 417 final int taskWidth = taskBounds.width(); 418 final int taskHeight = taskBounds.height(); 419 final int width = (int) (taskWidth * mHighResSnapshotScale); 420 final int height = (int) (taskHeight * mHighResSnapshotScale); 421 final RenderNode node = RenderNode.create("SnapshotController", null); 422 node.setLeftTopRightBottom(0, 0, width, height); 423 node.setClipToBounds(false); 424 final RecordingCanvas c = node.start(width, height); 425 c.drawColor(color); 426 decorPainter.setInsets(systemBarInsets); 427 decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */); 428 node.end(c); 429 final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); 430 if (hwBitmap == null) { 431 return null; 432 } 433 final Rect contentInsets = new Rect(systemBarInsets); 434 final Rect letterboxInsets = topActivity.getLetterboxInsets(); 435 InsetUtils.addInsets(contentInsets, letterboxInsets); 436 // Note, the app theme snapshot is never translucent because we enforce a non-translucent 437 // color above 438 return new TaskSnapshot( 439 System.currentTimeMillis() /* id */, 440 SystemClock.elapsedRealtimeNanos() /* captureTime */, 441 topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(), 442 hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation, 443 mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight), 444 contentInsets, letterboxInsets, false /* isLowResolution */, 445 false /* isRealSnapshot */, source.getWindowingMode(), 446 getAppearance(source), false /* isTranslucent */, false /* hasImeSurface */); 447 } 448 getSystemBarInsets(Rect frame, InsetsState state)449 static Rect getSystemBarInsets(Rect frame, InsetsState state) { 450 return state.calculateInsets( 451 frame, WindowInsets.Type.systemBars(), false /* ignoreVisibility */).toRect(); 452 } 453 454 /** 455 * @return The {@link WindowInsetsController.Appearance} flags for the top fullscreen opaque 456 * window in the given {@param TYPE}. 457 */ 458 @WindowInsetsController.Appearance getAppearance(TYPE source)459 private int getAppearance(TYPE source) { 460 final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(source); 461 final WindowState topFullscreenOpaqueWindow = topFullscreenActivity != null 462 ? topFullscreenActivity.getTopFullscreenOpaqueWindow() 463 : null; 464 if (topFullscreenOpaqueWindow != null) { 465 return topFullscreenOpaqueWindow.mAttrs.insetsFlags.appearance; 466 } 467 return 0; 468 } 469 470 /** 471 * Called when an {@link ActivityRecord} has been removed. 472 */ onAppRemoved(ActivityRecord activity)473 void onAppRemoved(ActivityRecord activity) { 474 mCache.onAppRemoved(activity); 475 } 476 477 /** 478 * Called when the process of an {@link ActivityRecord} has died. 479 */ onAppDied(ActivityRecord activity)480 void onAppDied(ActivityRecord activity) { 481 mCache.onAppDied(activity); 482 } 483 isAnimatingByRecents(@onNull Task task)484 boolean isAnimatingByRecents(@NonNull Task task) { 485 return task.isAnimatingByRecents(); 486 } 487 dump(PrintWriter pw, String prefix)488 void dump(PrintWriter pw, String prefix) { 489 pw.println(prefix + "mHighResSnapshotScale=" + mHighResSnapshotScale); 490 pw.println(prefix + "mSnapshotEnabled=" + mSnapshotEnabled); 491 mCache.dump(pw, prefix); 492 } 493 } 494