diff options
13 files changed, 462 insertions, 22 deletions
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 289c5cf4bf85..be69d3da3874 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -86,7 +86,9 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX( Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, false), ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX( - Flags::enableDesktopAppLaunchTransitionsBugfix, false); + Flags::enableDesktopAppLaunchTransitionsBugfix, false), + INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC( + Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 1707e61b28e4..3b77b1f65dac 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -16,6 +16,17 @@ flag { } flag { + name: "include_top_transparent_fullscreen_task_in_desktop_heuristic" + namespace: "lse_desktop_experience" + description: "Whether to include any top transparent fullscreen task launched in desktop /n" + "mode in the heuristic for if desktop windowing is showing or not." + bug: "379543275" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "enable_windowing_dynamic_initial_bounds" namespace: "lse_desktop_experience" description: "Enables new initial bounds for desktop windowing which adjust depending on app constraints" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt index bc56637b2a1e..d1dcc9b1d591 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt @@ -18,22 +18,29 @@ package com.android.wm.shell.compatui -import android.app.TaskInfo +import android.app.ActivityManager.RunningTaskInfo import android.content.Context import com.android.internal.R // TODO(b/347289970): Consider replacing with API /** * If the top activity should be exempt from desktop windowing and forced back to fullscreen. - * Currently includes all system ui activities and modal dialogs. However is the top activity is not + * Currently includes all system ui activities and modal dialogs. However if the top activity is not * being displayed, regardless of its configuration, we will not exempt it as to remain in the * desktop windowing environment. */ -fun isTopActivityExemptFromDesktopWindowing(context: Context, task: TaskInfo) = - (isSystemUiTask(context, task) || (task.numActivities > 0 && task.isActivityStackTransparent)) +fun isTopActivityExemptFromDesktopWindowing(context: Context, task: RunningTaskInfo) = + (isSystemUiTask(context, task) || isTransparentTask(task)) && !task.isTopActivityNoDisplay -private fun isSystemUiTask(context: Context, task: TaskInfo): Boolean { +/** + * Returns true if all activities in a tasks stack are transparent. If there are no activities will + * return false. + */ +fun isTransparentTask(task: RunningTaskInfo): Boolean = task.isActivityStackTransparent + && task.numActivities > 0 + +private fun isSystemUiTask(context: Context, task: RunningTaskInfo): Boolean { val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi) return task.baseActivity?.packageName == sysUiPackageName diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index c4abee3bed78..c5b570dd3d57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -54,7 +54,9 @@ class DesktopRepository( * @property closingTasks task ids for tasks that are going to close, but are currently visible. * @property freeformTasksInZOrder list of current freeform task ids ordered from top to bottom * @property fullImmersiveTaskId the task id of the desktop task that is in full-immersive mode. - * (top is at index 0). + * @property topTransparentFullscreenTaskId the task id of any current top transparent + * fullscreen task launched on top of Desktop Mode. Cleared when the transparent task is + * closed or sent to back. (top is at index 0). */ private data class DesktopTaskData( val activeTasks: ArraySet<Int> = ArraySet(), @@ -64,6 +66,7 @@ class DesktopRepository( val closingTasks: ArraySet<Int> = ArraySet(), val freeformTasksInZOrder: ArrayList<Int> = ArrayList(), var fullImmersiveTaskId: Int? = null, + var topTransparentFullscreenTaskId: Int? = null, ) { fun deepCopy(): DesktopTaskData = DesktopTaskData( @@ -73,6 +76,7 @@ class DesktopRepository( closingTasks = ArraySet(closingTasks), freeformTasksInZOrder = ArrayList(freeformTasksInZOrder), fullImmersiveTaskId = fullImmersiveTaskId, + topTransparentFullscreenTaskId = topTransparentFullscreenTaskId, ) fun clear() { @@ -82,6 +86,7 @@ class DesktopRepository( closingTasks.clear() freeformTasksInZOrder.clear() fullImmersiveTaskId = null + topTransparentFullscreenTaskId = null } } @@ -322,13 +327,27 @@ class DesktopRepository( fun getTaskInFullImmersiveState(displayId: Int): Int? = desktopTaskDataByDisplayId.getOrCreate(displayId).fullImmersiveTaskId + /** Sets the top transparent fullscreen task id for a given display. */ + fun setTopTransparentFullscreenTaskId(displayId: Int, taskId: Int) { + desktopTaskDataByDisplayId.getOrCreate(displayId).topTransparentFullscreenTaskId = taskId + } + + /** Returns the top transparent fullscreen task id for a given display, or null. */ + fun getTopTransparentFullscreenTaskId(displayId: Int): Int? = + desktopTaskDataByDisplayId.getOrCreate(displayId).topTransparentFullscreenTaskId + + /** Clears the top transparent fullscreen task id info for a given display. */ + fun clearTopTransparentFullscreenTaskId(displayId: Int) { + desktopTaskDataByDisplayId.getOrCreate(displayId).topTransparentFullscreenTaskId = null + } + private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { visibleTasksListeners.forEach { (listener, executor) -> executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } } } - /** Gets number of visible tasks on given [displayId] */ + /** Gets number of visible freeform tasks on given [displayId] */ fun getVisibleTaskCount(displayId: Int): Int = desktopTaskDataByDisplayId[displayId]?.visibleTasks?.size ?: 0.also { logD("getVisibleTaskCount=$it") } @@ -526,6 +545,10 @@ class DesktopRepository( ) pw.println("${innerPrefix}minimizedTasks=${data.minimizedTasks.toDumpString()}") pw.println("${innerPrefix}fullImmersiveTaskId=${data.fullImmersiveTaskId}") + pw.println( + "${innerPrefix}topTransparentFullscreenTaskId=" + + "${data.topTransparentFullscreenTaskId}" + ) pw.println("${innerPrefix}wallpaperActivityToken=$wallpaperActivityToken") } } 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 609ac0aac381..a3d3a90fef3e 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 @@ -21,7 +21,6 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.ActivityOptions import android.app.KeyguardManager import android.app.PendingIntent -import android.app.TaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM @@ -84,6 +83,7 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing +import com.android.wm.shell.compatui.isTransparentTask import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum @@ -308,11 +308,23 @@ class DesktopTasksController( } } - /** Gets number of visible tasks in [displayId]. */ + /** Gets number of visible freeform tasks in [displayId]. */ fun visibleTaskCount(displayId: Int): Int = taskRepository.getVisibleTaskCount(displayId) - /** Returns true if any tasks are visible in Desktop Mode. */ - fun isDesktopModeShowing(displayId: Int): Boolean = visibleTaskCount(displayId) > 0 + /** + * Returns true if any freeform tasks are visible or if a transparent fullscreen task exists on + * top in Desktop Mode. + */ + fun isDesktopModeShowing(displayId: Int): Boolean { + if ( + DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC + .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() + ) { + return visibleTaskCount(displayId) > 0 || + taskRepository.getTopTransparentFullscreenTaskId(displayId) != null + } + return visibleTaskCount(displayId) > 0 + } /** Moves focused task to desktop mode for given [displayId]. */ fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) { @@ -863,7 +875,11 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() - if (!task.isFreeform) addMoveToDesktopChanges(wct, task, displayId) + if (!task.isFreeform) { + addMoveToDesktopChanges(wct, task, displayId) + } else if (Flags.enableMoveToNextDisplayShortcut()) { + applyFreeformDisplayChange(wct, task, displayId) + } wct.reparent(task.token, displayAreaInfo.token, true /* onTop */) if (Flags.enablePerDisplayDesktopWallpaperActivity()) { @@ -1592,7 +1608,7 @@ class DesktopTasksController( TransitionUtil.isOpeningType(request.type) && taskRepository.isActiveTask(triggerTask.taskId)) - private fun isIncompatibleTask(task: TaskInfo) = + private fun isIncompatibleTask(task: RunningTaskInfo) = DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() && isTopActivityExemptFromDesktopWindowing(context, task) @@ -1848,6 +1864,15 @@ class DesktopTasksController( * fullscreen. */ private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + logV("handleIncompatibleTaskLaunch") + if (!isDesktopModeShowing(task.displayId)) return null + // Only update task repository for transparent task. + if ( + DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC + .isTrue() && isTransparentTask(task) + ) { + taskRepository.setTopTransparentFullscreenTaskId(task.displayId, task.taskId) + } // Already fullscreen, no-op. if (task.isFullscreen) return null return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } @@ -1909,6 +1934,50 @@ class DesktopTasksController( } } + /** + * Apply changes to move a freeform task from one display to another, which includes handling + * density changes between displays. + */ + private fun applyFreeformDisplayChange( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + destDisplayId: Int, + ) { + val sourceLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + val destLayout = displayController.getDisplayLayout(destDisplayId) ?: return + val bounds = taskInfo.configuration.windowConfiguration.bounds + val scaledWidth = bounds.width() * destLayout.densityDpi() / sourceLayout.densityDpi() + val scaledHeight = bounds.height() * destLayout.densityDpi() / sourceLayout.densityDpi() + val sourceWidthMargin = sourceLayout.width() - bounds.width() + val sourceHeightMargin = sourceLayout.height() - bounds.height() + val destWidthMargin = destLayout.width() - scaledWidth + val destHeightMargin = destLayout.height() - scaledHeight + val scaledLeft = + if (sourceWidthMargin != 0) { + bounds.left * destWidthMargin / sourceWidthMargin + } else { + destWidthMargin / 2 + } + val scaledTop = + if (sourceHeightMargin != 0) { + bounds.top * destHeightMargin / sourceHeightMargin + } else { + destHeightMargin / 2 + } + val boundsWithinDisplay = + if (destWidthMargin >= 0 && destHeightMargin >= 0) { + Rect(0, 0, scaledWidth, scaledHeight).apply { + offsetTo( + scaledLeft.coerceIn(0, destWidthMargin), + scaledTop.coerceIn(0, destHeightMargin), + ) + } + } else { + getInitialBounds(destLayout, taskInfo, destDisplayId) + } + wct.setBounds(taskInfo.token, boundsWithinDisplay) + } + private fun getInitialBounds( displayLayout: DisplayLayout, taskInfo: RunningTaskInfo, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index 9625b71ad3cb..5c79658b6809 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -75,6 +75,12 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state + if ( + DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC + .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() + ) { + updateTopTransparentFullscreenTaskId(info) + } updateWallpaperToken(info) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { handleBackNavigation(transition, info) @@ -264,4 +270,22 @@ class DesktopTasksTransitionObserver( } } } + + private fun updateTopTransparentFullscreenTaskId(info: TransitionInfo) { + info.changes.forEach { change -> + change.taskInfo?.let { task -> + val desktopRepository = desktopUserRepositories.getProfile(task.userId) + val displayId = task.displayId + // Clear `topTransparentFullscreenTask` information from repository if task + // is closed or sent to back. + if ( + TransitionUtil.isClosingMode(change.mode) && + task.taskId == + desktopRepository.getTopTransparentFullscreenTaskId(displayId) + ) { + desktopRepository.clearTopTransparentFullscreenTaskId(displayId) + } + } + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt index 3ab7b3418e02..7157a7f0b38f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt @@ -27,10 +27,9 @@ import org.junit.Test import org.junit.runner.RunWith /** - * Tests for {@link AppCompatUtils}. + * Tests for [@link AppCompatUtils]. * - * Build/Install/Run: - * atest WMShellUnitTests:AppCompatUtilsTest + * Build/Install/Run: atest WMShellUnitTests:AppCompatUtilsTest */ @RunWith(AndroidTestingRunner::class) @SmallTest diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index e777ec7b55f6..5629127b8c54 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -56,6 +56,11 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +/** + * Tests for [@link DesktopRepository]. + * + * Build/Install/Run: atest WMShellUnitTests:DesktopRepositoryTest + */ @SmallTest @RunWith(AndroidTestingRunner::class) @ExperimentalCoroutinesApi @@ -978,6 +983,22 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test + fun setTaskIdAsTopTransparentFullscreenTaskId_savesTaskId() { + repo.setTopTransparentFullscreenTaskId(displayId = DEFAULT_DISPLAY, taskId = 1) + + assertThat(repo.getTopTransparentFullscreenTaskId(DEFAULT_DISPLAY)).isEqualTo(1) + } + + @Test + fun clearTaskIdAsTopTransparentFullscreenTaskId_clearsTaskId() { + repo.setTopTransparentFullscreenTaskId(displayId = DEFAULT_DISPLAY, taskId = 1) + + repo.clearTopTransparentFullscreenTaskId(DEFAULT_DISPLAY) + + assertThat(repo.getTopTransparentFullscreenTaskId(DEFAULT_DISPLAY)).isNull() + } + + @Test fun setTaskInFullImmersiveState_savedAsInImmersiveState() { assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 0eb88e368054..4f37180baa37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -83,6 +83,7 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP +import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY import com.android.wm.shell.MockToken import com.android.wm.shell.R @@ -297,6 +298,7 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } + whenever(displayLayout.densityDpi()).thenReturn(160) whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) .thenReturn(Desktop.getDefaultInstance()) doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } @@ -543,6 +545,18 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun isDesktopModeShowing_topTransparentFullscreenTask_returnsTrue() { + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + + assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) @@ -1746,6 +1760,154 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) + fun moveToNextDisplay_sizeInDpPreserved() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Two displays have different density + whenever(displayLayout.densityDpi()).thenReturn(320) + whenever(displayLayout.width()).thenReturn(2400) + whenever(displayLayout.height()).thenReturn(1600) + val secondaryLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout) + whenever(secondaryLayout.densityDpi()).thenReturn(160) + whenever(secondaryLayout.width()).thenReturn(1280) + whenever(secondaryLayout.height()).thenReturn(720) + + // Place a task with a size of 640x480 at a position where the ratio of the left margin to + // the right margin is 1:3 and the ratio of top margin to the bottom margin is 1:2. + val task = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(440, 374, 1080, 854)) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val taskChange = changes[task.token.asBinder()] + assertThat(taskChange).isNotNull() + // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin + // to the right margin and the ratio of the top margin to bottom margin are also + // preserved. + assertThat(taskChange!!.configuration.windowConfiguration.bounds) + .isEqualTo(Rect(240, 160, 560, 400)) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) + fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Two displays have different density + whenever(displayLayout.densityDpi()).thenReturn(320) + whenever(displayLayout.width()).thenReturn(2400) + whenever(displayLayout.height()).thenReturn(1600) + val secondaryLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout) + whenever(secondaryLayout.densityDpi()).thenReturn(160) + whenever(secondaryLayout.width()).thenReturn(1280) + whenever(secondaryLayout.height()).thenReturn(720) + + // Place a task with a size of 640x480 at a position where the bottom-right corner of the + // window is outside the source display bounds. The destination display still has enough + // space to place the window within its bounds. + val task = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(2000, 1200, 2640, 1680)) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val taskChange = changes[task.token.asBinder()] + assertThat(taskChange).isNotNull() + assertThat(taskChange!!.configuration.windowConfiguration.bounds) + .isEqualTo(Rect(960, 480, 1280, 720)) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) + fun moveToNextDisplay_maximizedTask() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Two displays have different density + whenever(displayLayout.densityDpi()).thenReturn(320) + whenever(displayLayout.width()).thenReturn(1280) + whenever(displayLayout.height()).thenReturn(960) + val secondaryLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout) + whenever(secondaryLayout.densityDpi()).thenReturn(160) + whenever(secondaryLayout.width()).thenReturn(1280) + whenever(secondaryLayout.height()).thenReturn(720) + + // Place a task with a size equals to display size. + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(0, 0, 1280, 960)) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val taskChange = changes[task.token.asBinder()] + assertThat(taskChange).isNotNull() + // DP size is preserved. The window is centered in the destination display. + assertThat(taskChange!!.configuration.windowConfiguration.bounds) + .isEqualTo(Rect(320, 120, 960, 600)) + } + } + + @Test + @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) + fun moveToNextDisplay_defaultBoundsWhenDestinationTooSmall() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Two displays have different density + whenever(displayLayout.densityDpi()).thenReturn(320) + whenever(displayLayout.width()).thenReturn(2400) + whenever(displayLayout.height()).thenReturn(1600) + val secondaryLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout) + whenever(secondaryLayout.densityDpi()).thenReturn(160) + whenever(secondaryLayout.width()).thenReturn(640) + whenever(secondaryLayout.height()).thenReturn(480) + whenever(secondaryLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(0, 0, 640, 480) + } + + // A task with a size of 1800x1200 is being placed. To preserve DP size, + // 900x600 pixels are needed, which does not fit in the destination display. + val task = + setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(300, 200, 2100, 1400)) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val taskChange = changes[task.token.asBinder()] + assertThat(taskChange).isNotNull() + assertThat(taskChange!!.configuration.windowConfiguration.bounds.left).isAtLeast(0) + assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0) + assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640) + assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480) + } + } + + @Test fun getTaskWindowingMode() { val fullscreenTask = setUpFullscreenTask() val freeformTask = setUpFreeformTask() @@ -2473,6 +2635,63 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun handleRequest_topActivityTransparentWithDisplay_savedToDesktopRepository() { + val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + markTaskVisible(freeformTask) + + val transparentTask = + setUpFreeformTask(displayId = DEFAULT_DISPLAY).apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = false + numActivities = 1 + } + + controller.handleRequest(Binder(), createTransition(transparentTask)) + assertThat(taskRepository.getTopTransparentFullscreenTaskId(DEFAULT_DISPLAY)) + .isEqualTo(transparentTask.taskId) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_notSavedToDesktopRepository() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.handleRequest(Binder(), createTransition(task)) + assertThat(taskRepository.getTopTransparentFullscreenTaskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun handleRequest_onlyTopTransparentFullscreenTask_returnSwitchToFreeformWCT() { + val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId) + + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_returnNull() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT() { val freeformTask = setUpFreeformTask() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index c9623bcd5c16..c66d203fd89a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.os.IBinder import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE @@ -63,8 +64,15 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +/** + * Tests for [@link DesktopTasksTransitionObserver]. + * + * Build/Install/Run: atest WMShellUnitTests:DesktopTasksTransitionObserverTest + */ class DesktopTasksTransitionObserverTest { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val extendedMockitoRule = @@ -245,6 +253,48 @@ class DesktopTasksTransitionObserverTest { wct.assertRemoveAt(index = 0, wallpaperToken) } + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun topTransparentTaskClosed_clearTaskIdFromRepository() { + val mockTransition = Mockito.mock(IBinder::class.java) + val topTransparentTask = createTaskInfo(1) + whenever(taskRepository.getTopTransparentFullscreenTaskId(any())) + .thenReturn(topTransparentTask.taskId) + + transitionObserver.onTransitionReady( + transition = mockTransition, + info = createCloseTransition(topTransparentTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC, + ) + fun topTransparentTaskSentToBack_clearTaskIdFromRepository() { + val mockTransition = Mockito.mock(IBinder::class.java) + val topTransparentTask = createTaskInfo(1) + whenever(taskRepository.getTopTransparentFullscreenTaskId(any())) + .thenReturn(topTransparentTask.taskId) + + transitionObserver.onTransitionReady( + transition = mockTransition, + info = createToBackTransition(topTransparentTask), + startTransaction = mock(), + finishTransaction = mock(), + ) + + verify(taskRepository).clearTopTransparentFullscreenTaskId(topTransparentTask.displayId) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, @@ -301,6 +351,19 @@ class DesktopTasksTransitionObserverTest { } } + private fun createToBackTransition(task: RunningTaskInfo?): TransitionInfo { + return TransitionInfo(TRANSIT_TO_BACK, 0 /* flags */).apply { + addChange( + Change(mock(), mock()).apply { + mode = TRANSIT_TO_BACK + parent = null + taskInfo = task + flags = flags + } + ) + } + } + private fun getLatestWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out Transitions.TransitionHandler>? = null, diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index 7aed33d94223..16e20297dcf3 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -23,6 +23,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCA import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER; import static android.content.pm.ActivityInfo.isFixedOrientation; import static android.content.pm.ActivityInfo.isFixedOrientationLandscape; +import static android.content.pm.ActivityInfo.isFixedOrientationPortrait; import static android.content.pm.ActivityInfo.screenOrientationToString; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; @@ -66,6 +67,7 @@ class AppCompatOrientationPolicy { final boolean shouldCameraCompatControlOrientation = AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord); if (hasFullscreenOverride && isIgnoreOrientationRequestEnabled + && (isFixedOrientationLandscape(candidate) || isFixedOrientationPortrait(candidate)) // Do not override orientation to fullscreen for camera activities. // Fixed-orientation activities are rarely tested in other orientations, and it // often results in sideways or stretched previews. As the camera compat treatment diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index d0d3d4321a0a..f3b043bb51dd 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -60,7 +60,7 @@ class AppCompatSizeCompatModePolicy { /** * The precomputed display insets for resolving configuration. It will be non-null if - * {@link #shouldCreateAppCompatDisplayInsets} returns {@code true}. + * {@link ActivityRecord#shouldCreateAppCompatDisplayInsets} returns {@code true}. */ @Nullable private AppCompatDisplayInsets mAppCompatDisplayInsets; @@ -84,7 +84,7 @@ class AppCompatSizeCompatModePolicy { } /** - * @return The {@code true} if the current instance has {@link mAppCompatDisplayInsets} without + * @return The {@code true} if the current instance has {@link #mAppCompatDisplayInsets} without * considering the inheritance implemented in {@link #getAppCompatDisplayInsets()} */ boolean hasAppCompatDisplayInsetsWithoutInheritance() { diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java index 09ed9baba096..90bf5f03bb1f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java @@ -302,15 +302,15 @@ public class AppCompatOrientationPolicyTest extends WindowTestsBase { } @Test - public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() { + public void testOverrideOrientationIfNeeded_userFullscreenOverride_notLetterboxed_unchanged() { runTestScenarioWithActivity((robot) -> { robot.applyOnActivity((a) -> { a.setShouldApplyUserFullscreenOverride(true); a.setIgnoreOrientationRequest(true); }); - robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED, - /* expected */ SCREEN_ORIENTATION_USER); + robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LOCKED, + /* expected */ SCREEN_ORIENTATION_LOCKED); }); } |