Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b814bbd2 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "[2/n] Cascade windows in desktop windowing" into main

parents 3759b94d 31668bc3
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ enum class DesktopModeFlags(
) {
  // All desktop mode related flags will be added here
  DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true),
  CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
  WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity, true),
  MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
  THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
+9 −13
Original line number Diff line number Diff line
@@ -52,8 +52,10 @@ fun calculateInitialBounds(
    val idealSize = calculateIdealSize(screenBounds, scale)
    // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
    // Instead default to the desired initial bounds.
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)
    val topActivityInfo =
        taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds)
        taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)

    val initialSize: Size =
        when (taskInfo.configuration.orientation) {
@@ -100,7 +102,7 @@ fun calculateInitialBounds(
            }
        }

    return positionInScreen(initialSize, screenBounds)
    return positionInScreen(initialSize, stableBounds)
}

/**
@@ -163,16 +165,10 @@ private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
}

/** Adjusts bounds to be positioned in the middle of the screen. */
private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
    // TODO(b/325240051): Position apps with bottom heavy offset
    val heightOffset = (screenBounds.height() - desiredSize.height) / 2
    val widthOffset = (screenBounds.width() - desiredSize.width) / 2
    return Rect(
        widthOffset,
        heightOffset,
        desiredSize.width + widthOffset,
        desiredSize.height + heightOffset
    )
private fun positionInScreen(desiredSize: Size, stableBounds: Rect): Rect =
    Rect(0, 0, desiredSize.width, desiredSize.height).apply {
        val offset = DesktopTaskPosition.Center.getTopLeftCoordinates(stableBounds, this)
        offsetTo(offset.x, offset.y)
    }

/**
+130 −0
Original line number Diff line number Diff line
/*
 * 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.TaskInfo
import android.graphics.Point
import android.graphics.Rect
import android.view.Gravity
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomLeft
import com.android.wm.shell.desktopmode.DesktopTaskPosition.BottomRight
import com.android.wm.shell.desktopmode.DesktopTaskPosition.Center
import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopLeft
import com.android.wm.shell.desktopmode.DesktopTaskPosition.TopRight

/**
 * The position of a task window in desktop mode.
 */
sealed class DesktopTaskPosition {
    data object Center : DesktopTaskPosition() {
        private const val WINDOW_HEIGHT_PROPORTION = 0.375

        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
            val x = (frame.width() - window.width()) / 2
            // Position with more margin at the bottom.
            val y = (frame.height() - window.height()) * WINDOW_HEIGHT_PROPORTION + frame.top
            return Point(x, y.toInt())
        }

        override fun next(): DesktopTaskPosition {
            return BottomRight
        }
    }

    data object BottomRight : DesktopTaskPosition() {
        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
            return Point(frame.right - window.width(), frame.bottom - window.height())
        }

        override fun next(): DesktopTaskPosition {
            return TopLeft
        }
    }

    data object TopLeft : DesktopTaskPosition() {
        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
            return Point(frame.left, frame.top)
        }

        override fun next(): DesktopTaskPosition {
            return BottomLeft
        }
    }

    data object BottomLeft : DesktopTaskPosition() {
        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
            return Point(frame.left, frame.bottom - window.height())
        }

        override fun next(): DesktopTaskPosition {
            return TopRight
        }
    }

    data object TopRight : DesktopTaskPosition() {
        override fun getTopLeftCoordinates(frame: Rect, window: Rect): Point {
            return Point(frame.right - window.width(), frame.top)
        }

        override fun next(): DesktopTaskPosition {
            return Center
        }
    }

    /**
     * Returns the top left coordinates for the window to be placed in the given
     * DesktopTaskPosition in the frame.
     */
    abstract fun getTopLeftCoordinates(frame: Rect, window: Rect): Point

    abstract fun next(): DesktopTaskPosition
}

/**
 * If the app has specified horizontal or vertical gravity layout, don't change the
 * task position for cascading effect.
 */
fun canChangeTaskPosition(taskInfo: TaskInfo): Boolean {
    taskInfo.topActivityInfo?.windowLayout?.let {
        val horizontalGravityApplied = it.gravity.and(Gravity.HORIZONTAL_GRAVITY_MASK)
        val verticalGravityApplied = it.gravity.and(Gravity.VERTICAL_GRAVITY_MASK)
        return horizontalGravityApplied == 0 && verticalGravityApplied == 0
    }
    return true
}

/**
 * Returns the current DesktopTaskPosition for a given window in the frame.
 */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition {
    return when {
        top == bounds.top && left == bounds.left -> TopLeft
        top == bounds.top && right == bounds.right -> TopRight
        bottom == bounds.bottom && left == bounds.left -> BottomLeft
        bottom == bounds.bottom && right == bounds.right -> BottomRight
        else -> Center
    }
}

internal fun cascadeWindow(frame: Rect, prev: Rect, dest: Rect) {
    val lastPos = frame.getDesktopTaskPosition(prev)
    val candidatePos = lastPos.next()
    val destCoord = candidatePos.getTopLeftCoordinates(frame, dest)
    dest.offsetTo(destCoord.x, destCoord.y)
}
+27 −10
Original line number Diff line number Diff line
@@ -403,7 +403,7 @@ class DesktopTasksController(
     * The second part of the animated drag to desktop transition, called after
     * [startDragToDesktop].
     */
    private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
    private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
        ProtoLog.v(
            WM_SHELL_DESKTOP_MODE,
            "DesktopTasksController: finalizeDragToDesktop taskId=%d",
@@ -415,7 +415,6 @@ class DesktopTasksController(
        val taskToMinimize =
            bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
        addMoveToDesktopChanges(wct, taskInfo)
        wct.setBounds(taskInfo.token, freeformBounds)
        val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
        transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
    }
@@ -1071,10 +1070,12 @@ class DesktopTasksController(
        return if (wct.isEmpty) null else wct
    }

    private fun addMoveToDesktopChanges(
    @VisibleForTesting
    fun addMoveToDesktopChanges(
        wct: WindowContainerTransaction,
        taskInfo: RunningTaskInfo
    ) {
        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
        val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
        val targetWindowingMode =
@@ -1084,6 +1085,28 @@ class DesktopTasksController(
            } else {
                WINDOWING_MODE_FREEFORM
            }
        val initialBounds = if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
            calculateInitialBounds(displayLayout, taskInfo)
        } else {
            getDefaultDesktopTaskBounds(displayLayout)
        }

        if (DesktopModeFlags.CASCADING_WINDOWS.isEnabled(context)) {
            val stableBounds = Rect()
            displayLayout.getStableBoundsForDesktopMode(stableBounds)

            val activeTasks = desktopModeTaskRepository
                .getActiveNonMinimizedOrderedTasks(taskInfo.displayId)
            activeTasks.firstOrNull()?.let { activeTask ->
                shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let {
                    cascadeWindow(stableBounds,
                        it.configuration.windowConfiguration.bounds, initialBounds)
                }
            }
        }
        if (canChangeTaskPosition(taskInfo)) {
            wct.setBounds(taskInfo.token, initialBounds)
        }
        wct.setWindowingMode(taskInfo.token, targetWindowingMode)
        wct.reorder(taskInfo.token, true /* onTop */)
        if (useDesktopOverrideDensity()) {
@@ -1343,16 +1366,10 @@ class DesktopTasksController(
        val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
        when (indicatorType) {
            IndicatorType.TO_DESKTOP_INDICATOR -> {
                val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
                    ?: return IndicatorType.NO_INDICATOR
                // Start a new jank interaction for the drag release to desktop window animation.
                interactionJankMonitor.begin(taskSurface, context,
                    CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE, "to_desktop")
                if (DesktopModeFlags.DYNAMIC_INITIAL_BOUNDS.isEnabled(context)) {
                    finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
                } else {
                    finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
                }
                finalizeDragToDesktop(taskInfo)
            }
            IndicatorType.NO_INDICATOR,
            IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+168 −7
Original line number Diff line number Diff line
@@ -43,6 +43,7 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.Gravity
import android.view.SurfaceControl
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
@@ -185,12 +186,12 @@ class DesktopTasksControllerTest : ShellTestCase() {

  private val DISPLAY_DIMENSION_SHORT = 1600
  private val DISPLAY_DIMENSION_LONG = 2560
  private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
  private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
  private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
  private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
  private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
  private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
  private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275)
  private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085)
  private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635)
  private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275)
  private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611)
  private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275)

  @Before
  fun setUp() {
@@ -589,6 +590,140 @@ class DesktopTasksControllerTest : ShellTestCase() {
    assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
  }

  @Test
  fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() {
    setUpLandscapeDisplay()
    val task = setUpFullscreenTask(gravity = Gravity.LEFT)
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(finalBounds).isEqualTo(Rect())
  }

  @Test
  fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() {
    setUpLandscapeDisplay()
    val task = setUpFullscreenTask(gravity = Gravity.RIGHT)
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(finalBounds).isEqualTo(Rect())
  }

  @Test
  fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() {
    setUpLandscapeDisplay()
    val task = setUpFullscreenTask(gravity = Gravity.TOP)
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(finalBounds).isEqualTo(Rect())
  }

  @Test
  fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() {
    setUpLandscapeDisplay()
    val task = setUpFullscreenTask(gravity = Gravity.BOTTOM)
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(finalBounds).isEqualTo(Rect())
  }

  @Test
  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
  fun addMoveToDesktopChanges_positionBottomRight() {
    setUpLandscapeDisplay()
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)

    setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS)

    val task = setUpFullscreenTask()
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
      .isEqualTo(DesktopTaskPosition.BottomRight)
  }

  @Test
  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
  fun addMoveToDesktopChanges_positionTopLeft() {
    setUpLandscapeDisplay()
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)

    addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds)

    val task = setUpFullscreenTask()
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
      .isEqualTo(DesktopTaskPosition.TopLeft)
  }

  @Test
  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
  fun addMoveToDesktopChanges_positionBottomLeft() {
    setUpLandscapeDisplay()
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)

    addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds)

    val task = setUpFullscreenTask()
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
      .isEqualTo(DesktopTaskPosition.BottomLeft)
  }

  @Test
  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
  fun addMoveToDesktopChanges_positionTopRight() {
    setUpLandscapeDisplay()
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)

    addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds)

    val task = setUpFullscreenTask()
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
      .isEqualTo(DesktopTaskPosition.TopRight)
  }

  @Test
  @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
  fun addMoveToDesktopChanges_positionResetsToCenter() {
    setUpLandscapeDisplay()
    val stableBounds = Rect()
    displayLayout.getStableBoundsForDesktopMode(stableBounds)

    addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds)

    val task = setUpFullscreenTask()
    val wct = WindowContainerTransaction()
    controller.addMoveToDesktopChanges(wct, task)

    val finalBounds = findBoundsChange(wct, task)
    assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
      .isEqualTo(DesktopTaskPosition.Center)
  }

  @Test
  fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
    val task = setUpFullscreenTask()
@@ -2406,6 +2541,17 @@ class DesktopTasksControllerTest : ShellTestCase() {
  private val desktopWallpaperIntent: Intent
    get() = Intent(context, DesktopWallpaperActivity::class.java)

  private fun addFreeformTaskAtPosition(
    pos: DesktopTaskPosition,
    stableBounds: Rect,
    bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS
  ): RunningTaskInfo {
    val offset = pos.getTopLeftCoordinates(stableBounds, bounds)
    val prevTaskBounds = Rect(bounds)
    prevTaskBounds.offsetTo(offset.x, offset.y)
    return setUpFreeformTask(bounds = prevTaskBounds)
  }

  private fun setUpFreeformTask(
      displayId: Int = DEFAULT_DISPLAY,
      bounds: Rect? = null
@@ -2434,11 +2580,13 @@ class DesktopTasksControllerTest : ShellTestCase() {
      windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
      deviceOrientation: Int = ORIENTATION_LANDSCAPE,
      screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
      shouldLetterbox: Boolean = false
      shouldLetterbox: Boolean = false,
      gravity: Int = Gravity.NO_GRAVITY
  ): RunningTaskInfo {
    val task = createFullscreenTask(displayId)
    val activityInfo = ActivityInfo()
    activityInfo.screenOrientation = screenOrientation
    activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0)
    with(task) {
      topActivityInfo = activityInfo
      isResizeable = isResizable
@@ -2479,11 +2627,23 @@ class DesktopTasksControllerTest : ShellTestCase() {
  private fun setUpLandscapeDisplay() {
    whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
    whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG,
      DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT
    )
    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
      (i.arguments.first() as Rect).set(stableBounds)
    }
  }

  private fun setUpPortraitDisplay() {
    whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
    whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
    val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT,
      DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT
    )
    whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
      (i.arguments.first() as Rect).set(stableBounds)
    }
  }

  private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
@@ -2601,6 +2761,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
    const val SECOND_DISPLAY = 2
    val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
    const val MAX_TASK_LIMIT = 6
    private const val TASKBAR_FRAME_HEIGHT = 200
  }
}