Loading libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +48 −32 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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) { Loading @@ -64,16 +83,13 @@ 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")) .findObject(By.res(packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) ?.takeIf { it.isCheckable } ?.apply { if (!isChecked) click() } ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } ?: error("'With custom actions' checkbox not found") fun pauseMedia() = mediaController?.transportControls?.pause() Loading @@ -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" } } libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +25 −20 Original line number Diff line number Diff line Loading @@ -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() } Loading Loading @@ -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()) } Loading @@ -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 }) Loading @@ -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 Loading @@ -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) ?: 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 { Loading Loading @@ -211,7 +217,6 @@ 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") // ... should also contain the "Full screen" and "Close" buttons, ... Loading @@ -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) Loading libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +64 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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? { Loading @@ -64,3 +91,32 @@ 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 libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +8 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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> Loading Loading
libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +48 −32 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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) { Loading @@ -64,16 +83,13 @@ 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")) .findObject(By.res(packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) ?.takeIf { it.isCheckable } ?.apply { if (!isChecked) click() } ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } ?: error("'With custom actions' checkbox not found") fun pauseMedia() = mediaController?.transportControls?.pause() Loading @@ -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" } }
libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +25 −20 Original line number Diff line number Diff line Loading @@ -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() } Loading Loading @@ -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()) } Loading @@ -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 }) Loading @@ -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 Loading @@ -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) ?: 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 { Loading Loading @@ -211,7 +217,6 @@ 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") // ... should also contain the "Full screen" and "Close" buttons, ... Loading @@ -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) Loading
libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +64 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading @@ -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? { Loading @@ -64,3 +91,32 @@ 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
libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml +8 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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> Loading