diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3aa4a77431..1a07565c54 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -490,4 +490,7 @@
36dp
89dp
16dp
+
+
+ 136px
diff --git a/src/com/android/launcher3/util/window/WindowManagerProxy.java b/src/com/android/launcher3/util/window/WindowManagerProxy.java
index 32f17367e7..6a0090c60b 100644
--- a/src/com/android/launcher3/util/window/WindowManagerProxy.java
+++ b/src/com/android/launcher3/util/window/WindowManagerProxy.java
@@ -17,6 +17,7 @@ package com.android.launcher3.util.window;
import static android.view.Display.DEFAULT_DISPLAY;
+import static com.android.launcher3.Utilities.dpToPx;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.testing.shared.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.testing.shared.ResourceUtils.NAVBAR_HEIGHT;
@@ -49,6 +50,9 @@ import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.testing.shared.ResourceUtils;
@@ -130,11 +134,11 @@ public class WindowManagerProxy implements ResourceBasedOverride {
Resources systemRes = context.getResources();
Configuration config = systemRes.getConfiguration();
- boolean isTablet = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
+ boolean isLargeScreen = config.smallestScreenWidthDp > MIN_TABLET_WIDTH;
boolean isGesture = isGestureNav(context);
boolean isPortrait = config.screenHeightDp > config.screenWidthDp;
- int bottomNav = isTablet
+ int bottomNav = isLargeScreen
? 0
: (isPortrait
? getDimenByName(systemRes, NAVBAR_HEIGHT)
@@ -165,6 +169,9 @@ public class WindowManagerProxy implements ResourceBasedOverride {
insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
}
+ applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+ context, isLargeScreen, dpToPx(config.screenWidthDp), oldInsets, insetsBuilder);
+
WindowInsets result = insetsBuilder.build();
Insets systemWindowInsets = result.getInsetsIgnoringVisibility(
WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
@@ -173,6 +180,71 @@ public class WindowManagerProxy implements ResourceBasedOverride {
return result;
}
+ /**
+ * For large screen, when display cutout is at bottom left/right corner of screen, override
+ * display cutout's bottom inset to 0, because launcher allows drawing content over that area.
+ */
+ private static void applyDisplayCutoutBottomInsetOverrideOnLargeScreen(
+ @NonNull Context context,
+ boolean isLargeScreen,
+ int screenWidthPx,
+ @NonNull WindowInsets windowInsets,
+ @NonNull WindowInsets.Builder insetsBuilder) {
+ if (!isLargeScreen || !Utilities.ATLEAST_S) {
+ return;
+ }
+
+ final DisplayCutout displayCutout = windowInsets.getDisplayCutout();
+ if (displayCutout == null) {
+ return;
+ }
+
+ if (!areBottomDisplayCutoutsSmallAndAtCorners(
+ displayCutout.getBoundingRectBottom(), screenWidthPx, context.getResources())) {
+ return;
+ }
+
+ Insets oldDisplayCutoutInset = windowInsets.getInsets(WindowInsets.Type.displayCutout());
+ Insets newDisplayCutoutInset = Insets.of(
+ oldDisplayCutoutInset.left,
+ oldDisplayCutoutInset.top,
+ oldDisplayCutoutInset.right,
+ 0);
+ insetsBuilder.setInsetsIgnoringVisibility(
+ WindowInsets.Type.displayCutout(), newDisplayCutoutInset);
+ }
+
+ /**
+ * @see doc at {@link #areBottomDisplayCutoutsSmallAndAtCorners(Rect, int, int)}
+ */
+ private static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+ @NonNull Rect cutoutRectBottom, int screenWidthPx, @NonNull Resources res) {
+ return areBottomDisplayCutoutsSmallAndAtCorners(cutoutRectBottom, screenWidthPx,
+ res.getDimensionPixelSize(R.dimen.max_width_and_height_of_small_display_cutout));
+ }
+
+ /**
+ * Return true if bottom display cutouts are at bottom left/right corners, AND has width or
+ * height <= maxWidthAndHeightOfSmallCutoutPx. Note that display cutout rect and screenWidthPx
+ * passed to this method should be in the SAME screen rotation.
+ *
+ * @param cutoutRectBottom bottom display cutout rect, this is based on current screen rotation
+ * @param screenWidthPx screen width in px based on current screen rotation
+ * @param maxWidthAndHeightOfSmallCutoutPx maximum width and height pixels of cutout.
+ */
+ @VisibleForTesting
+ static boolean areBottomDisplayCutoutsSmallAndAtCorners(
+ @NonNull Rect cutoutRectBottom, int screenWidthPx,
+ int maxWidthAndHeightOfSmallCutoutPx) {
+ // Empty cutoutRectBottom means there is no display cutout at the bottom. We should ignore
+ // it by returning false.
+ if (cutoutRectBottom.isEmpty()) {
+ return false;
+ }
+ return (cutoutRectBottom.right <= maxWidthAndHeightOfSmallCutoutPx)
+ || cutoutRectBottom.left >= (screenWidthPx - maxWidthAndHeightOfSmallCutoutPx);
+ }
+
protected int getStatusBarHeight(Context context, boolean isPortrait, int statusBarInset) {
Resources systemRes = context.getResources();
int statusBarHeight = getDimenByName(systemRes,
@@ -249,6 +321,12 @@ public class WindowManagerProxy implements ResourceBasedOverride {
DisplayCutout rotatedCutout = rotateCutout(
displayInfo.cutout, displayInfo.size.x, displayInfo.size.y, rotation, i);
Rect insets = getSafeInsets(rotatedCutout);
+ if (areBottomDisplayCutoutsSmallAndAtCorners(
+ rotatedCutout.getBoundingRectBottom(),
+ bounds.width(),
+ context.getResources())) {
+ insets.bottom = 0;
+ }
insets.top = Math.max(insets.top, statusBarHeight);
insets.bottom = Math.max(insets.bottom, navBarHeight);
diff --git a/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
new file mode 100644
index 0000000000..4819388129
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/window/WindowManagerProxyTest.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 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.launcher3.util.window
+
+import android.graphics.Rect
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.window.WindowManagerProxy.areBottomDisplayCutoutsSmallAndAtCorners
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit test for [WindowManagerProxy] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WindowManagerProxyTest {
+
+ private val windowWidthPx = 2000
+
+ private val bottomLeftCutout = Rect(0, 2364, 136, 2500)
+ private val bottomRightCutout = Rect(1864, 2364, 2000, 2500)
+
+ private val bottomLeftCutoutWithOffset = Rect(10, 2364, 136, 2500)
+ private val bottomRightCutoutWithOffset = Rect(1864, 2364, 1990, 2500)
+
+ private val maxWidthAndHeightOfSmallCutoutPx = 136
+
+ @Test
+ fun cutout_at_bottom_right_corner() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomRightCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_left_corner_with_offset() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutoutWithOffset,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_right_corner_with_offset() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomRightCutoutWithOffset,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_left_corner() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_at_bottom_edge_at_bottom_corners() {
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_too_big_not_at_bottom_corners() {
+ // Rect in size of 200px
+ val bigBottomLeftCutout = Rect(0, 2300, 200, 2500)
+
+ assertFalse(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ bigBottomLeftCutout,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_too_small_at_bottom_corners() {
+ // Rect in size of 100px
+ val smallBottomLeft = Rect(0, 2400, 100, 2500)
+
+ assertTrue(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ smallBottomLeft,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+
+ @Test
+ fun cutout_empty_not_at_bottom_corners() {
+ val emptyRect = Rect(0, 0, 0, 0)
+
+ assertFalse(
+ areBottomDisplayCutoutsSmallAndAtCorners(
+ emptyRect,
+ windowWidthPx,
+ maxWidthAndHeightOfSmallCutoutPx
+ )
+ )
+ }
+}