diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index e9d438a569d55..5a364ddf3a8f6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -39,7 +39,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) - ?: error("Could not get MediaSessionManager") + ?: error("Could not get MediaSessionManager") private val mediaController: MediaController? get() = mediaSessionManager.getActiveSessions(null).firstOrNull { @@ -69,8 +69,10 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( action: String? = null, stringExtras: Map ) { - launchViaIntentAndWaitShown(wmHelper, expectedWindowName, action, stringExtras, - waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition)) + launchViaIntentAndWaitShown( + wmHelper, expectedWindowName, action, stringExtras, + waitConditions = arrayOf(WindowManagerStateHelper.pipShownCondition) + ) } /** @@ -85,7 +87,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( // from "the bottom". repeat(FOCUS_ATTEMPTS) { uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } - ?: error("The object we try to focus on is gone.") + ?: error("The object we try to focus on is gone.") uiDevice.pressDPadDown() uiDevice.waitForIdle() @@ -100,29 +102,62 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( // Wait on WMHelper or simply wait for 3 seconds wmHelper?.waitPipShown() ?: SystemClock.sleep(3_000) // when entering pip, the dismiss button is visible at the start. to ensure the pip - // animation is complete, wait until the pip dismiss button is no longer visible. + // animation is complete, wait until the pip dismiss button is no longer visible. // b/176822698: dismiss-only state will be removed in the future uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) } + @JvmOverloads + fun enterPipViaHomeButton(wmHelper: WindowManagerStateHelper) { + uiDevice.pressHome() + + // Wait on WMHelper or simply wait for 3 seconds + wmHelper.waitPipShown() ?: SystemClock.sleep(3_000) + // when entering pip, the dismiss button is visible at the start. to ensure the pip + // animation is complete, wait until the pip dismiss button is no longer visible. + // b/176822698: dismiss-only state will be removed in the future + uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) + } + + @JvmOverloads + fun enterPipViaSwipeToHome(wmHelper: WindowManagerStateHelper) { + uiDevice.swipe( + uiDevice.displayWidth / 2, uiDevice.displayHeight - 10, + uiDevice.displayWidth / 2, uiDevice.displayHeight - 300, 3 + ) + + // Wait on WMHelper or simply wait for 3 seconds + wmHelper.waitPipShown() ?: SystemClock.sleep(3_000) + // when entering pip, the dismiss button is visible at the start. to ensure the pip + // animation is complete, wait until the pip dismiss button is no longer visible. + // b/176822698: dismiss-only state will be removed in the future + uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) + } + + fun enableEnterPipOnUserLeaveHint() { + clickObject(ENTER_PIP_ON_USER_LEAVE_HINT) + } + fun clickStartMediaSessionButton() { clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID) } fun checkWithCustomActionsCheckbox() = uiDevice - .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) - ?.takeIf { it.isCheckable } - ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } - ?: error("'With custom actions' checkbox not found") + .findObject(By.res(component.packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) + ?.takeIf { it.isCheckable } + ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } + ?: error("'With custom actions' checkbox not found") fun pauseMedia() = mediaController?.transportControls?.pause() - ?: error("No active media session found") + ?: error("No active media session found") fun stopMedia() = mediaController?.transportControls?.stop() - ?: error("No active media session found") + ?: error("No active media session found") - @Deprecated("Use PipAppHelper.closePipWindow(wmHelper) instead", - ReplaceWith("closePipWindow(wmHelper)")) + @Deprecated( + "Use PipAppHelper.closePipWindow(wmHelper) instead", + ReplaceWith("closePipWindow(wmHelper)") + ) fun closePipWindow() { if (isTelevision) { uiDevice.closeTvPipWindow() @@ -152,7 +187,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) val dismissPipObject = uiDevice.findObject(dismissSelector) - ?: error("PIP window dismiss button not found") + ?: error("PIP window dismiss button not found") val dismissButtonBounds = dismissPipObject.visibleBounds uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) } @@ -172,7 +207,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) val expandPipObject = uiDevice.findObject(expandSelector) - ?: error("PIP window expand button not found") + ?: error("PIP window expand button not found") val expandButtonBounds = expandPipObject.visibleBounds uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) wmHelper.waitPipGone() @@ -194,5 +229,6 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( private const val ENTER_PIP_BUTTON_ID = "enter_pip" private const val WITH_CUSTOM_ACTIONS_BUTTON_ID = "with_custom_actions" private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" + private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual" } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt new file mode 100644 index 0000000000000..04bf856fd9f8d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022 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.flicker.pip + +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.Assume +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test entering pip from an app via [onUserLeaveHint] and by navigating to home. + * + * To run this test: `atest WMShellFlickerTests:EnterPipOnUserLeaveHintTest` + * + * Actions: + * Launch an app in full screen + * Select "Via code behind" radio button + * Press Home button to put [pipApp] in pip mode + * + * Notes: + * 1. All assertions are inherited from [EnterPipTest] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest(testSpec) { + + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setupAndTeardown(this) + setup { + eachRun { + pipApp.launchViaIntent(wmHelper) + pipApp.enableEnterPipOnUserLeaveHint() + } + } + teardown { + eachRun { + pipApp.exit(wmHelper) + } + } + transitions { + when (testSpec.isGesturalNavigation) { + true -> pipApp.enterPipViaSwipeToHome(wmHelper) + false -> pipApp.enterPipViaHomeButton(wmHelper) + } + } + } + + override fun pipAppLayerAlwaysVisible() { + if (!testSpec.isGesturalNavigation) super.pipAppLayerAlwaysVisible() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayers { + this.isVisible(pipApp.component) + .then().isInvisible(pipApp.component) + .then().isVisible(pipApp.component) + } + } + } + + override fun pipLayerReduces() { + // in gestural nav the pip enters through alpha animation + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.pipLayerReduces() + } + + override fun focusChanges() { + // in gestural nav the focus goes to different activity on swipe up + Assume.assumeFalse(testSpec.isGesturalNavigation) + super.focusChanges() + } + + override fun pipLayerRemainInsideVisibleBounds() { + if (!testSpec.isGesturalNavigation) super.pipLayerRemainInsideVisibleBounds() else { + // pip layer in gesture nav will disappear during transition + testSpec.assertLayersStart { + this.visibleRegion(pipApp.component).coversAtMost(displayBounds) + } + testSpec.assertLayersEnd { + this.visibleRegion(pipApp.component).coversAtMost(displayBounds) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index 0640ac526bd04..b4267ffced5cb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -43,7 +43,7 @@ import org.junit.runners.Parameterized * * Notes: * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] + * are inherited from [PipTransition] * 2. Part of the test setup occurs automatically via * [com.android.server.wm.flicker.TransitionRunnerWithRules], * including configuring navigation mode, initial orientation and ensuring no @@ -54,7 +54,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { /** * Defines the transition used to run the test @@ -98,7 +98,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipAppLayerAlwaysVisible() { + open fun pipAppLayerAlwaysVisible() { testSpec.assertLayers { this.isVisible(pipApp.component) } @@ -122,7 +122,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipLayerRemainInsideVisibleBounds() { + open fun pipLayerRemainInsideVisibleBounds() { testSpec.assertLayersVisibleRegion(pipApp.component) { coversAtMost(displayBounds) } @@ -133,7 +133,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun pipLayerReduces() { + open fun pipLayerReduces() { val layerName = pipApp.component.toLayerName() testSpec.assertLayers { val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } @@ -175,7 +175,7 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { */ @Presubmit @Test - fun focusChanges() { + open fun focusChanges() { testSpec.assertEventLog { this.focusChanges(pipApp.`package`, "NexusLauncherActivity") } @@ -192,8 +192,10 @@ class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { @JvmStatic fun getParams(): List { return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 3) + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 3 + ) } } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml index 909b77c878949..e9e7bb61660dd 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml @@ -40,6 +40,32 @@ android:layout_height="wrap_content" android:text="With custom actions"/> + + + + + + + + +