Merge "Fix Pip tests after Pip Menu migration to a Window"
This commit is contained in:
committed by
Android (Google) Code Review
commit
cf69ba1961
@@ -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)
|
||||
uiDevice.closePipWindow()
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user