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
17package com.android.server.wm;
18
19import static com.android.server.wm.TaskSnapshotPersister.DISABLE_FULL_SIZED_BITMAPS;
20import static com.android.server.wm.TaskSnapshotPersister.REDUCED_SCALE;
21import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SCREENSHOT;
22import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
23import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
24
25import android.annotation.Nullable;
26import android.app.ActivityManager;
27import android.app.ActivityManager.TaskSnapshot;
28import android.content.pm.PackageManager;
29import android.graphics.Bitmap;
30import android.graphics.GraphicBuffer;
31import android.graphics.PixelFormat;
32import android.graphics.Rect;
33import android.os.Environment;
34import android.os.Handler;
35import android.util.ArraySet;
36import android.util.Slog;
37import android.view.DisplayListCanvas;
38import android.view.RenderNode;
39import android.view.SurfaceControl;
40import android.view.ThreadedRenderer;
41import android.view.View;
42import android.view.WindowManager.LayoutParams;
43
44import com.android.internal.annotations.VisibleForTesting;
45import com.android.internal.graphics.ColorUtils;
46import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
47import com.android.server.policy.WindowManagerPolicy.StartingSurface;
48import com.android.server.wm.TaskSnapshotSurface.SystemBarBackgroundPainter;
49import com.android.server.wm.utils.InsetUtils;
50
51import com.google.android.collect.Sets;
52
53import 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 */
68class 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
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
129    void systemReady() {
130        mPersister.start();
131    }
132
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     */
140    void notifyAppVisibilityChanged(AppWindowToken appWindowToken, boolean visible) {
141        if (!visible) {
142            handleClosingApps(Sets.newArraySet(appWindowToken));
143        }
144    }
145
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
164    void addSkipClosingAppSnapshotTasks(ArraySet<Task> tasks) {
165        mSkipClosingAppSnapshotTasks.addAll(tasks);
166    }
167
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     */
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     */
217    StartingSurface createStartingSurface(AppWindowToken token,
218            TaskSnapshot snapshot) {
219        return TaskSnapshotSurface.create(mService, token, snapshot);
220    }
221
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
283    private boolean shouldDisableSnapshots() {
284        return mIsRunningOnWear || mIsRunningOnTv || mIsRunningOnIoT;
285    }
286
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
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
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
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     */
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     */
379    void onAppRemoved(AppWindowToken wtoken) {
380        mCache.onAppRemoved(wtoken);
381    }
382
383    /**
384     * Called when the process of an {@link AppWindowToken} has died.
385     */
386    void onAppDied(AppWindowToken wtoken) {
387        mCache.onAppDied(wtoken);
388    }
389
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     */
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     */
407    void setPersisterPaused(boolean paused) {
408        mPersister.setPaused(paused);
409    }
410
411    /**
412     * Called when screen is being turned off.
413     */
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     */
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
453    void dump(PrintWriter pw, String prefix) {
454        mCache.dump(pw, prefix);
455    }
456}
457