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