summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt80
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipBasicTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt45
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt72
-rw-r--r--libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_pip.xml8
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>