diff options
5 files changed, 256 insertions, 15 deletions
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f73775becac9..6ebe81dc1b02 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -501,6 +501,17 @@ fullscreen if dragged until the top bound of the task is within the area. --> <dimen name="desktop_mode_transition_area_height">16dp</dimen> + <!-- The width of the area where a desktop task will transition to fullscreen. --> + <dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen> + + <!-- The height of the area where a desktop task will transition to fullscreen. --> + <dimen name="desktop_mode_fullscreen_from_desktop_height">40dp</dimen> + + <!-- The height on the screen where drag to the left or right edge will result in a + desktop task snapping to split size. The empty space between this and the top is to allow + for corner drags without transition. --> + <dimen name="desktop_mode_split_from_desktop_height">100dp</dimen> + <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) --> <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item> <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 6250fc5820aa..405341803a46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -19,6 +19,8 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; @@ -28,11 +30,13 @@ import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.ActivityManager; +import android.app.WindowConfiguration; import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.Region; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -41,6 +45,8 @@ import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.view.animation.DecelerateInterpolator; +import androidx.annotation.VisibleForTesting; + import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; @@ -93,28 +99,114 @@ public class DesktopModeVisualIndicator { /** * Based on the coordinates of the current drag event, determine which indicator type we should * display, including no visible indicator. - * TODO(b/280828642): Update drag zones per starting windowing mode. */ - IndicatorType updateIndicatorType(PointF inputCoordinates) { + IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) { final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId); // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. - IndicatorType result = mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - ? IndicatorType.NO_INDICATOR : IndicatorType.TO_DESKTOP_INDICATOR; - int transitionAreaHeight = mContext.getResources().getDimensionPixelSize( - com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); - int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( + IndicatorType result = IndicatorType.NO_INDICATOR; + final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_transition_area_width); - if (inputCoordinates.y <= transitionAreaHeight) { + // Because drags in freeform use task position for indicator calculation, we need to + // account for the possibility of the task going off the top of the screen by captionHeight + final int captionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_freeform_decor_caption_height); + final Region fullscreenRegion = calculateFullscreenRegion(layout, windowingMode, + captionHeight); + final Region splitLeftRegion = calculateSplitLeftRegion(layout, windowingMode, + transitionAreaWidth, captionHeight); + final Region splitRightRegion = calculateSplitRightRegion(layout, windowingMode, + transitionAreaWidth, captionHeight); + final Region toDesktopRegion = calculateToDesktopRegion(layout, windowingMode, + splitLeftRegion, splitRightRegion, fullscreenRegion); + if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { result = IndicatorType.TO_FULLSCREEN_INDICATOR; - } else if (inputCoordinates.x <= transitionAreaWidth) { + } + if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { result = IndicatorType.TO_SPLIT_LEFT_INDICATOR; - } else if (inputCoordinates.x >= layout.width() - transitionAreaWidth) { + } + if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; } + if (toDesktopRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + result = IndicatorType.TO_DESKTOP_INDICATOR; + } transitionIndicator(result); return result; } + @VisibleForTesting + Region calculateFullscreenRegion(DisplayLayout layout, + @WindowConfiguration.WindowingMode int windowingMode, int captionHeight) { + final Region region = new Region(); + int edgeTransitionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_transition_area_height); + // A thin, short Rect at the top of the screen. + if (windowingMode == WINDOWING_MODE_FREEFORM) { + int fromFreeformWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width); + int fromFreeformHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height); + region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2), + -captionHeight, + (layout.width() / 2) + (fromFreeformWidth / 2), + fromFreeformHeight)); + } + // A screen-wide, shorter Rect if the task is in fullscreen or split. + if (windowingMode == WINDOWING_MODE_FULLSCREEN + || windowingMode == WINDOWING_MODE_MULTI_WINDOW) { + region.union(new Rect(0, + -captionHeight, + layout.width(), + edgeTransitionHeight)); + } + return region; + } + + @VisibleForTesting + Region calculateToDesktopRegion(DisplayLayout layout, + @WindowConfiguration.WindowingMode int windowingMode, + Region splitLeftRegion, Region splitRightRegion, + Region toFullscreenRegion) { + final Region region = new Region(); + // If in desktop, we need no region. Otherwise it's the same for all windowing modes. + if (windowingMode != WINDOWING_MODE_FREEFORM) { + region.union(new Rect(0, 0, layout.width(), layout.height())); + region.op(splitLeftRegion, Region.Op.DIFFERENCE); + region.op(splitRightRegion, Region.Op.DIFFERENCE); + region.op(toFullscreenRegion, Region.Op.DIFFERENCE); + } + return region; + } + + @VisibleForTesting + Region calculateSplitLeftRegion(DisplayLayout layout, + @WindowConfiguration.WindowingMode int windowingMode, + int transitionEdgeWidth, int captionHeight) { + final Region region = new Region(); + // In freeform, keep the top corners clear. + int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM + ? mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : + -captionHeight; + region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height())); + return region; + } + + @VisibleForTesting + Region calculateSplitRightRegion(DisplayLayout layout, + @WindowConfiguration.WindowingMode int windowingMode, + int transitionEdgeWidth, int captionHeight) { + final Region region = new Region(); + // In freeform, keep the top corners clear. + int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM + ? mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) : + -captionHeight; + region.union(new Rect(layout.width() - transitionEdgeWidth, transitionHeight, + layout.width(), layout.height())); + return region; + } + /** * Create a fullscreen indicator with no animation */ @@ -175,7 +267,6 @@ public class DesktopModeVisualIndicator { mDisplayController.getDisplayLayout(mTaskInfo.displayId)); animator.start(); mCurrentType = IndicatorType.NO_INDICATOR; - } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 837cb99602c8..a7477f55b8b0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -893,7 +893,7 @@ class DesktopTasksController( } // Then, update the indicator type. val indicator = visualIndicator ?: return - indicator.updateIndicatorType(PointF(inputX, taskTop)) + indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode) } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 7db3d382ed8e..8e5f51685a2d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -726,7 +726,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTransitionDragActive = false; final int statusBarHeight = getStatusBarHeight( relevantDecor.mTaskInfo.displayId); - if (ev.getY() > 2 * statusBarHeight) { + if (ev.getRawY() > 2 * statusBarHeight) { if (DesktopModeStatus.isEnabled()) { animateToDesktop(relevantDecor, ev); } @@ -751,10 +751,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDesktopTasksController.ifPresent( c -> c.updateVisualIndicator( relevantDecor.mTaskInfo, - relevantDecor.mTaskSurface, ev.getX(), ev.getY())); + relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY())); final int statusBarHeight = getStatusBarHeight( relevantDecor.mTaskInfo.displayId); - if (ev.getY() > statusBarHeight) { + if (ev.getRawY() > statusBarHeight) { if (mMoveToDesktopAnimator == null) { mMoveToDesktopAnimator = new MoveToDesktopAnimator( mContext, mDragToDesktopAnimationStartBounds, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt new file mode 100644 index 000000000000..f8ce4ee8e1ce --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -0,0 +1,139 @@ +/* + * 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.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.graphics.Rect +import android.graphics.Region +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.SyncTransactionQueue +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopModeVisualIndicatorTest : ShellTestCase() { + @Mock private lateinit var taskInfo: RunningTaskInfo + @Mock private lateinit var syncQueue: SyncTransactionQueue + @Mock private lateinit var displayController: DisplayController + @Mock private lateinit var taskSurface: SurfaceControl + @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var displayLayout: DisplayLayout + + private lateinit var visualIndicator: DesktopModeVisualIndicator + + @Before + fun setUp() { + visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController, + context, taskSurface, taskDisplayAreaOrganizer) + whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) + whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) + } + + @Test + fun testFullscreenRegionCalculation() { + val transitionHeight = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_transition_area_height) + val fromFreeformWidth = mContext.resources.getDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_from_desktop_width + ) + val fromFreeformHeight = mContext.resources.getDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_from_desktop_height + ) + var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, + WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect( + DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2, + -50, + DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2, + fromFreeformHeight)) + testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, + WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight)) + } + + @Test + fun testSplitLeftRegionCalculation() { + val transitionHeight = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_split_from_desktop_height) + var testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) + testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, + WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, transitionHeight, 32, 1600)) + testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, + WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) + } + + @Test + fun testSplitRightRegionCalculation() { + val transitionHeight = context.resources.getDimensionPixelSize( + R.dimen.desktop_mode_split_from_desktop_height) + var testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) + testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, + WINDOWING_MODE_FREEFORM, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(2368, transitionHeight, 2400, 1600)) + testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, + WINDOWING_MODE_MULTI_WINDOW, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) + } + + @Test + fun testToDesktopRegionCalculation() { + val fullscreenRegion = visualIndicator.calculateFullscreenRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT) + val splitLeftRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + val splitRightRegion = visualIndicator.calculateSplitRightRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + val desktopRegion = visualIndicator.calculateToDesktopRegion(displayLayout, + WINDOWING_MODE_FULLSCREEN, splitLeftRegion, splitRightRegion, fullscreenRegion) + var testRegion = Region() + testRegion.union(DISPLAY_BOUNDS) + testRegion.op(splitLeftRegion, Region.Op.DIFFERENCE) + testRegion.op(splitRightRegion, Region.Op.DIFFERENCE) + testRegion.op(fullscreenRegion, Region.Op.DIFFERENCE) + assertThat(desktopRegion).isEqualTo(testRegion) + } + + companion object { + private const val TRANSITION_AREA_WIDTH = 32 + private const val CAPTION_HEIGHT = 50 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + } +}
\ No newline at end of file |