diff options
5 files changed, 146 insertions, 61 deletions
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 ed5f8a42258b..bc74daf307fc 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 @@ -20,20 +20,17 @@ import android.app.Instrumentation import android.media.session.MediaController import android.media.session.MediaSessionManager import android.os.SystemClock -import android.view.KeyEvent.KEYCODE_WINDOW import androidx.test.uiautomator.By -import androidx.test.uiautomator.Until +import androidx.test.uiautomator.BySelector import com.android.server.wm.flicker.helpers.closePipWindow import com.android.server.wm.flicker.helpers.hasPipWindow -import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME import com.android.wm.shell.flicker.TEST_APP_PIP_ACTIVITY_LABEL +import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow +import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild import com.android.wm.shell.flicker.testapp.Components -import org.junit.Assert.assertNotNull import org.junit.Assert.fail -class PipAppHelper( - instrumentation: Instrumentation -) : BaseAppHelper( +class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, TEST_APP_PIP_ACTIVITY_LABEL, Components.PipActivity() @@ -47,12 +44,34 @@ class PipAppHelper( it.packageName == packageName } - fun clickButton(resourceId: String) = - uiDevice.findObject(By.res(packageName, resourceId))?.click() - ?: fail("$resourceId button is not found") + fun clickObject(resId: String) { + val selector = By.res(packageName, resId) + val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") + + if (!isTelevision) { + obj.click() + } else { + focusOnObject(selector) || error("Could not focus on `$resId` object") + uiDevice.pressDPadCenter() + } + } + + private fun focusOnObject(selector: BySelector): Boolean { + // We expect all the focusable UI elements to be arranged in a way so that it is possible + // to "cycle" over all them by clicking the D-Pad DOWN button, going back up to "the top" + // from "the bottom". + repeat(FOCUS_ATTEMPTS) { + uiDevice.findObject(selector)?.apply { if (isFocusedOrHasFocusedChild) return true } + ?: error("The object we try to focus on is gone.") + + uiDevice.pressDPadDown() + uiDevice.waitForIdle() + } + return false + } fun clickEnterPipButton() { - clickButton("enter_pip") + clickObject(ENTER_PIP_BUTTON_ID) // TODO(b/172321238): remove this check once hasPipWindow is fixed on TVs if (!isTelevision) { @@ -64,17 +83,14 @@ class PipAppHelper( } fun clickStartMediaSessionButton() { - val startButton = uiDevice.findObject(By.res(packageName, "media_session_start")) - assertNotNull("Start button not found, this usually happens when the device " + - "was left in an unknown state (e.g. in split screen)", startButton) - startButton.click() + clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID) } fun checkWithCustomActionsCheckbox() = uiDevice - .findObject(By.res(packageName, "with_custom_actions")) - ?.takeIf { it.isCheckable } - ?.apply { if (!isChecked) click() } - ?: error("'With custom actions' checkbox not found") + .findObject(By.res(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") @@ -83,21 +99,21 @@ class PipAppHelper( ?: error("No active media session found") fun closePipWindow() { - // TODO(b/172321238): remove this check once and simply call closePipWindow once the TV - // logic is integrated there. - if (!isTelevision) { - uiDevice.closePipWindow() + if (isTelevision) { + uiDevice.closeTvPipWindow() } else { - // Bring up Pip menu - uiDevice.pressKeyCode(KEYCODE_WINDOW) - - // Wait for the menu to come up and render the close button - val closeButton = uiDevice.wait( - Until.findObject(By.res(SYSTEM_UI_PACKAGE_NAME, "close_button")), 3_000) - assertNotNull("Pip menu close button is not found", closeButton) - closeButton.click() + uiDevice.closePipWindow() + } - waitUntilClosed() + if (!waitUntilClosed()) { + fail("Couldn't close Pip") } } + + companion object { + private const val FOCUS_ATTEMPTS = 20 + 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" + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt index 70425a343c16..49094e609fbc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt @@ -42,7 +42,7 @@ class TvPipBasicTest( testApp.launchViaIntent() // Set up ratio and enter Pip - testApp.clickButton(radioButtonId) + testApp.clickObject(radioButtonId) testApp.clickEnterPipButton() val actualRatio: Float = testApp.ui?.visibleBounds?.ratio diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 4cb6447f7d7e..66efb5ae3c2d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -59,17 +59,24 @@ class TvPipMenuTests : TvPipTestBase() { @Test fun pipMenu_correctPosition() { - val pipMenu = enterPip_openMenu_assertShown() - - // Make sure it's fullscreen - assertTrue("Pip menu should be shown fullscreen", pipMenu.isFullscreen(uiDevice)) + enterPip_openMenu_assertShown() // Make sure the PiP task is positioned where it should be. val activityBounds: Rect = testApp.ui?.visibleBounds - ?: error("Could not retrieve PiP Activity bounds") + ?: error("Could not retrieve Pip Activity bounds") assertTrue("Pip Activity is positioned correctly while Pip menu is shown", pipBoundsWhileInMenu == activityBounds) + // Make sure the Pip Menu Actions are positioned correctly. + uiDevice.findTvPipMenuControls()?.visibleBounds?.run { + assertTrue("Pip Menu Actions should be positioned below the Activity in Pip", + top >= activityBounds.bottom) + assertTrue("Pip Menu Actions should be positioned central horizontally", + centerX() == uiDevice.displayWidth / 2) + assertTrue("Pip Menu Actions should be fully shown on the screen", + left >= 0 && right <= uiDevice.displayWidth && bottom <= uiDevice.displayHeight) + } ?: error("Could not retrieve Pip Menu Actions bounds") + testApp.closePipWindow() } @@ -100,11 +107,11 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain the Close button - val closeButton = uiDevice.findTvPipMenuCloseButton() + uiDevice.findTvPipMenuCloseButton() ?: fail("\"Close PIP\" button should be shown in Pip menu") // Clicking on the Close button should close the app - closeButton.click() + uiDevice.clickTvPipMenuCloseButton() assertTrue("\"Close PIP\" button should close the PiP", testApp.waitUntilClosed()) } @@ -113,12 +120,12 @@ class TvPipMenuTests : TvPipTestBase() { enterPip_openMenu_assertShown() // PiP menu should contain the Fullscreen button - val fullscreenButton = uiDevice.findTvPipMenuFullscreenButton() + uiDevice.findTvPipMenuFullscreenButton() ?: fail("\"Full screen\" button should be shown in Pip menu") // Clicking on the fullscreen button should return app to the fullscreen mode. // Click, wait for the app to go fullscreen - fullscreenButton.click() + uiDevice.clickTvPipMenuFullscreenButton() assertTrue("\"Full screen\" button should open the app fullscreen", wait { testApp.ui?.isFullscreen(uiDevice) ?: false }) @@ -136,12 +143,12 @@ class TvPipMenuTests : TvPipTestBase() { assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Pause button - val pauseButton = uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + "playing media session.") // When we pause media, the button should change from Pause to Play - pauseButton.click() + uiDevice.clickTvPipMenuElementWithDescription(pauseButtonDescription) assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Play button now @@ -161,27 +168,26 @@ class TvPipMenuTests : TvPipTestBase() { // PiP menu should contain "No-Op", "Off" and "Clear" buttons... uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP) ?: fail("\"No-Op\" button should be shown in Pip menu") - val offButton = uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) ?: fail("\"Off\" button should be shown in Pip menu") uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) ?: fail("\"Clear\" button should be shown in Pip menu") // ... and should also contain the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - offButton.click() + uiDevice.clickTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) // Invoking the "Off" action should replace it with the "On" action/button and should // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ... uiDevice.waitForTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_ON) ?: fail("\"On\" button should be shown in Pip for a corresponding custom action") assertNull("\"No-Op\" button should not be shown in Pip menu", uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)) - val clearButton = - uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) ?: fail("\"Clear\" button should be shown in Pip menu") // ... as well as the "Full screen" and "Close" buttons. assertFullscreenAndCloseButtonsAreShown() - clearButton.click() + uiDevice.clickTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions and their corresponding // buttons, ... uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(TEST_APP_PIP_MENU_ACTION_ON)?.also { @@ -211,9 +217,8 @@ class TvPipMenuTests : TvPipTestBase() { ?: fail("\"No-Op\" button should be shown in Pip menu") uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF) ?: fail("\"Off\" button should be shown in Pip menu") - val clearButton = - uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) - ?: fail("\"Clear\" button should be shown in Pip menu") + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) + ?: fail("\"Clear\" button should be shown in Pip menu") // ... should also contain the "Full screen" and "Close" buttons, ... assertFullscreenAndCloseButtonsAreShown() // ... but should not contain media buttons. @@ -222,7 +227,7 @@ class TvPipMenuTests : TvPipTestBase() { assertNull("\"Pause\" button should not be shown in menu when there are custom actions", uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) - clearButton.click() + uiDevice.clickTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR) // Invoking the "Clear" action should remove all the custom actions, which should bring up // media buttons... uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 0732794903b7..587b5510b0b4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.pip.tv import android.view.KeyEvent import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until @@ -25,11 +26,18 @@ import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME /** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */ private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu" +private const val TV_PIP_MENU_CONTROLS_ID = "pip_controls" private const val TV_PIP_MENU_CLOSE_BUTTON_ID = "close_button" private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "full_button" +private const val FOCUS_ATTEMPTS = 10 private const val WAIT_TIME_MS = 3_000L +private val TV_PIP_MENU_CLOSE_BUTTON_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID) +private val TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR = + By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID) + private val tvPipMenuSelector = By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID) fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW) @@ -39,16 +47,35 @@ fun UiDevice.waitForTvPipMenu(): UiObject2? = fun UiDevice.waitForTvPipMenuToClose(): Boolean = wait(Until.gone(tvPipMenuSelector), WAIT_TIME_MS) -fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = findObject( - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID)) +fun UiDevice.findTvPipMenuControls(): UiObject2? = + findObject(tvPipMenuSelector) + ?.findObject(By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CONTROLS_ID)) -fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? = findObject( - By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID)) +fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = + findObject(tvPipMenuSelector)?.findObject(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) -fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? { - val buttonSelector = By.desc(desc) - val menuWithButtonSelector = By.copy(tvPipMenuSelector).hasDescendant(buttonSelector) - return findObject(menuWithButtonSelector)?.findObject(buttonSelector) +fun UiDevice.clickTvPipMenuCloseButton() { + focusOnObjectInTvPipMenu(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) || + error("Could not focus on the Close button") + pressDPadCenter() +} + +fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? = + findObject(tvPipMenuSelector)?.findObject(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) + +fun UiDevice.clickTvPipMenuFullscreenButton() { + focusOnObjectInTvPipMenu(TV_PIP_MENU_FULLSCREEN_BUTTON_SELECTOR) || + error("Could not focus on the Fullscreen button") + pressDPadCenter() +} + +fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? = + findObject(tvPipMenuSelector)?.findObject(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) + +fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { + focusOnObjectInTvPipMenu(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) || + error("Could not focus on the Pip menu object with \"$desc\" description") + pressDPadCenter() } fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { @@ -63,4 +90,33 @@ fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boole fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth +} + +val UiObject2.isFocusedOrHasFocusedChild: Boolean + get() = isFocused || findObject(By.focused(true)) != null + +fun UiDevice.closeTvPipWindow() { + // Check if Pip menu is Open. If it's not, open it. + if (findObject(tvPipMenuSelector) == null) { + pressWindowKey() + waitForTvPipMenu() ?: error("Could not open Pip menu") + } + + clickTvPipMenuCloseButton() + waitForTvPipMenuToClose() +} + +private fun UiDevice.focusOnObjectInTvPipMenu(objectSelector: BySelector): Boolean { + repeat(FOCUS_ATTEMPTS) { + val menu = findObject(tvPipMenuSelector) ?: error("Pip Menu is now shown") + val objectToFocus = menu.findObject(objectSelector) + .apply { if (isFocusedOrHasFocusedChild) return true } + ?: error("The object we try to focus on is gone.") + val currentlyFocused = menu.findObject(By.focused(true)) + ?: error("Pip menu does not contain a focused element") + if (objectToFocus.visibleCenter.x < currentlyFocused.visibleCenter.x) + pressDPadLeft() else pressDPadRight() + waitForIdle() + } + return false }
\ No newline at end of file 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 e5d2f82080a2..909b77c87894 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 @@ -21,6 +21,12 @@ android:orientation="vertical" android:background="@android:color/holo_blue_bright"> + <!-- All the buttons (and other clickable elements) should be arranged in a way so that it is + possible to "cycle" over all them by clicking on the D-Pad DOWN button. The way we do it + here is by arranging them this vertical LL and by relying on the nextFocusDown attribute + where things are arranged differently and to circle back up to the top once we reach the + bottom. --> + <Button android:id="@+id/enter_pip" android:layout_width="wrap_content" @@ -87,12 +93,14 @@ android:id="@+id/media_session_start" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:nextFocusDown="@id/media_session_stop" android:text="Start"/> <Button android:id="@+id/media_session_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:nextFocusDown="@id/enter_pip" android:text="Stop"/> </LinearLayout> |