diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index c29a1daf11ff6..eb5fc92e5ccc2 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1422,7 +1422,7 @@ public interface WindowManager extends ViewManager { * this window is visible. * @hide */ - @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) + @RequiresPermission(permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int PRIVATE_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 0x00080000; /** @@ -1442,6 +1442,15 @@ public interface WindowManager extends ViewManager { @RequiresPermission(permission.DEVICE_POWER) public static final int PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN = 0x00200000; + /** + * Flag to indicate that this window should be considered a screen decoration similar to the + * nav bar and status bar. This will cause this window to affect the window insets reported + * to other windows when it is visible. + * @hide + */ + @RequiresPermission(permission.STATUS_BAR_SERVICE) + public static final int PRIVATE_FLAG_IS_SCREEN_DECOR = 0x00400000; + /** * Control flags that are private to the platform. * @hide @@ -1526,7 +1535,11 @@ public interface WindowManager extends ViewManager { @ViewDebug.FlagToString( mask = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, equals = PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN, - name = "ACQUIRES_SLEEP_TOKEN") + name = "ACQUIRES_SLEEP_TOKEN"), + @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IS_SCREEN_DECOR, + equals = PRIVATE_FLAG_IS_SCREEN_DECOR, + name = "IS_SCREEN_DECOR") }) @TestApi public int privateFlags; diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 137e551dbc9e4..ebe3633de402c 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -758,7 +758,8 @@ public interface WindowManagerPolicy { * @param attrs The window layout parameters to be modified. These values * are modified in-place. */ - public void adjustWindowParamsLw(WindowManager.LayoutParams attrs); + public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs, + boolean hasStatusBarServicePermission); /** * After the window manager has computed the current configuration based @@ -1172,13 +1173,13 @@ public interface WindowManagerPolicy { /** * Called when layout of the windows is about to start. * - * @param isDefaultDisplay true if window is on {@link Display#DEFAULT_DISPLAY}. + * @param displayId Id of the display we are doing layout on. * @param displayWidth The current full width of the screen. * @param displayHeight The current full height of the screen. * @param displayRotation The current rotation being applied to the base window. * @param uiMode The current uiMode in configuration. */ - public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight, + public void beginLayoutLw(int displayId, int displayWidth, int displayHeight, int displayRotation, int uiMode); /** diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index fd186ce68a90a..45f645bbed5b3 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -66,6 +66,7 @@ import static android.view.WindowManager.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_STATUS_BAR_VISIBLE_TRANSPARENT; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR; @@ -198,6 +199,7 @@ import android.service.dreams.IDreamManager; import android.service.vr.IPersistentVrStateCallbacks; import android.speech.RecognizerIntent; import android.telecom.TelecomManager; +import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -456,6 +458,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private AccessibilityShortcutController mAccessibilityShortcutController; boolean mSafeMode; + private final ArraySet mScreenDecorWindows = new ArraySet<>(); WindowState mStatusBar = null; int mStatusBarHeight; WindowState mNavigationBar = null; @@ -2613,7 +2616,19 @@ public class PhoneWindowManager implements WindowManagerPolicy { } @Override - public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { + public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs, + boolean hasStatusBarServicePermission) { + + final boolean isScreenDecor = (attrs.privateFlags & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0; + if (mScreenDecorWindows.contains(win)) { + if (!isScreenDecor) { + // No longer has the flag set, so remove from the set. + mScreenDecorWindows.remove(win); + } + } else if (isScreenDecor && hasStatusBarServicePermission) { + mScreenDecorWindows.add(win); + } + switch (attrs.type) { case TYPE_SYSTEM_OVERLAY: case TYPE_SECURE_SYSTEM_OVERLAY: @@ -3074,6 +3089,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { */ @Override public int prepareAddWindowLw(WindowState win, WindowManager.LayoutParams attrs) { + + if ((attrs.privateFlags & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.STATUS_BAR_SERVICE, + "PhoneWindowManager"); + mScreenDecorWindows.add(win); + } + switch (attrs.type) { case TYPE_STATUS_BAR: mContext.enforceCallingOrSelfPermission( @@ -3125,6 +3148,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mNavigationBar = null; mNavigationBarController.setWindow(null); } + mScreenDecorWindows.remove(win); } static final boolean PRINT_ANIM = false; @@ -4395,8 +4419,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** {@inheritDoc} */ @Override - public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight, - int displayRotation, int uiMode) { + public void beginLayoutLw(int displayId, int displayWidth, int displayHeight, + int displayRotation, int uiMode) { + final boolean isDefaultDisplay = displayId == DEFAULT_DISPLAY; mDisplayRotation = displayRotation; final int overscanLeft, overscanTop, overscanRight, overscanBottom; if (isDefaultDisplay) { @@ -4524,6 +4549,71 @@ public class PhoneWindowManager implements WindowManagerPolicy { updateSystemUiVisibilityLw(); } } + layoutScreenDecorWindows(displayId, displayWidth, displayHeight, pf, df, dcf); + } + + private void layoutScreenDecorWindows(int displayId, int displayWidth, int displayHeight, + Rect pf, Rect df, Rect dcf) { + if (mScreenDecorWindows.isEmpty()) { + return; + } + + for (int i = mScreenDecorWindows.size() - 1; i >= 0; --i) { + final WindowState w = mScreenDecorWindows.valueAt(i); + if (w.getDisplayId() != displayId || !w.isVisibleLw()) { + // Skip if not on the same display or not visible. + continue; + } + + w.computeFrameLw(pf /* parentFrame */, df /* displayFrame */, df /* overlayFrame */, + df /* contentFrame */, df /* visibleFrame */, dcf /* decorFrame */, + df /* stableFrame */, df /* outsetFrame */); + final Rect frame = w.getFrameLw(); + + if (frame.left <= 0 && frame.top <= 0) { + // Docked at left or top. + if (frame.bottom >= displayHeight) { + // Docked left. + mDockLeft = Math.max(frame.right, mDockLeft); + } else if (frame.right >= displayWidth ) { + // Docked top. + mDockTop = Math.max(frame.bottom, mDockTop); + } else { + Slog.w(TAG, "layoutScreenDecorWindows: Ignoring decor win=" + w + + " not docked on left or top of display. frame=" + frame + + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight); + } + } else if (frame.right >= displayWidth && frame.bottom >= displayHeight) { + // Docked at right or bottom. + if (frame.top <= 0) { + // Docked right. + mDockRight = Math.min(frame.left, mDockRight); + } else if (frame.left <= 0) { + // Docked bottom. + mDockBottom = Math.min(frame.top, mDockBottom); + } else { + Slog.w(TAG, "layoutScreenDecorWindows: Ignoring decor win=" + w + + " not docked on right or bottom" + " of display. frame=" + frame + + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight); + } + } else { + // Screen decor windows are required to be docked on one of the sides of the screen. + Slog.w(TAG, "layoutScreenDecorWindows: Ignoring decor win=" + w + + " not docked on one of the sides of the display. frame=" + frame + + " displayWidth=" + displayWidth + " displayHeight=" + displayHeight); + } + } + + mContentTop = mSystemTop = mVoiceContentTop = mCurTop = mRestrictedScreenTop = mDockTop; + mContentLeft = mSystemLeft = mVoiceContentLeft = mCurLeft = mRestrictedScreenLeft + = mRestrictedOverscanScreenLeft = mDockLeft; + mContentBottom = mSystemBottom = mVoiceContentBottom = mCurBottom = mDockBottom; + mContentRight = mSystemRight = mVoiceContentRight = mCurRight = mDockRight; + + mRestrictedScreenWidth = mDockRight - mRestrictedScreenLeft; + mRestrictedScreenHeight = mDockBottom - mRestrictedScreenTop; + mRestrictedOverscanScreenWidth = mDockRight - mRestrictedOverscanScreenLeft; + mRestrictedOverscanScreenHeight = mDockBottom - mRestrictedOverscanScreenTop; } private boolean layoutStatusBar(Rect pf, Rect df, Rect of, Rect vf, Rect dcf, int sysui, @@ -4823,9 +4913,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** {@inheritDoc} */ @Override public void layoutWindowLw(WindowState win, WindowState attached) { - // We've already done the navigation bar and status bar. If the status bar can receive - // input, we need to layout it again to accomodate for the IME window. - if ((win == mStatusBar && !canReceiveInput(win)) || win == mNavigationBar) { + // We've already done the navigation bar, status bar, and all screen decor windows. If the + // status bar can receive input, we need to layout it again to accommodate for the IME + // window. + if ((win == mStatusBar && !canReceiveInput(win)) || win == mNavigationBar + || mScreenDecorWindows.contains(win)) { return; } final WindowManager.LayoutParams attrs = win.getAttrs(); @@ -4864,6 +4956,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (!isDefaultDisplay) { + // TODO: Need to fix this and above to take into account decor windows. if (attached != null) { // If this window is attached to another, our display // frame is the same as the one we are attached to. diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 2f54e0e18b8b0..67d62e103f178 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2877,8 +2877,7 @@ class DisplayContent extends WindowContainer + + + + diff --git a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java new file mode 100644 index 0000000000000..209fdf1605a66 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2017 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.BLUE; +import static android.graphics.Color.RED; +import static android.view.Gravity.BOTTOM; +import static android.view.Gravity.LEFT; +import static android.view.Gravity.RIGHT; +import static android.view.Gravity.TOP; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR; +import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; +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_WATCH_OUTSIDE_TOUCH; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IS_SCREEN_DECOR; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static org.junit.Assert.assertEquals; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.Point; +import android.os.Handler; +import android.platform.test.annotations.Presubmit; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.TextView; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +/** + * Tests for the {@link android.view.WindowManager.LayoutParams#PRIVATE_FLAG_IS_SCREEN_DECOR} flag. + * + * Build/Install/Run: + * bit FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests + */ +// TODO: Add test for FLAG_FULLSCREEN which hides the status bar and also other flags. +// TODO: Test non-Activity windows. +// TODO: Test secondary display. +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class ScreenDecorWindowTests { + + private final Context mContext = InstrumentationRegistry.getTargetContext(); + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + + private WindowManager mWm; + private ArrayList mWindows = new ArrayList<>(); + + @Rule + public ActivityTestRule mTestActivityRule = new ActivityTestRule<>( + TestActivity.class, false /* initialTouchMode */, false /* launchActivity */); + private Activity mTestActivity; + + private int mDecorThickness; + private int mHalfDecorThickness; + + @Before + public void setUp() { + mWm = mContext.getSystemService(WindowManager.class); + final Point size = new Point(); + mWm.getDefaultDisplay().getSize(size); + mDecorThickness = Math.min(size.x, size.y) / 3; + mHalfDecorThickness = mDecorThickness / 2; + mTestActivity = launchActivity(mTestActivityRule); + } + + @After + public void tearDown() { + while (!mWindows.isEmpty()) { + removeWindow(mWindows.get(0)); + } + finishActivity(mTestActivityRule); + } + + @Test + public void testScreenSides() throws Exception { + // Decor on top + final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness); + WindowInsets insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetTop(), mDecorThickness); + + // Decor at the bottom + updateWindow(decorWindow, BOTTOM, MATCH_PARENT, mDecorThickness, 0, 0); + insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetBottom(), mDecorThickness); + + // Decor to the left + updateWindow(decorWindow, LEFT, mDecorThickness, MATCH_PARENT, 0, 0); + insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetLeft(), mDecorThickness); + + // Decor to the right + updateWindow(decorWindow, RIGHT, mDecorThickness, MATCH_PARENT, 0, 0); + insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetRight(), mDecorThickness); + } + + @Test + public void testMultipleDecors() throws Exception { + // Test 2 decor windows on-top. + createDecorWindow(TOP, MATCH_PARENT, mHalfDecorThickness); + WindowInsets insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetTop(), mHalfDecorThickness); + createDecorWindow(TOP, MATCH_PARENT, mDecorThickness); + insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetTop(), mDecorThickness); + + // And one at the bottom. + createDecorWindow(BOTTOM, MATCH_PARENT, mHalfDecorThickness); + insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetTop(), mDecorThickness); + assertGreaterOrEqual(insets.getSystemWindowInsetBottom(), mHalfDecorThickness); + } + + @Test + public void testFlagChange() throws Exception { + WindowInsets initialInsets = getInsets(mTestActivity); + + final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness); + WindowInsets insets = getInsets(mTestActivity); + assertEquals(mDecorThickness, insets.getSystemWindowInsetTop()); + + updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness, + 0, PRIVATE_FLAG_IS_SCREEN_DECOR); + insets = getInsets(mTestActivity); + assertEquals(initialInsets.getSystemWindowInsetTop(), insets.getSystemWindowInsetTop()); + + updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness, + PRIVATE_FLAG_IS_SCREEN_DECOR, PRIVATE_FLAG_IS_SCREEN_DECOR); + insets = getInsets(mTestActivity); + assertEquals(mDecorThickness, insets.getSystemWindowInsetTop()); + } + + @Test + public void testRemoval() throws Exception { + WindowInsets initialInsets = getInsets(mTestActivity); + + final View decorWindow = createDecorWindow(TOP, MATCH_PARENT, mDecorThickness); + WindowInsets insets = getInsets(mTestActivity); + assertGreaterOrEqual(insets.getSystemWindowInsetTop(), mDecorThickness); + + removeWindow(decorWindow); + insets = getInsets(mTestActivity); + assertEquals(initialInsets.getSystemWindowInsetTop(), insets.getSystemWindowInsetTop()); + } + + private View createAppWindow() { + return createWindow("appWindow", TOP, MATCH_PARENT, MATCH_PARENT, BLUE, + FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, 0); + } + + private View createDecorWindow(int gravity, int width, int height) { + return createWindow("decorWindow", gravity, width, height, RED, + FLAG_LAYOUT_IN_SCREEN, PRIVATE_FLAG_IS_SCREEN_DECOR); + } + + private View createWindow(String name, int gravity, int width, int height, int color, int flags, + int privateFlags) { + + final View[] viewHolder = new View[1]; + final int finalFlag = flags + | FLAG_NOT_FOCUSABLE | FLAG_WATCH_OUTSIDE_TOUCH | FLAG_NOT_TOUCHABLE; + + // Needs to run on the UI thread. + Handler.getMain().runWithScissors(() -> { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + width, height, TYPE_APPLICATION_OVERLAY, finalFlag, PixelFormat.OPAQUE); + lp.gravity = gravity; + lp.privateFlags |= privateFlags; + + final TextView view = new TextView(mContext); + view.setText("ScreenDecorWindowTests - " + name); + view.setBackgroundColor(color); + mWm.addView(view, lp); + mWindows.add(view); + viewHolder[0] = view; + }, 0); + + waitForIdle(); + return viewHolder[0]; + } + + private void updateWindow(View v, int gravity, int width, int height, + int privateFlags, int privateFlagsMask) { + // Needs to run on the UI thread. + Handler.getMain().runWithScissors(() -> { + final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) v.getLayoutParams(); + lp.gravity = gravity; + lp.width = width; + lp.height = height; + setPrivateFlags(lp, privateFlags, privateFlagsMask); + + mWm.updateViewLayout(v, lp); + }, 0); + + waitForIdle(); + } + + private void removeWindow(View v) { + Handler.getMain().runWithScissors(() -> mWm.removeView(v), 0); + mWindows.remove(v); + waitForIdle(); + } + + private WindowInsets getInsets(View v) { + return new WindowInsets(v.getRootWindowInsets()); + } + + private WindowInsets getInsets(Activity a) { + return new WindowInsets(a.getWindow().getDecorView().getRootWindowInsets()); + } + + /** + * Set the flags of the window, as per the + * {@link WindowManager.LayoutParams WindowManager.LayoutParams} + * flags. + * + * @param flags The new window flags (see WindowManager.LayoutParams). + * @param mask Which of the window flag bits to modify. + */ + public void setPrivateFlags(WindowManager.LayoutParams lp, int flags, int mask) { + lp.flags = (lp.flags & ~mask) | (flags & mask); + } + + /** Asserts that the first entry is greater than or equal to the second entry. */ + private void assertGreaterOrEqual(int first, int second) throws Exception { + Assert.assertTrue("Excepted " + first + " >= " + second, first >= second); + } + + private Activity launchActivity(ActivityTestRule activityRule) { + final Activity activity = activityRule.launchActivity(null); + waitForIdle(); + return activity; + } + + private void finishActivity(ActivityTestRule activityRule) { + final Activity activity = activityRule.getActivity(); + if (activity != null) { + activity.finish(); + } + } + + private void waitForIdle() { + mInstrumentation.waitForIdleSync(); + } + + public static class TestActivity extends Activity { + } +} diff --git a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java index 9f57f4993ed59..33d4721d5f66c 100644 --- a/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java +++ b/services/tests/servicestests/src/com/android/server/wm/TestWindowManagerPolicy.java @@ -148,8 +148,8 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override - public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) { - + public void adjustWindowParamsLw(WindowState win, WindowManager.LayoutParams attrs, + boolean hasStatusBarServicePermission) { } @Override @@ -290,7 +290,7 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override - public void beginLayoutLw(boolean isDefaultDisplay, int displayWidth, int displayHeight, + public void beginLayoutLw(int displayId, int displayWidth, int displayHeight, int displayRotation, int uiMode) { }