1 /* 2 * Copyright (C) 2016 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 17 package com.android.server.wm; 18 19 import static com.android.server.wm.TaskSnapshotPersister.DISABLE_FULL_SIZED_BITMAPS; 20 import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT; 21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; 22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.ActivityManager; 27 import android.app.ActivityManager.TaskSnapshot; 28 import android.content.pm.PackageManager; 29 import android.graphics.Bitmap; 30 import android.graphics.GraphicBuffer; 31 import android.graphics.PixelFormat; 32 import android.graphics.RecordingCanvas; 33 import android.graphics.Rect; 34 import android.graphics.RenderNode; 35 import android.os.Environment; 36 import android.os.Handler; 37 import android.util.ArraySet; 38 import android.util.Slog; 39 import android.view.SurfaceControl; 40 import android.view.ThreadedRenderer; 41 import android.view.WindowManager.LayoutParams; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.graphics.ColorUtils; 45 import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; 46 import com.android.server.policy.WindowManagerPolicy.StartingSurface; 47 import com.android.server.wm.TaskSnapshotSurface.SystemBarBackgroundPainter; 48 import com.android.server.wm.utils.InsetUtils; 49 50 import com.google.android.collect.Sets; 51 52 import java.io.PrintWriter; 53 54 /** 55 * When an app token becomes invisible, we take a snapshot (bitmap) of the corresponding task and 56 * put it into our cache. Internally we use gralloc buffers to be able to draw them wherever we 57 * like without any copying. 58 * <p> 59 * System applications may retrieve a snapshot to represent the current state of a task, and draw 60 * them in their own process. 61 * <p> 62 * When we task becomes visible again, we show a starting window with the snapshot as the content to 63 * make app transitions more responsive. 64 * <p> 65 * To access this class, acquire the global window manager lock. 66 */ 67 class TaskSnapshotController { 68 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotController" : TAG_WM; 69 70 /** 71 * Return value for {@link #getSnapshotMode}: We are allowed to take a real screenshot to be 72 * used as the snapshot. 73 */ 74 @VisibleForTesting 75 static final int SNAPSHOT_MODE_REAL = 0; 76 77 /** 78 * Return value for {@link #getSnapshotMode}: We are not allowed to take a real screenshot but 79 * we should try to use the app theme to create a dummy representation of the app. 80 */ 81 @VisibleForTesting 82 static final int SNAPSHOT_MODE_APP_THEME = 1; 83 84 /** 85 * Return value for {@link #getSnapshotMode}: We aren't allowed to take any snapshot. 86 */ 87 @VisibleForTesting 88 static final int SNAPSHOT_MODE_NONE = 2; 89 90 private final WindowManagerService mService; 91 92 private final TaskSnapshotCache mCache; 93 private final TaskSnapshotPersister mPersister; 94 private final TaskSnapshotLoader mLoader; 95 private final ArraySet<Task> mSkipClosingAppSnapshotTasks = new ArraySet<>(); 96 private final ArraySet<Task> mTmpTasks = new ArraySet<>(); 97 private final Handler mHandler = new Handler(); 98 private final float mFullSnapshotScale; 99 100 private final Rect mTmpRect = new Rect(); 101 102 /** 103 * Flag indicating whether we are running on an Android TV device. 104 */ 105 private final boolean mIsRunningOnTv; 106 107 /** 108 * Flag indicating whether we are running on an IoT device. 109 */ 110 private final boolean mIsRunningOnIoT; 111 112 /** 113 * Flag indicating whether we are running on an Android Wear device. 114 */ 115 private final boolean mIsRunningOnWear; 116 TaskSnapshotController(WindowManagerService service)117 TaskSnapshotController(WindowManagerService service) { 118 mService = service; 119 mPersister = new TaskSnapshotPersister(mService, Environment::getDataSystemCeDirectory); 120 mLoader = new TaskSnapshotLoader(mPersister); 121 mCache = new TaskSnapshotCache(mService, mLoader); 122 mIsRunningOnTv = mService.mContext.getPackageManager().hasSystemFeature( 123 PackageManager.FEATURE_LEANBACK); 124 mIsRunningOnIoT = mService.mContext.getPackageManager().hasSystemFeature( 125 PackageManager.FEATURE_EMBEDDED); 126 mIsRunningOnWear = mService.mContext.getPackageManager().hasSystemFeature( 127 PackageManager.FEATURE_WATCH); 128 mFullSnapshotScale = mService.mContext.getResources().getFloat( 129 com.android.internal.R.dimen.config_fullTaskSnapshotScale); 130 } 131 systemReady()132 void systemReady() { 133 mPersister.start(); 134 } 135 onTransitionStarting(DisplayContent displayContent)136 void onTransitionStarting(DisplayContent displayContent) { 137 handleClosingApps(displayContent.mClosingApps); 138 } 139 140 /** 141 * Called when the visibility of an app changes outside of the regular app transition flow. 142 */ notifyAppVisibilityChanged(AppWindowToken appWindowToken, boolean visible)143 void notifyAppVisibilityChanged(AppWindowToken appWindowToken, boolean visible) { 144 if (!visible) { 145 handleClosingApps(Sets.newArraySet(appWindowToken)); 146 } 147 } 148 handleClosingApps(ArraySet<AppWindowToken> closingApps)149 private void handleClosingApps(ArraySet<AppWindowToken> closingApps) { 150 if (shouldDisableSnapshots()) { 151 return; 152 } 153 154 // We need to take a snapshot of the task if and only if all activities of the task are 155 // either closing or hidden. 156 getClosingTasks(closingApps, mTmpTasks); 157 snapshotTasks(mTmpTasks); 158 mSkipClosingAppSnapshotTasks.clear(); 159 } 160 161 /** 162 * Adds the given {@param tasks} to the list of tasks which should not have their snapshots 163 * taken upon the next processing of the set of closing apps. The caller is responsible for 164 * calling {@link #snapshotTasks} to ensure that the task has an up-to-date snapshot. 165 */ 166 @VisibleForTesting addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks)167 void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) { 168 mSkipClosingAppSnapshotTasks.addAll(tasks); 169 } 170 snapshotTasks(ArraySet<Task> tasks)171 void snapshotTasks(ArraySet<Task> tasks) { 172 for (int i = tasks.size() - 1; i >= 0; i--) { 173 final Task task = tasks.valueAt(i); 174 final int mode = getSnapshotMode(task); 175 final TaskSnapshot snapshot; 176 switch (mode) { 177 case SNAPSHOT_MODE_NONE: 178 continue; 179 case SNAPSHOT_MODE_APP_THEME: 180 snapshot = drawAppThemeSnapshot(task); 181 break; 182 case SNAPSHOT_MODE_REAL: 183 snapshot = snapshotTask(task); 184 break; 185 default: 186 snapshot = null; 187 break; 188 } 189 if (snapshot != null) { 190 final GraphicBuffer buffer = snapshot.getSnapshot(); 191 if (buffer.getWidth() == 0 || buffer.getHeight() == 0) { 192 buffer.destroy(); 193 Slog.e(TAG, "Invalid task snapshot dimensions " + buffer.getWidth() + "x" 194 + buffer.getHeight()); 195 } else { 196 mCache.putSnapshot(task, snapshot); 197 mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); 198 task.onSnapshotChanged(snapshot); 199 } 200 } 201 } 202 } 203 204 /** 205 * Retrieves a snapshot. If {@param restoreFromDisk} equals {@code true}, DO HOLD THE WINDOW 206 * MANAGER LOCK WHEN CALLING THIS METHOD! 207 */ getSnapshot(int taskId, int userId, boolean restoreFromDisk, boolean reducedResolution)208 @Nullable TaskSnapshot getSnapshot(int taskId, int userId, boolean restoreFromDisk, 209 boolean reducedResolution) { 210 return mCache.getSnapshot(taskId, userId, restoreFromDisk, reducedResolution 211 || DISABLE_FULL_SIZED_BITMAPS); 212 } 213 214 /** 215 * Creates a starting surface for {@param token} with {@param snapshot}. DO NOT HOLD THE WINDOW 216 * MANAGER LOCK WHEN CALLING THIS METHOD! 217 */ createStartingSurface(AppWindowToken token, TaskSnapshot snapshot)218 StartingSurface createStartingSurface(AppWindowToken token, 219 TaskSnapshot snapshot) { 220 return TaskSnapshotSurface.create(mService, token, snapshot); 221 } 222 223 /** 224 * Find the window for a given task to take a snapshot. Top child of the task is usually the one 225 * we're looking for, but during app transitions, trampoline activities can appear in the 226 * children, which should be ignored. 227 */ findAppTokenForSnapshot(Task task)228 @Nullable private AppWindowToken findAppTokenForSnapshot(Task task) { 229 for (int i = task.getChildCount() - 1; i >= 0; --i) { 230 final AppWindowToken appWindowToken = task.getChildAt(i); 231 if (appWindowToken == null || !appWindowToken.isSurfaceShowing() 232 || appWindowToken.findMainWindow() == null) { 233 continue; 234 } 235 final boolean hasVisibleChild = appWindowToken.forAllWindows( 236 // Ensure at least one window for the top app is visible before attempting to 237 // take a screenshot. Visible here means that the WSA surface is shown and has 238 // an alpha greater than 0. 239 ws -> ws.mWinAnimator != null && ws.mWinAnimator.getShown() 240 && ws.mWinAnimator.mLastAlpha > 0f, true /* traverseTopToBottom */); 241 if (hasVisibleChild) { 242 return appWindowToken; 243 } 244 } 245 return null; 246 } 247 248 @Nullable createTaskSnapshot(@onNull Task task, float scaleFraction)249 SurfaceControl.ScreenshotGraphicBuffer createTaskSnapshot(@NonNull Task task, 250 float scaleFraction) { 251 if (task.getSurfaceControl() == null) { 252 if (DEBUG_SCREENSHOT) { 253 Slog.w(TAG_WM, "Failed to take screenshot. No surface control for " + task); 254 } 255 return null; 256 } 257 task.getBounds(mTmpRect); 258 mTmpRect.offsetTo(0, 0); 259 final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer = 260 SurfaceControl.captureLayers( 261 task.getSurfaceControl().getHandle(), mTmpRect, scaleFraction); 262 final GraphicBuffer buffer = screenshotBuffer != null ? screenshotBuffer.getGraphicBuffer() 263 : null; 264 if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) { 265 return null; 266 } 267 return screenshotBuffer; 268 } 269 snapshotTask(Task task)270 @Nullable private TaskSnapshot snapshotTask(Task task) { 271 if (!mService.mPolicy.isScreenOn()) { 272 if (DEBUG_SCREENSHOT) { 273 Slog.i(TAG_WM, "Attempted to take screenshot while display was off."); 274 } 275 return null; 276 } 277 278 final AppWindowToken appWindowToken = findAppTokenForSnapshot(task); 279 if (appWindowToken == null) { 280 if (DEBUG_SCREENSHOT) { 281 Slog.w(TAG_WM, "Failed to take screenshot. No visible windows for " + task); 282 } 283 return null; 284 } 285 if (appWindowToken.hasCommittedReparentToAnimationLeash()) { 286 if (DEBUG_SCREENSHOT) { 287 Slog.w(TAG_WM, "Failed to take screenshot. App is animating " + appWindowToken); 288 } 289 return null; 290 } 291 292 final boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic(); 293 final float scaleFraction = isLowRamDevice 294 ? mPersister.getReducedScale() 295 : mFullSnapshotScale; 296 297 final WindowState mainWindow = appWindowToken.findMainWindow(); 298 if (mainWindow == null) { 299 Slog.w(TAG_WM, "Failed to take screenshot. No main window for " + task); 300 return null; 301 } 302 final SurfaceControl.ScreenshotGraphicBuffer screenshotBuffer = 303 createTaskSnapshot(task, scaleFraction); 304 305 if (screenshotBuffer == null) { 306 if (DEBUG_SCREENSHOT) { 307 Slog.w(TAG_WM, "Failed to take screenshot for " + task); 308 } 309 return null; 310 } 311 final boolean isWindowTranslucent = mainWindow.getAttrs().format != PixelFormat.OPAQUE; 312 return new TaskSnapshot( 313 appWindowToken.mActivityComponent, screenshotBuffer.getGraphicBuffer(), 314 screenshotBuffer.getColorSpace(), 315 appWindowToken.getTask().getConfiguration().orientation, 316 getInsets(mainWindow), isLowRamDevice /* reduced */, scaleFraction /* scale */, 317 true /* isRealSnapshot */, task.getWindowingMode(), getSystemUiVisibility(task), 318 !appWindowToken.fillsParent() || isWindowTranslucent); 319 } 320 shouldDisableSnapshots()321 private boolean shouldDisableSnapshots() { 322 return mIsRunningOnWear || mIsRunningOnTv || mIsRunningOnIoT; 323 } 324 getInsets(WindowState state)325 private Rect getInsets(WindowState state) { 326 // XXX(b/72757033): These are insets relative to the window frame, but we're really 327 // interested in the insets relative to the task bounds. 328 final Rect insets = minRect(state.getContentInsets(), state.getStableInsets()); 329 InsetUtils.addInsets(insets, state.mAppToken.getLetterboxInsets()); 330 return insets; 331 } 332 minRect(Rect rect1, Rect rect2)333 private Rect minRect(Rect rect1, Rect rect2) { 334 return new Rect(Math.min(rect1.left, rect2.left), 335 Math.min(rect1.top, rect2.top), 336 Math.min(rect1.right, rect2.right), 337 Math.min(rect1.bottom, rect2.bottom)); 338 } 339 340 /** 341 * Retrieves all closing tasks based on the list of closing apps during an app transition. 342 */ 343 @VisibleForTesting getClosingTasks(ArraySet<AppWindowToken> closingApps, ArraySet<Task> outClosingTasks)344 void getClosingTasks(ArraySet<AppWindowToken> closingApps, ArraySet<Task> outClosingTasks) { 345 outClosingTasks.clear(); 346 for (int i = closingApps.size() - 1; i >= 0; i--) { 347 final AppWindowToken atoken = closingApps.valueAt(i); 348 final Task task = atoken.getTask(); 349 350 // If the task of the app is not visible anymore, it means no other app in that task 351 // is opening. Thus, the task is closing. 352 if (task != null && !task.isVisible() && !mSkipClosingAppSnapshotTasks.contains(task)) { 353 outClosingTasks.add(task); 354 } 355 } 356 } 357 358 @VisibleForTesting getSnapshotMode(Task task)359 int getSnapshotMode(Task task) { 360 final AppWindowToken topChild = task.getTopChild(); 361 if (!task.isActivityTypeStandardOrUndefined() && !task.isActivityTypeAssistant()) { 362 return SNAPSHOT_MODE_NONE; 363 } else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) { 364 return SNAPSHOT_MODE_APP_THEME; 365 } else { 366 return SNAPSHOT_MODE_REAL; 367 } 368 } 369 370 /** 371 * If we are not allowed to take a real screenshot, this attempts to represent the app as best 372 * as possible by using the theme's window background. 373 */ drawAppThemeSnapshot(Task task)374 private TaskSnapshot drawAppThemeSnapshot(Task task) { 375 final AppWindowToken topChild = task.getTopChild(); 376 if (topChild == null) { 377 return null; 378 } 379 final WindowState mainWindow = topChild.findMainWindow(); 380 if (mainWindow == null) { 381 return null; 382 } 383 final int color = ColorUtils.setAlphaComponent( 384 task.getTaskDescription().getBackgroundColor(), 255); 385 final LayoutParams attrs = mainWindow.getAttrs(); 386 final SystemBarBackgroundPainter decorPainter = new SystemBarBackgroundPainter(attrs.flags, 387 attrs.privateFlags, attrs.systemUiVisibility, task.getTaskDescription(), 388 mFullSnapshotScale); 389 final int width = (int) (task.getBounds().width() * mFullSnapshotScale); 390 final int height = (int) (task.getBounds().height() * mFullSnapshotScale); 391 392 final RenderNode node = RenderNode.create("TaskSnapshotController", null); 393 node.setLeftTopRightBottom(0, 0, width, height); 394 node.setClipToBounds(false); 395 final RecordingCanvas c = node.start(width, height); 396 c.drawColor(color); 397 decorPainter.setInsets(mainWindow.getContentInsets(), mainWindow.getStableInsets()); 398 decorPainter.drawDecors(c, null /* statusBarExcludeFrame */); 399 node.end(c); 400 final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); 401 if (hwBitmap == null) { 402 return null; 403 } 404 405 // Note, the app theme snapshot is never translucent because we enforce a non-translucent 406 // color above 407 return new TaskSnapshot(topChild.mActivityComponent, hwBitmap.createGraphicBufferHandle(), 408 hwBitmap.getColorSpace(), topChild.getTask().getConfiguration().orientation, 409 getInsets(mainWindow), ActivityManager.isLowRamDeviceStatic() /* reduced */, 410 mFullSnapshotScale, false /* isRealSnapshot */, task.getWindowingMode(), 411 getSystemUiVisibility(task), false); 412 } 413 414 /** 415 * Called when an {@link AppWindowToken} has been removed. 416 */ onAppRemoved(AppWindowToken wtoken)417 void onAppRemoved(AppWindowToken wtoken) { 418 mCache.onAppRemoved(wtoken); 419 } 420 421 /** 422 * Called when the process of an {@link AppWindowToken} has died. 423 */ onAppDied(AppWindowToken wtoken)424 void onAppDied(AppWindowToken wtoken) { 425 mCache.onAppDied(wtoken); 426 } 427 notifyTaskRemovedFromRecents(int taskId, int userId)428 void notifyTaskRemovedFromRecents(int taskId, int userId) { 429 mCache.onTaskRemoved(taskId); 430 mPersister.onTaskRemovedFromRecents(taskId, userId); 431 } 432 433 /** 434 * See {@link TaskSnapshotPersister#removeObsoleteFiles} 435 */ removeObsoleteTaskFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)436 void removeObsoleteTaskFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) { 437 mPersister.removeObsoleteFiles(persistentTaskIds, runningUserIds); 438 } 439 440 /** 441 * Temporarily pauses/unpauses persisting of task snapshots. 442 * 443 * @param paused Whether task snapshot persisting should be paused. 444 */ setPersisterPaused(boolean paused)445 void setPersisterPaused(boolean paused) { 446 mPersister.setPaused(paused); 447 } 448 449 /** 450 * Called when screen is being turned off. 451 */ screenTurningOff(ScreenOffListener listener)452 void screenTurningOff(ScreenOffListener listener) { 453 if (shouldDisableSnapshots()) { 454 listener.onScreenOff(); 455 return; 456 } 457 458 // We can't take a snapshot when screen is off, so take a snapshot now! 459 mHandler.post(() -> { 460 try { 461 synchronized (mService.mGlobalLock) { 462 mTmpTasks.clear(); 463 mService.mRoot.forAllTasks(task -> { 464 if (task.isVisible()) { 465 mTmpTasks.add(task); 466 } 467 }); 468 snapshotTasks(mTmpTasks); 469 } 470 } finally { 471 listener.onScreenOff(); 472 } 473 }); 474 } 475 476 /** 477 * @return The SystemUI visibility flags for the top fullscreen window in the given 478 * {@param task}. 479 */ getSystemUiVisibility(Task task)480 private int getSystemUiVisibility(Task task) { 481 final AppWindowToken topFullscreenToken = task.getTopFullscreenAppToken(); 482 final WindowState topFullscreenWindow = topFullscreenToken != null 483 ? topFullscreenToken.getTopFullscreenWindow() 484 : null; 485 if (topFullscreenWindow != null) { 486 return topFullscreenWindow.getSystemUiVisibility(); 487 } 488 return 0; 489 } 490 dump(PrintWriter pw, String prefix)491 void dump(PrintWriter pw, String prefix) { 492 pw.println(prefix + "mFullSnapshotScale=" + mFullSnapshotScale); 493 mCache.dump(pw, prefix); 494 } 495 } 496