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 + ) + ) + } +}