diff options
3 files changed, 257 insertions, 45 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 92c3020a14e6..bc2ed3f35b45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -1472,7 +1472,9 @@ public abstract class WMShellModule { RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, IWindowManager windowManager, ShellTaskOrganizer shellTaskOrganizer, - DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider + DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + InputManager inputManager, + @ShellMainThread Handler mainHandler ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -1484,7 +1486,9 @@ public abstract class WMShellModule { rootTaskDisplayAreaOrganizer, windowManager, shellTaskOrganizer, - desktopWallpaperActivityTokenProvider)); + desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler)); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt index e89aafe267ed..904d86282c39 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt @@ -22,6 +22,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.windowingModeToString import android.content.Context +import android.hardware.input.InputManager +import android.os.Handler import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -29,11 +31,13 @@ import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction +import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.transition.Transitions /** Controls the display windowing mode in desktop mode */ @@ -44,8 +48,26 @@ class DesktopDisplayModeController( private val windowManager: IWindowManager, private val shellTaskOrganizer: ShellTaskOrganizer, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val inputManager: InputManager, + @ShellMainThread private val mainHandler: Handler, ) { + private val onTabletModeChangedListener = + object : InputManager.OnTabletModeChangedListener { + override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) { + refreshDisplayWindowingMode() + } + } + + init { + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + inputManager.registerOnTabletModeChangedListener( + onTabletModeChangedListener, + mainHandler, + ) + } + } + fun refreshDisplayWindowingMode() { if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return @@ -89,10 +111,20 @@ class DesktopDisplayModeController( transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } - private fun getTargetWindowingModeForDefaultDisplay(): Int { + @VisibleForTesting + fun getTargetWindowingModeForDefaultDisplay(): Int { if (isExtendedDisplayEnabled() && hasExternalDisplay()) { return WINDOWING_MODE_FREEFORM } + if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { + if (isInClamshellMode()) { + return WINDOWING_MODE_FREEFORM + } + return WINDOWING_MODE_FULLSCREEN + } + + // If form factor-based desktop first switch is disabled, use the default display windowing + // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC). return windowManager.getWindowingMode(DEFAULT_DISPLAY) } @@ -108,6 +140,8 @@ class DesktopDisplayModeController( private fun hasExternalDisplay() = rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } + private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF + private fun logV(msg: String, vararg arguments: Any?) { ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt index cc37c440f650..450989dd334d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt @@ -21,9 +21,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver +import android.hardware.input.InputManager import android.os.Binder +import android.os.Handler import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.view.Display.DEFAULT_DISPLAY @@ -44,6 +47,7 @@ import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -64,13 +68,18 @@ import org.mockito.kotlin.whenever */ @SmallTest @RunWith(TestParameterInjector::class) -class DesktopDisplayModeControllerTest : ShellTestCase() { +class DesktopDisplayModeControllerTest( + @TestParameter(valuesProvider = FlagsParameterizationProvider::class) + flags: FlagsParameterization +) : ShellTestCase() { private val transitions = mock<Transitions>() private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val mockWindowManager = mock<IWindowManager>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val inputManager = mock<InputManager>() + private val mainHandler = mock<Handler>() private lateinit var controller: DesktopDisplayModeController @@ -82,6 +91,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) private val wallpaperToken = MockToken().token() + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder()) @@ -95,27 +108,20 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { mockWindowManager, shellTaskOrganizer, desktopWallpaperActivityTokenProvider, + inputManager, + mainHandler, ) runningTasks.add(freeformTask) runningTasks.add(fullscreenTask) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks)) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) + setTabletModeStatus(SwitchState.UNKNOWN) } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectToSwitch: Boolean, - ) { - defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode) - val settingsSession = - ExtendedDisplaySettingsSession( - context.contentResolver, - if (extendedDisplayEnabled) 1 else 0, - ) - - settingsSession.use { + private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) { + defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN) + ExtendedDisplaySettingsSession(context.contentResolver, 1).use { connectExternalDisplay() if (expectToSwitch) { // Assumes [connectExternalDisplay] properly triggered the switching transition. @@ -133,7 +139,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FULLSCREEN) } else { @@ -144,25 +150,64 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled( - @TestParameter param: ModeSwitchTestCase - ) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - // When the flag is disabled, never switch. - expectToSwitch = false, - ) + fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() { + // When the flag is disabled, never switch. + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false) } @Test @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) - fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) { - testDisplayWindowingModeSwitch( - param.defaultWindowingMode, - param.extendedDisplayEnabled, - param.expectToSwitchByDefault, - ) + fun displayWindowingModeSwitchOnDisplayConnected() { + testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING) + @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH) + fun testTargetWindowingMode_formfactorDisabled( + @TestParameter param: ExternalDisplayBasedTargetModeTestCase, + @TestParameter tabletModeStatus: SwitchState, + ) { + whenever(mockWindowManager.getWindowingMode(anyInt())) + .thenReturn(param.defaultWindowingMode) + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING, + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH, + ) + fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) { + if (param.hasExternalDisplay) { + connectExternalDisplay() + } else { + disconnectExternalDisplay() + } + setTabletModeStatus(param.tabletModeStatus) + + ExtendedDisplaySettingsSession( + context.contentResolver, + if (param.extendedDisplayEnabled) 1 else 0, + ) + .use { + assertThat(controller.getTargetWindowingModeForDefaultDisplay()) + .isEqualTo(param.expectedWindowingMode) + } } @Test @@ -217,6 +262,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { controller.refreshDisplayWindowingMode() } + private fun setTabletModeStatus(status: SwitchState) { + whenever(inputManager.isInTabletMode()).thenReturn(status.value) + } + private class ExtendedDisplaySettingsSession( private val contentResolver: ContentResolver, private val overrideValue: Int, @@ -233,33 +282,158 @@ class DesktopDisplayModeControllerTest : ShellTestCase() { } } + private class FlagsParameterizationProvider : TestParameterValuesProvider() { + override fun provideValues( + context: TestParameterValuesProvider.Context + ): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH + ) + } + } + companion object { const val EXTERNAL_DISPLAY_ID = 100 - enum class ModeSwitchTestCase( + enum class SwitchState(val value: Int) { + UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN), + ON(InputManager.SWITCH_STATE_ON), + OFF(InputManager.SWITCH_STATE_OFF), + } + + enum class ExternalDisplayBasedTargetModeTestCase( val defaultWindowingMode: Int, + val hasExternalDisplay: Boolean, val extendedDisplayEnabled: Boolean, - val expectToSwitchByDefault: Boolean, + val expectedWindowingMode: Int, ) { - FULLSCREEN_DISPLAY( + FREEFORM_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_EXTERNAL_EXTENDED( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FREEFORM_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, extendedDisplayEnabled = true, - expectToSwitchByDefault = true, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_EXTENDED( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = true, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + FREEFORM_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = true, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FULLSCREEN_DISPLAY_MIRRORING( + FULLSCREEN_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = true, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), - FREEFORM_DISPLAY( + FREEFORM_NO_EXTERNAL_MIRROR( defaultWindowingMode = WINDOWING_MODE_FREEFORM, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + FULLSCREEN_NO_EXTERNAL_MIRROR( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + hasExternalDisplay = false, + extendedDisplayEnabled = false, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + } + + enum class FormFactorBasedTargetModeTestCase( + val hasExternalDisplay: Boolean, + val extendedDisplayEnabled: Boolean, + val tabletModeStatus: SwitchState, + val expectedWindowingMode: Int, + ) { + EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = true, extendedDisplayEnabled = true, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, ), - FREEFORM_DISPLAY_MIRRORING( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, + NO_EXTERNAL_EXTENDED_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_TABLET( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.ON, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_MIRROR_CLAMSHELL( + hasExternalDisplay = false, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.OFF, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FREEFORM, + ), + NO_EXTERNAL_EXTENDED_UNKNOWN( + hasExternalDisplay = false, + extendedDisplayEnabled = true, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = true, + extendedDisplayEnabled = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, + ), + NO_EXTERNAL_MIRROR_UNKNOWN( + hasExternalDisplay = false, extendedDisplayEnabled = false, - expectToSwitchByDefault = false, + tabletModeStatus = SwitchState.UNKNOWN, + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN, ), } } |