/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server.wm; import static android.graphics.Color.WHITE; import static android.graphics.Color.alpha; import static android.view.SurfaceControl.HIDDEN; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; import static android.view.WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES; import static android.view.WindowManager.LayoutParams.FLAG_LOCAL_FOCUS_MODE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; import static android.view.WindowManager.LayoutParams.FLAG_SCALED; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TASK_SNAPSHOT; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static com.android.internal.policy.DecorView.NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES; import static com.android.internal.policy.DecorView.STATUS_BAR_COLOR_VIEW_ATTRIBUTES; import static com.android.internal.policy.DecorView.getColorViewLeftInset; import static com.android.internal.policy.DecorView.getColorViewTopInset; import static com.android.internal.policy.DecorView.getNavigationBarRect; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.Nullable; import android.app.ActivityManager.TaskDescription; import android.app.ActivityManager.TaskSnapshot; import android.app.ActivityThread; import android.content.Context; import android.graphics.Canvas; import android.graphics.GraphicBuffer; import android.graphics.Paint; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.util.MergedConfiguration; import android.util.Slog; import android.view.IWindowSession; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowManagerPolicy.StartingSurface; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.DecorView; import com.android.internal.view.BaseIWindow; /** * This class represents a starting window that shows a snapshot. *

* DO NOT HOLD THE WINDOW MANAGER LOCK WHEN CALLING METHODS OF THIS CLASS! */ class TaskSnapshotSurface implements StartingSurface { private static final long SIZE_MISMATCH_MINIMUM_TIME_MS = 450; /** * When creating the starting window, we use the exact same layout flags such that we end up * with a window with the exact same dimensions etc. However, these flags are not used in layout * and might cause other side effects so we exclude them. */ private static final int FLAG_INHERIT_EXCLUDES = FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE | FLAG_NOT_TOUCH_MODAL | FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE | FLAG_HARDWARE_ACCELERATED | FLAG_IGNORE_CHEEK_PRESSES | FLAG_LOCAL_FOCUS_MODE | FLAG_SLIPPERY | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_SPLIT_TOUCH | FLAG_SCALED | FLAG_SECURE; private static final int PRIVATE_FLAG_INHERITS = PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND; private static final String TAG = TAG_WITH_CLASS_NAME ? "SnapshotStartingWindow" : TAG_WM; private static final int MSG_REPORT_DRAW = 0; private static final String TITLE_FORMAT = "SnapshotStartingWindow for taskId=%s"; private final Window mWindow; private final Surface mSurface; private SurfaceControl mChildSurfaceControl; private final IWindowSession mSession; private final WindowManagerService mService; private final Rect mTaskBounds; private final Rect mStableInsets = new Rect(); private final Rect mContentInsets = new Rect(); private final Rect mFrame = new Rect(); private TaskSnapshot mSnapshot; private final CharSequence mTitle; private boolean mHasDrawn; private long mShownTime; private final Handler mHandler; private boolean mSizeMismatch; private final Paint mBackgroundPaint = new Paint(); private final int mStatusBarColor; @VisibleForTesting final SystemBarBackgroundPainter mSystemBarBackgroundPainter; private final int mOrientationOnCreation; static TaskSnapshotSurface create(WindowManagerService service, AppWindowToken token, TaskSnapshot snapshot) { final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); final Window window = new Window(); final IWindowSession session = WindowManagerGlobal.getWindowSession(); window.setSession(session); final Surface surface = new Surface(); final Rect tmpRect = new Rect(); final Rect tmpFrame = new Rect(); final Rect taskBounds; final Rect tmpContentInsets = new Rect(); final Rect tmpStableInsets = new Rect(); final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); int backgroundColor = WHITE; int statusBarColor = 0; int navigationBarColor = 0; final int sysUiVis; final int windowFlags; final int windowPrivateFlags; final int currentOrientation; synchronized (service.mWindowMap) { final WindowState mainWindow = token.findMainWindow(); if (mainWindow == null) { Slog.w(TAG, "TaskSnapshotSurface.create: Failed to find main window for token=" + token); return null; } sysUiVis = mainWindow.getSystemUiVisibility(); windowFlags = mainWindow.getAttrs().flags; windowPrivateFlags = mainWindow.getAttrs().privateFlags; layoutParams.type = TYPE_APPLICATION_STARTING; layoutParams.format = snapshot.getSnapshot().getFormat(); layoutParams.flags = (windowFlags & ~FLAG_INHERIT_EXCLUDES) | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE; layoutParams.privateFlags = PRIVATE_FLAG_TASK_SNAPSHOT | (windowPrivateFlags & PRIVATE_FLAG_INHERITS); layoutParams.token = token.token; layoutParams.width = LayoutParams.MATCH_PARENT; layoutParams.height = LayoutParams.MATCH_PARENT; layoutParams.systemUiVisibility = sysUiVis; final Task task = token.getTask(); if (task != null) { layoutParams.setTitle(String.format(TITLE_FORMAT, task.mTaskId)); final TaskDescription taskDescription = task.getTaskDescription(); if (taskDescription != null) { backgroundColor = taskDescription.getBackgroundColor(); statusBarColor = taskDescription.getStatusBarColor(); navigationBarColor = taskDescription.getNavigationBarColor(); } taskBounds = new Rect(); task.getBounds(taskBounds); } else { taskBounds = null; } currentOrientation = mainWindow.getConfiguration().orientation; } try { final int res = session.addToDisplay(window, window.mSeq, layoutParams, View.VISIBLE, token.getDisplayContent().getDisplayId(), tmpRect, tmpRect, tmpRect, null); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); return null; } } catch (RemoteException e) { // Local call. } final TaskSnapshotSurface snapshotSurface = new TaskSnapshotSurface(service, window, surface, snapshot, layoutParams.getTitle(), backgroundColor, statusBarColor, navigationBarColor, sysUiVis, windowFlags, windowPrivateFlags, taskBounds, currentOrientation); window.setOuter(snapshotSurface); try { session.relayout(window, window.mSeq, layoutParams, -1, -1, View.VISIBLE, 0, tmpFrame, tmpRect, tmpContentInsets, tmpRect, tmpStableInsets, tmpRect, tmpRect, tmpMergedConfiguration, surface); } catch (RemoteException e) { // Local call. } snapshotSurface.setFrames(tmpFrame, tmpContentInsets, tmpStableInsets); snapshotSurface.drawSnapshot(); return snapshotSurface; } @VisibleForTesting TaskSnapshotSurface(WindowManagerService service, Window window, Surface surface, TaskSnapshot snapshot, CharSequence title, int backgroundColor, int statusBarColor, int navigationBarColor, int sysUiVis, int windowFlags, int windowPrivateFlags, Rect taskBounds, int currentOrientation) { mService = service; mHandler = new Handler(mService.mH.getLooper()); mSession = WindowManagerGlobal.getWindowSession(); mWindow = window; mSurface = surface; mSnapshot = snapshot; mTitle = title; mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); mTaskBounds = taskBounds; mSystemBarBackgroundPainter = new SystemBarBackgroundPainter(windowFlags, windowPrivateFlags, sysUiVis, statusBarColor, navigationBarColor); mStatusBarColor = statusBarColor; mOrientationOnCreation = currentOrientation; } @Override public void remove() { synchronized (mService.mWindowMap) { final long now = SystemClock.uptimeMillis(); if (mSizeMismatch && now - mShownTime < SIZE_MISMATCH_MINIMUM_TIME_MS) { mHandler.postAtTime(this::remove, mShownTime + SIZE_MISMATCH_MINIMUM_TIME_MS); if (DEBUG_STARTING_WINDOW) { Slog.v(TAG, "Defer removing snapshot surface in " + (now - mShownTime) + "ms"); } return; } } try { if (DEBUG_STARTING_WINDOW) Slog.v(TAG, "Removing snapshot surface"); mSession.remove(mWindow); } catch (RemoteException e) { // Local call. } } @VisibleForTesting void setFrames(Rect frame, Rect contentInsets, Rect stableInsets) { mFrame.set(frame); mContentInsets.set(contentInsets); mStableInsets.set(stableInsets); mSizeMismatch = (mFrame.width() != mSnapshot.getSnapshot().getWidth() || mFrame.height() != mSnapshot.getSnapshot().getHeight()); mSystemBarBackgroundPainter.setInsets(contentInsets, stableInsets); } private void drawSnapshot() { final GraphicBuffer buffer = mSnapshot.getSnapshot(); if (DEBUG_STARTING_WINDOW) Slog.v(TAG, "Drawing snapshot surface sizeMismatch=" + mSizeMismatch); if (mSizeMismatch) { // The dimensions of the buffer and the window don't match, so attaching the buffer // will fail. Better create a child window with the exact dimensions and fill the parent // window with the background color! drawSizeMismatchSnapshot(buffer); } else { drawSizeMatchSnapshot(buffer); } synchronized (mService.mWindowMap) { mShownTime = SystemClock.uptimeMillis(); mHasDrawn = true; } reportDrawn(); // In case window manager leaks us, make sure we don't retain the snapshot. mSnapshot = null; } private void drawSizeMatchSnapshot(GraphicBuffer buffer) { mSurface.attachAndQueueBuffer(buffer); mSurface.release(); } private void drawSizeMismatchSnapshot(GraphicBuffer buffer) { final SurfaceSession session = new SurfaceSession(mSurface); // Keep a reference to it such that it doesn't get destroyed when finalized. mChildSurfaceControl = new SurfaceControl(session, mTitle + " - task-snapshot-surface", buffer.getWidth(), buffer.getHeight(), buffer.getFormat(), HIDDEN); Surface surface = new Surface(); surface.copyFrom(mChildSurfaceControl); // Clip off ugly navigation bar. final Rect crop = calculateSnapshotCrop(); final Rect frame = calculateSnapshotFrame(crop); SurfaceControl.openTransaction(); try { // We can just show the surface here as it will still be hidden as the parent is // still hidden. mChildSurfaceControl.show(); mChildSurfaceControl.setWindowCrop(crop); mChildSurfaceControl.setPosition(frame.left, frame.top); } finally { SurfaceControl.closeTransaction(); } surface.attachAndQueueBuffer(buffer); surface.release(); final Canvas c = mSurface.lockCanvas(null); drawBackgroundAndBars(c, frame); mSurface.unlockCanvasAndPost(c); mSurface.release(); } @VisibleForTesting Rect calculateSnapshotCrop() { final Rect rect = new Rect(); rect.set(0, 0, mSnapshot.getSnapshot().getWidth(), mSnapshot.getSnapshot().getHeight()); final Rect insets = mSnapshot.getContentInsets(); // Let's remove all system decorations except the status bar, but only if the task is at the // very top of the screen. rect.inset(insets.left, mTaskBounds.top != 0 ? insets.top : 0, insets.right, insets.bottom); return rect; } @VisibleForTesting Rect calculateSnapshotFrame(Rect crop) { final Rect frame = new Rect(crop); // By default, offset it to to top/left corner frame.offsetTo(-crop.left, -crop.top); // However, we also need to make space for the navigation bar on the left side. final int colorViewLeftInset = getColorViewLeftInset(mStableInsets.left, mContentInsets.left); frame.offset(colorViewLeftInset, 0); return frame; } @VisibleForTesting void drawBackgroundAndBars(Canvas c, Rect frame) { final int statusBarHeight = mSystemBarBackgroundPainter.getStatusBarColorViewHeight(); final boolean fillHorizontally = c.getWidth() > frame.right; final boolean fillVertically = c.getHeight() > frame.bottom; if (fillHorizontally) { c.drawRect(frame.right, alpha(mStatusBarColor) == 0xFF ? statusBarHeight : 0, c.getWidth(), fillVertically ? frame.bottom : c.getHeight(), mBackgroundPaint); } if (fillVertically) { c.drawRect(0, frame.bottom, c.getWidth(), c.getHeight(), mBackgroundPaint); } mSystemBarBackgroundPainter.drawDecors(c, frame); } private void reportDrawn() { try { mSession.finishDrawing(mWindow); } catch (RemoteException e) { // Local call. } } private static Handler sHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_REPORT_DRAW: final boolean hasDrawn; final TaskSnapshotSurface surface = (TaskSnapshotSurface) msg.obj; synchronized (surface.mService.mWindowMap) { hasDrawn = surface.mHasDrawn; } if (hasDrawn) { surface.reportDrawn(); } break; } } }; @VisibleForTesting static class Window extends BaseIWindow { private TaskSnapshotSurface mOuter; public void setOuter(TaskSnapshotSurface outer) { mOuter = outer; } @Override public void resized(Rect frame, Rect overscanInsets, Rect contentInsets, Rect visibleInsets, Rect stableInsets, Rect outsets, boolean reportDraw, MergedConfiguration mergedConfiguration, Rect backDropFrame, boolean forceLayout, boolean alwaysConsumeNavBar, int displayId) { if (mergedConfiguration != null && mOuter != null && mOuter.mOrientationOnCreation != mergedConfiguration.getMergedConfiguration().orientation) { // The orientation of the screen is changing. We better remove the snapshot ASAP as // we are going to wait on the new window in any case to unfreeze the screen, and // the starting window is not needed anymore. sHandler.post(mOuter::remove); } if (reportDraw) { sHandler.obtainMessage(MSG_REPORT_DRAW, mOuter).sendToTarget(); } } } /** * Helper class to draw the background of the system bars in regions the task snapshot isn't * filling the window. */ static class SystemBarBackgroundPainter { private final Rect mContentInsets = new Rect(); private final Rect mStableInsets = new Rect(); private final Paint mStatusBarPaint = new Paint(); private final Paint mNavigationBarPaint = new Paint(); private final int mStatusBarColor; private final int mNavigationBarColor; private final int mWindowFlags; private final int mWindowPrivateFlags; private final int mSysUiVis; SystemBarBackgroundPainter( int windowFlags, int windowPrivateFlags, int sysUiVis, int statusBarColor, int navigationBarColor) { mWindowFlags = windowFlags; mWindowPrivateFlags = windowPrivateFlags; mSysUiVis = sysUiVis; final Context context = ActivityThread.currentActivityThread().getSystemUiContext(); mStatusBarColor = DecorView.calculateStatusBarColor(windowFlags, context.getColor(R.color.system_bar_background_semi_transparent), statusBarColor); mNavigationBarColor = navigationBarColor; mStatusBarPaint.setColor(mStatusBarColor); mNavigationBarPaint.setColor(navigationBarColor); } void setInsets(Rect contentInsets, Rect stableInsets) { mContentInsets.set(contentInsets); mStableInsets.set(stableInsets); } int getStatusBarColorViewHeight() { final boolean forceStatusBarBackground = (mWindowPrivateFlags & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND) != 0; if (STATUS_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( mSysUiVis, mStatusBarColor, mWindowFlags, forceStatusBarBackground)) { return getColorViewTopInset(mStableInsets.top, mContentInsets.top); } else { return 0; } } private boolean isNavigationBarColorViewVisible() { return NAVIGATION_BAR_COLOR_VIEW_ATTRIBUTES.isVisible( mSysUiVis, mNavigationBarColor, mWindowFlags, false /* force */); } void drawDecors(Canvas c, @Nullable Rect alreadyDrawnFrame) { drawStatusBarBackground(c, alreadyDrawnFrame, getStatusBarColorViewHeight()); drawNavigationBarBackground(c); } @VisibleForTesting void drawStatusBarBackground(Canvas c, @Nullable Rect alreadyDrawnFrame, int statusBarHeight) { if (statusBarHeight > 0 && (alreadyDrawnFrame == null || c.getWidth() > alreadyDrawnFrame.right)) { final int rightInset = DecorView.getColorViewRightInset(mStableInsets.right, mContentInsets.right); final int left = alreadyDrawnFrame != null ? alreadyDrawnFrame.right : 0; c.drawRect(left, 0, c.getWidth() - rightInset, statusBarHeight, mStatusBarPaint); } } @VisibleForTesting void drawNavigationBarBackground(Canvas c) { final Rect navigationBarRect = new Rect(); getNavigationBarRect(c.getWidth(), c.getHeight(), mStableInsets, mContentInsets, navigationBarRect); final boolean visible = isNavigationBarColorViewVisible(); if (visible && !navigationBarRect.isEmpty()) { c.drawRect(navigationBarRect, mNavigationBarPaint); } } } }