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