Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +23 −2 Original line number Diff line number Diff line Loading @@ -59,6 +59,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; Loading Loading @@ -237,7 +238,8 @@ public abstract class WMShellModule { InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter) { Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, Loading @@ -259,7 +261,8 @@ public abstract class WMShellModule { interactionJankMonitor, genericLinksParser, multiInstanceHelper, desktopTasksLimiter); desktopTasksLimiter, desktopActivityOrientationHandler); } return new CaptionWindowDecorViewModel( context, Loading Loading @@ -675,6 +678,24 @@ public abstract class WMShellModule { return new DesktopModeTaskRepository(); } @WMSingleton @Provides static Optional<DesktopActivityOrientationChangeHandler> provideActivityOrientationHandler( Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, TaskStackListenerImpl taskStackListener, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository ) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(new DesktopActivityOrientationChangeHandler( context, shellInit, shellTaskOrganizer, taskStackListener, toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository)); } return Optional.empty(); } @WMSingleton @Provides static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt 0 → 100644 +112 −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.ActivityManager.RunningTaskInfo import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.ScreenOrientation import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect import android.util.Size import android.window.WindowContainerTransaction import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.TaskStackListenerCallback import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit /** Handles task resizing to respect orientation change of non-resizeable activities in desktop. */ class DesktopActivityOrientationChangeHandler( context: Context, shellInit: ShellInit, private val shellTaskOrganizer: ShellTaskOrganizer, private val taskStackListener: TaskStackListenerImpl, private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler, private val taskRepository: DesktopModeTaskRepository, ) { init { if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback({ onInit() }, this) } } private fun onInit() { taskStackListener.addListener(object : TaskStackListenerCallback { override fun onActivityRequestedOrientationChanged( taskId: Int, @ScreenOrientation requestedOrientation: Int ) { // Handle requested screen orientation changes at runtime. handleActivityOrientationChange(taskId, requestedOrientation) } }) } /** * Triggered with onTaskInfoChanged to handle: * * New activity launching from same task with different orientation * * Top activity closing in same task with different orientation to previous activity */ fun handleActivityOrientationChange(oldTask: RunningTaskInfo, newTask: RunningTaskInfo) { val newTopActivityInfo = newTask.topActivityInfo ?: return val oldTopActivityInfo = oldTask.topActivityInfo ?: return // Check if screen orientation is different from old task info so there is no duplicated // calls to handle runtime requested orientation changes. if (oldTopActivityInfo.screenOrientation != newTopActivityInfo.screenOrientation) { handleActivityOrientationChange(newTask.taskId, newTopActivityInfo.screenOrientation) } } private fun handleActivityOrientationChange( taskId: Int, @ScreenOrientation requestedOrientation: Int ) { if (!Flags.respectOrientationChangeForUnresizeable()) return val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return if (!isDesktopModeShowing(task.displayId) || !task.isFreeform || task.isResizeable) return val taskBounds = task.configuration.windowConfiguration.bounds val taskHeight = taskBounds.height() val taskWidth = taskBounds.width() if (taskWidth == taskHeight) return val orientation = if (taskWidth > taskHeight) ORIENTATION_LANDSCAPE else ORIENTATION_PORTRAIT // Non-resizeable activity requested opposite orientation. if (orientation == ORIENTATION_PORTRAIT && ActivityInfo.isFixedOrientationLandscape(requestedOrientation) || orientation == ORIENTATION_LANDSCAPE && ActivityInfo.isFixedOrientationPortrait(requestedOrientation)) { val finalSize = Size(taskHeight, taskWidth) // Use the center x as the resizing anchor point. val left = taskBounds.centerX() - finalSize.width / 2 val right = left + finalSize.width val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height) val wct = WindowContainerTransaction().setBounds(task.token, finalBounds) resizeHandler.startTransition(wct) } } private fun isDesktopModeShowing(displayId: Int): Boolean = taskRepository.getVisibleTaskCount(displayId) > 0 } No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +12 −3 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; Loading Loading @@ -166,6 +167,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private TaskOperations mTaskOperations; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; private final Transitions mTransitions; private final Optional<DesktopActivityOrientationChangeHandler> mActivityOrientationChangeHandler; private SplitScreenController mSplitScreenController; Loading Loading @@ -215,7 +218,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler ) { this( context, Loading @@ -241,7 +245,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { rootTaskDisplayAreaOrganizer, new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter); desktopTasksLimiter, activityOrientationChangeHandler); } @VisibleForTesting Loading Loading @@ -269,7 +274,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter) { Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; Loading Loading @@ -297,6 +303,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { DesktopModeWindowDecoration decoration; RunningTaskInfo taskInfo; Loading Loading @@ -388,6 +395,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { incrementEventReceiverTasks(taskInfo.displayId); } decoration.relayout(taskInfo); mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); } @Override Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt 0 → 100644 +263 −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.ActivityManager.RunningTaskInfo import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT import android.graphics.Rect import android.os.Binder 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.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** * Test class for {@link DesktopActivityOrientationChangeHandler} * * Usage: atest WMShellUnitTests:DesktopActivityOrientationChangeHandlerTest */ @SmallTest @RunWith(AndroidTestingRunner::class) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var taskStackListener: TaskStackListenerImpl private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler private lateinit var shellInit: ShellInit private lateinit var taskRepository: DesktopModeTaskRepository // Mock running tasks are registered here so we can get the list from mock shell task organizer. private val runningTasks = mutableListOf<RunningTaskInfo>() @Before fun setUp() { mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } shellInit = spy(ShellInit(testExecutor)) taskRepository = DesktopModeTaskRepository() whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, taskStackListener, resizeTransitionHandler, taskRepository) shellInit.init() } @After fun tearDown() { mockitoSession.finishMocking() runningTasks.clear() } @Test fun instantiate_addInitCallback() { verify(shellInit).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) } @Test fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) clearInvocations(shellInit) handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, taskStackListener, resizeTransitionHandler, taskRepository) verify(shellInit, never()).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) } @Test fun handleActivityOrientationChange_resizeable_doNothing() { val task = setUpFreeformTask() taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() { val task = createFullscreenTask() task.isResizeable = false val activityInfo = ActivityInfo() activityInfo.screenOrientation = SCREEN_ORIENTATION_PORTRAIT task.topActivityInfo = activityInfo whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) taskRepository.addActiveTask(DEFAULT_DISPLAY, task.taskId) taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task.taskId, visible = true) runningTasks.add(task) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { val task = setUpFreeformTask(isResizeable = false) val newTask = setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT) handler.handleActivityOrientationChange(task, newTask) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_notInDesktopMode_doNothing() { val task = setUpFreeformTask(isResizeable = false) taskRepository.updateTaskVisibility(task.displayId, task.taskId, visible = false) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { val task = setUpFreeformTask(isResizeable = false) val oldBounds = task.configuration.windowConfiguration.bounds val newTask = setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE) handler.handleActivityOrientationChange(task, newTask) val wct = getLatestResizeDesktopTaskWct() val finalBounds = findBoundsChange(wct, newTask) assertNotNull(finalBounds) val finalWidth = finalBounds.width() val finalHeight = finalBounds.height() // Bounds is landscape. assertTrue(finalWidth > finalHeight) // Aspect ratio remains the same. assertEquals(oldBounds.height() / oldBounds.width(), finalWidth / finalHeight) // Anchor point for resizing is at the center. assertEquals(oldBounds.centerX(), finalBounds.centerX()) } @Test fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { val oldBounds = Rect(0, 0, 500, 200) val task = setUpFreeformTask( isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds ) val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds) handler.handleActivityOrientationChange(task, newTask) val wct = getLatestResizeDesktopTaskWct() val finalBounds = findBoundsChange(wct, newTask) assertNotNull(finalBounds) val finalWidth = finalBounds.width() val finalHeight = finalBounds.height() // Bounds is portrait. assertTrue(finalHeight > finalWidth) // Aspect ratio remains the same. assertEquals(oldBounds.width() / oldBounds.height(), finalHeight / finalWidth) // Anchor point for resizing is at the center. assertEquals(oldBounds.centerX(), finalBounds.centerX()) } private fun setUpFreeformTask( displayId: Int = DEFAULT_DISPLAY, isResizeable: Boolean = true, orientation: Int = SCREEN_ORIENTATION_PORTRAIT, bounds: Rect? = Rect(0, 0, 200, 500) ): RunningTaskInfo { val task = createFreeformTask(displayId, bounds) val activityInfo = ActivityInfo() activityInfo.screenOrientation = orientation task.topActivityInfo = activityInfo task.isResizeable = isResizeable whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) taskRepository.addActiveTask(displayId, task.taskId) taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) runningTasks.add(task) return task } private fun getLatestResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(resizeTransitionHandler, atLeastOnce()) .startTransition(capture(arg), eq(currentBounds)) return arg.value } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds } No newline at end of file libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +5 −1 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter Loading Loading @@ -167,6 +168,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper @Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter @Mock private lateinit var mockFreeformTaskTransitionStarter: FreeformTaskTransitionStarter @Mock private lateinit var mockActivityOrientationChangeHandler: DesktopActivityOrientationChangeHandler private lateinit var spyContext: TestableContext private val transactionFactory = Supplier<SurfaceControl.Transaction> { Loading Loading @@ -220,7 +223,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter) Optional.of(mockTasksLimiter), Optional.of(mockActivityOrientationChangeHandler) ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +23 −2 Original line number Diff line number Diff line Loading @@ -59,6 +59,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; Loading Loading @@ -237,7 +238,8 @@ public abstract class WMShellModule { InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter) { Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return new DesktopModeWindowDecorViewModel( context, Loading @@ -259,7 +261,8 @@ public abstract class WMShellModule { interactionJankMonitor, genericLinksParser, multiInstanceHelper, desktopTasksLimiter); desktopTasksLimiter, desktopActivityOrientationHandler); } return new CaptionWindowDecorViewModel( context, Loading Loading @@ -675,6 +678,24 @@ public abstract class WMShellModule { return new DesktopModeTaskRepository(); } @WMSingleton @Provides static Optional<DesktopActivityOrientationChangeHandler> provideActivityOrientationHandler( Context context, ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, TaskStackListenerImpl taskStackListener, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository ) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of(new DesktopActivityOrientationChangeHandler( context, shellInit, shellTaskOrganizer, taskStackListener, toggleResizeDesktopTaskTransitionHandler, desktopModeTaskRepository)); } return Optional.empty(); } @WMSingleton @Provides static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandler.kt 0 → 100644 +112 −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.ActivityManager.RunningTaskInfo import android.content.Context import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.ScreenOrientation import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect import android.util.Size import android.window.WindowContainerTransaction import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.TaskStackListenerCallback import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit /** Handles task resizing to respect orientation change of non-resizeable activities in desktop. */ class DesktopActivityOrientationChangeHandler( context: Context, shellInit: ShellInit, private val shellTaskOrganizer: ShellTaskOrganizer, private val taskStackListener: TaskStackListenerImpl, private val resizeHandler: ToggleResizeDesktopTaskTransitionHandler, private val taskRepository: DesktopModeTaskRepository, ) { init { if (DesktopModeStatus.canEnterDesktopMode(context)) { shellInit.addInitCallback({ onInit() }, this) } } private fun onInit() { taskStackListener.addListener(object : TaskStackListenerCallback { override fun onActivityRequestedOrientationChanged( taskId: Int, @ScreenOrientation requestedOrientation: Int ) { // Handle requested screen orientation changes at runtime. handleActivityOrientationChange(taskId, requestedOrientation) } }) } /** * Triggered with onTaskInfoChanged to handle: * * New activity launching from same task with different orientation * * Top activity closing in same task with different orientation to previous activity */ fun handleActivityOrientationChange(oldTask: RunningTaskInfo, newTask: RunningTaskInfo) { val newTopActivityInfo = newTask.topActivityInfo ?: return val oldTopActivityInfo = oldTask.topActivityInfo ?: return // Check if screen orientation is different from old task info so there is no duplicated // calls to handle runtime requested orientation changes. if (oldTopActivityInfo.screenOrientation != newTopActivityInfo.screenOrientation) { handleActivityOrientationChange(newTask.taskId, newTopActivityInfo.screenOrientation) } } private fun handleActivityOrientationChange( taskId: Int, @ScreenOrientation requestedOrientation: Int ) { if (!Flags.respectOrientationChangeForUnresizeable()) return val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return if (!isDesktopModeShowing(task.displayId) || !task.isFreeform || task.isResizeable) return val taskBounds = task.configuration.windowConfiguration.bounds val taskHeight = taskBounds.height() val taskWidth = taskBounds.width() if (taskWidth == taskHeight) return val orientation = if (taskWidth > taskHeight) ORIENTATION_LANDSCAPE else ORIENTATION_PORTRAIT // Non-resizeable activity requested opposite orientation. if (orientation == ORIENTATION_PORTRAIT && ActivityInfo.isFixedOrientationLandscape(requestedOrientation) || orientation == ORIENTATION_LANDSCAPE && ActivityInfo.isFixedOrientationPortrait(requestedOrientation)) { val finalSize = Size(taskHeight, taskWidth) // Use the center x as the resizing anchor point. val left = taskBounds.centerX() - finalSize.width / 2 val right = left + finalSize.width val finalBounds = Rect(left, taskBounds.top, right, taskBounds.top + finalSize.height) val wct = WindowContainerTransaction().setBounds(task.token, finalBounds) resizeHandler.startTransition(wct) } } private fun isDesktopModeShowing(displayId: Int): Boolean = taskRepository.getVisibleTaskCount(displayId) > 0 } No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +12 −3 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; Loading Loading @@ -166,6 +167,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private TaskOperations mTaskOperations; private final Supplier<SurfaceControl.Transaction> mTransactionFactory; private final Transitions mTransitions; private final Optional<DesktopActivityOrientationChangeHandler> mActivityOrientationChangeHandler; private SplitScreenController mSplitScreenController; Loading Loading @@ -215,7 +218,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { InteractionJankMonitor interactionJankMonitor, AppToWebGenericLinksParser genericLinksParser, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler ) { this( context, Loading @@ -241,7 +245,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { rootTaskDisplayAreaOrganizer, new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter); desktopTasksLimiter, activityOrientationChangeHandler); } @VisibleForTesting Loading Loading @@ -269,7 +274,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter) { Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; Loading Loading @@ -297,6 +303,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { DesktopModeWindowDecoration decoration; RunningTaskInfo taskInfo; Loading Loading @@ -388,6 +395,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { incrementEventReceiverTasks(taskInfo.displayId); } decoration.relayout(taskInfo); mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); } @Override Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt 0 → 100644 +263 −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.ActivityManager.RunningTaskInfo import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT import android.graphics.Rect import android.os.Binder 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.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.TaskStackListenerImpl import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** * Test class for {@link DesktopActivityOrientationChangeHandler} * * Usage: atest WMShellUnitTests:DesktopActivityOrientationChangeHandlerTest */ @SmallTest @RunWith(AndroidTestingRunner::class) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var taskStackListener: TaskStackListenerImpl private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler private lateinit var shellInit: ShellInit private lateinit var taskRepository: DesktopModeTaskRepository // Mock running tasks are registered here so we can get the list from mock shell task organizer. private val runningTasks = mutableListOf<RunningTaskInfo>() @Before fun setUp() { mockitoSession = mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } shellInit = spy(ShellInit(testExecutor)) taskRepository = DesktopModeTaskRepository() whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, taskStackListener, resizeTransitionHandler, taskRepository) shellInit.init() } @After fun tearDown() { mockitoSession.finishMocking() runningTasks.clear() } @Test fun instantiate_addInitCallback() { verify(shellInit).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) } @Test fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) clearInvocations(shellInit) handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, taskStackListener, resizeTransitionHandler, taskRepository) verify(shellInit, never()).addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) } @Test fun handleActivityOrientationChange_resizeable_doNothing() { val task = setUpFreeformTask() taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() { val task = createFullscreenTask() task.isResizeable = false val activityInfo = ActivityInfo() activityInfo.screenOrientation = SCREEN_ORIENTATION_PORTRAIT task.topActivityInfo = activityInfo whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) taskRepository.addActiveTask(DEFAULT_DISPLAY, task.taskId) taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task.taskId, visible = true) runningTasks.add(task) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { val task = setUpFreeformTask(isResizeable = false) val newTask = setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT) handler.handleActivityOrientationChange(task, newTask) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_notInDesktopMode_doNothing() { val task = setUpFreeformTask(isResizeable = false) taskRepository.updateTaskVisibility(task.displayId, task.taskId, visible = false) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @Test fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { val task = setUpFreeformTask(isResizeable = false) val oldBounds = task.configuration.windowConfiguration.bounds val newTask = setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE) handler.handleActivityOrientationChange(task, newTask) val wct = getLatestResizeDesktopTaskWct() val finalBounds = findBoundsChange(wct, newTask) assertNotNull(finalBounds) val finalWidth = finalBounds.width() val finalHeight = finalBounds.height() // Bounds is landscape. assertTrue(finalWidth > finalHeight) // Aspect ratio remains the same. assertEquals(oldBounds.height() / oldBounds.width(), finalWidth / finalHeight) // Anchor point for resizing is at the center. assertEquals(oldBounds.centerX(), finalBounds.centerX()) } @Test fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { val oldBounds = Rect(0, 0, 500, 200) val task = setUpFreeformTask( isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds ) val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds) handler.handleActivityOrientationChange(task, newTask) val wct = getLatestResizeDesktopTaskWct() val finalBounds = findBoundsChange(wct, newTask) assertNotNull(finalBounds) val finalWidth = finalBounds.width() val finalHeight = finalBounds.height() // Bounds is portrait. assertTrue(finalHeight > finalWidth) // Aspect ratio remains the same. assertEquals(oldBounds.width() / oldBounds.height(), finalHeight / finalWidth) // Anchor point for resizing is at the center. assertEquals(oldBounds.centerX(), finalBounds.centerX()) } private fun setUpFreeformTask( displayId: Int = DEFAULT_DISPLAY, isResizeable: Boolean = true, orientation: Int = SCREEN_ORIENTATION_PORTRAIT, bounds: Rect? = Rect(0, 0, 200, 500) ): RunningTaskInfo { val task = createFreeformTask(displayId, bounds) val activityInfo = ActivityInfo() activityInfo.screenOrientation = orientation task.topActivityInfo = activityInfo task.isResizeable = isResizeable whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) taskRepository.addActiveTask(displayId, task.taskId) taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) runningTasks.add(task) return task } private fun getLatestResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(resizeTransitionHandler, atLeastOnce()) .startTransition(capture(arg), eq(currentBounds)) return arg.value } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds } No newline at end of file
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +5 −1 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter Loading Loading @@ -167,6 +168,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockMultiInstanceHelper: MultiInstanceHelper @Mock private lateinit var mockTasksLimiter: DesktopTasksLimiter @Mock private lateinit var mockFreeformTaskTransitionStarter: FreeformTaskTransitionStarter @Mock private lateinit var mockActivityOrientationChangeHandler: DesktopActivityOrientationChangeHandler private lateinit var spyContext: TestableContext private val transactionFactory = Supplier<SurfaceControl.Transaction> { Loading Loading @@ -220,7 +223,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockRootTaskDisplayAreaOrganizer, windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter) Optional.of(mockTasksLimiter), Optional.of(mockActivityOrientationChangeHandler) ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) Loading