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