Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ecad6304 authored by Sergey Nikolaienkov's avatar Sergey Nikolaienkov
Browse files

Fix Pip tests after Pip Menu migration to a Window

Instrumentation tests on TVs used to use UiObject2.click() method to
interact with UI elements. This was inject input events that would
prompt input framework to enable the touch mode on the device. This was
wrong as TV devices are not expecting to work in the touch mode.
Migration of Pip menu from an Activity to a Window exposed this issue,
which caused multiple TV Pip tests to fail.
With this CL we change the way we interact with UI: we only are using
DPad navigation, which also means that we have to "track" the focused
element.

Bug: 174818743
Test: atest WMShellFlickerTests:TvPipBasicTest
Test: atest WMShellFlickerTests:TvPipMenuTests
Test: atest WMShellFlickerTests:TvPipNotificationTests
Change-Id: I335f8fd8d9c840f751d207c5e60b6af582118fee
parent df112354
Loading
Loading
Loading
Loading
+48 −32
Original line number Original line Diff line number Diff line
@@ -20,20 +20,17 @@ import android.app.Instrumentation
import android.media.session.MediaController
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.MediaSessionManager
import android.os.SystemClock
import android.os.SystemClock
import android.view.KeyEvent.KEYCODE_WINDOW
import androidx.test.uiautomator.By
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.closePipWindow
import com.android.server.wm.flicker.helpers.hasPipWindow
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.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 com.android.wm.shell.flicker.testapp.Components
import org.junit.Assert.assertNotNull
import org.junit.Assert.fail
import org.junit.Assert.fail


class PipAppHelper(
class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper(
    instrumentation: Instrumentation
) : BaseAppHelper(
        instrumentation,
        instrumentation,
        TEST_APP_PIP_ACTIVITY_LABEL,
        TEST_APP_PIP_ACTIVITY_LABEL,
        Components.PipActivity()
        Components.PipActivity()
@@ -47,12 +44,34 @@ class PipAppHelper(
            it.packageName == packageName
            it.packageName == packageName
        }
        }


    fun clickButton(resourceId: String) =
    fun clickObject(resId: String) {
            uiDevice.findObject(By.res(packageName, resourceId))?.click()
        val selector = By.res(packageName, resId)
                ?: fail("$resourceId button is not found")
        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() {
    fun clickEnterPipButton() {
        clickButton("enter_pip")
        clickObject(ENTER_PIP_BUTTON_ID)


        // TODO(b/172321238): remove this check once hasPipWindow is fixed on TVs
        // TODO(b/172321238): remove this check once hasPipWindow is fixed on TVs
        if (!isTelevision) {
        if (!isTelevision) {
@@ -64,16 +83,13 @@ class PipAppHelper(
    }
    }


    fun clickStartMediaSessionButton() {
    fun clickStartMediaSessionButton() {
        val startButton = uiDevice.findObject(By.res(packageName, "media_session_start"))
        clickObject(MEDIA_SESSION_START_RADIO_BUTTON_ID)
        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()
    }
    }


    fun checkWithCustomActionsCheckbox() = uiDevice
    fun checkWithCustomActionsCheckbox() = uiDevice
            .findObject(By.res(packageName, "with_custom_actions"))
            .findObject(By.res(packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID))
                ?.takeIf { it.isCheckable }
                ?.takeIf { it.isCheckable }
            ?.apply { if (!isChecked) click() }
                ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) }
                ?: error("'With custom actions' checkbox not found")
                ?: error("'With custom actions' checkbox not found")


    fun pauseMedia() = mediaController?.transportControls?.pause()
    fun pauseMedia() = mediaController?.transportControls?.pause()
@@ -83,21 +99,21 @@ class PipAppHelper(
            ?: error("No active media session found")
            ?: error("No active media session found")


    fun closePipWindow() {
    fun closePipWindow() {
        // TODO(b/172321238): remove this check once and simply call closePipWindow once the TV
        if (isTelevision) {
        //  logic is integrated there.
            uiDevice.closeTvPipWindow()
        if (!isTelevision) {
            uiDevice.closePipWindow()
        } else {
        } else {
            // Bring up Pip menu
            uiDevice.closePipWindow()
            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()


            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"
    }
}
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -42,7 +42,7 @@ class TvPipBasicTest(
        testApp.launchViaIntent()
        testApp.launchViaIntent()


        // Set up ratio and enter Pip
        // Set up ratio and enter Pip
        testApp.clickButton(radioButtonId)
        testApp.clickObject(radioButtonId)
        testApp.clickEnterPipButton()
        testApp.clickEnterPipButton()


        val actualRatio: Float = testApp.ui?.visibleBounds?.ratio
        val actualRatio: Float = testApp.ui?.visibleBounds?.ratio
+25 −20
Original line number Original line Diff line number Diff line
@@ -59,17 +59,24 @@ class TvPipMenuTests : TvPipTestBase() {


    @Test
    @Test
    fun pipMenu_correctPosition() {
    fun pipMenu_correctPosition() {
        val pipMenu = enterPip_openMenu_assertShown()
        enterPip_openMenu_assertShown()

        // Make sure it's fullscreen
        assertTrue("Pip menu should be shown fullscreen", pipMenu.isFullscreen(uiDevice))


        // Make sure the PiP task is positioned where it should be.
        // Make sure the PiP task is positioned where it should be.
        val activityBounds: Rect = testApp.ui?.visibleBounds
        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",
        assertTrue("Pip Activity is positioned correctly while Pip menu is shown",
                pipBoundsWhileInMenu == activityBounds)
                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()
        testApp.closePipWindow()
    }
    }


@@ -100,11 +107,11 @@ class TvPipMenuTests : TvPipTestBase() {
        enterPip_openMenu_assertShown()
        enterPip_openMenu_assertShown()


        // PiP menu should contain the Close button
        // PiP menu should contain the Close button
        val closeButton = uiDevice.findTvPipMenuCloseButton()
        uiDevice.findTvPipMenuCloseButton()
                ?: fail("\"Close PIP\" button should be shown in Pip menu")
                ?: fail("\"Close PIP\" button should be shown in Pip menu")


        // Clicking on the Close button should close the app
        // Clicking on the Close button should close the app
        closeButton.click()
        uiDevice.clickTvPipMenuCloseButton()
        assertTrue("\"Close PIP\" button should close the PiP", testApp.waitUntilClosed())
        assertTrue("\"Close PIP\" button should close the PiP", testApp.waitUntilClosed())
    }
    }


@@ -113,12 +120,12 @@ class TvPipMenuTests : TvPipTestBase() {
        enterPip_openMenu_assertShown()
        enterPip_openMenu_assertShown()


        // PiP menu should contain the Fullscreen button
        // PiP menu should contain the Fullscreen button
        val fullscreenButton = uiDevice.findTvPipMenuFullscreenButton()
        uiDevice.findTvPipMenuFullscreenButton()
                ?: fail("\"Full screen\" button should be shown in Pip menu")
                ?: fail("\"Full screen\" button should be shown in Pip menu")


        // Clicking on the fullscreen button should return app to the fullscreen mode.
        // Clicking on the fullscreen button should return app to the fullscreen mode.
        // Click, wait for the app to go fullscreen
        // Click, wait for the app to go fullscreen
        fullscreenButton.click()
        uiDevice.clickTvPipMenuFullscreenButton()
        assertTrue("\"Full screen\" button should open the app fullscreen",
        assertTrue("\"Full screen\" button should open the app fullscreen",
                wait { testApp.ui?.isFullscreen(uiDevice) ?: false })
                wait { testApp.ui?.isFullscreen(uiDevice) ?: false })


@@ -136,12 +143,12 @@ class TvPipMenuTests : TvPipTestBase() {
        assertFullscreenAndCloseButtonsAreShown()
        assertFullscreenAndCloseButtonsAreShown()


        // PiP menu should contain the Pause button
        // 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 " +
                ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " +
                        "playing media session.")
                        "playing media session.")


        // When we pause media, the button should change from Pause to Play
        // When we pause media, the button should change from Pause to Play
        pauseButton.click()
        uiDevice.clickTvPipMenuElementWithDescription(pauseButtonDescription)


        assertFullscreenAndCloseButtonsAreShown()
        assertFullscreenAndCloseButtonsAreShown()
        // PiP menu should contain the Play button now
        // 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...
        // PiP menu should contain "No-Op", "Off" and "Clear" buttons...
        uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)
        uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)
                ?: fail("\"No-Op\" button should be shown in Pip menu")
                ?: 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")
                ?: fail("\"Off\" button should be shown in Pip menu")
        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")
                ?: fail("\"Clear\" button should be shown in Pip menu")
        // ... and should also contain the "Full screen" and "Close" buttons.
        // ... and should also contain the "Full screen" and "Close" buttons.
        assertFullscreenAndCloseButtonsAreShown()
        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
        // 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 ...
        // remove the "No-Op" action/button. "Clear" action/button should remain in the menu ...
        uiDevice.waitForTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_ON)
        uiDevice.waitForTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_ON)
                ?: fail("\"On\" button should be shown in Pip for a corresponding custom action")
                ?: fail("\"On\" button should be shown in Pip for a corresponding custom action")
        assertNull("\"No-Op\" button should not be shown in Pip menu",
        assertNull("\"No-Op\" button should not be shown in Pip menu",
                uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP))
                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")
                        ?: fail("\"Clear\" button should be shown in Pip menu")
        // ... as well as the "Full screen" and "Close" buttons.
        // ... as well as the "Full screen" and "Close" buttons.
        assertFullscreenAndCloseButtonsAreShown()
        assertFullscreenAndCloseButtonsAreShown()


        clearButton.click()
        uiDevice.clickTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR)
        // Invoking the "Clear" action should remove all the custom actions and their corresponding
        // Invoking the "Clear" action should remove all the custom actions and their corresponding
        // buttons, ...
        // buttons, ...
        uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(TEST_APP_PIP_MENU_ACTION_ON)?.also {
        uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(TEST_APP_PIP_MENU_ACTION_ON)?.also {
@@ -211,7 +217,6 @@ class TvPipMenuTests : TvPipTestBase() {
                ?: fail("\"No-Op\" button should be shown in Pip menu")
                ?: fail("\"No-Op\" button should be shown in Pip menu")
        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")
                ?: fail("\"Off\" button should be shown in Pip menu")
        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")
                ?: fail("\"Clear\" button should be shown in Pip menu")
        // ... should also contain the "Full screen" and "Close" buttons, ...
        // ... should also contain the "Full screen" and "Close" buttons, ...
@@ -222,7 +227,7 @@ class TvPipMenuTests : TvPipTestBase() {
        assertNull("\"Pause\" button should not be shown in menu when there are custom actions",
        assertNull("\"Pause\" button should not be shown in menu when there are custom actions",
                uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription))
                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
        // Invoking the "Clear" action should remove all the custom actions, which should bring up
        // media buttons...
        // media buttons...
        uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription)
        uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription)
+64 −8
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.pip.tv


import android.view.KeyEvent
import android.view.KeyEvent
import androidx.test.uiautomator.By
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
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 */
/** 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_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_CLOSE_BUTTON_ID = "close_button"
private const val TV_PIP_MENU_FULLSCREEN_BUTTON_ID = "full_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 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)
private val tvPipMenuSelector = By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_ROOT_ID)


fun UiDevice.pressWindowKey() = pressKeyCode(KeyEvent.KEYCODE_WINDOW)
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.waitForTvPipMenuToClose(): Boolean = wait(Until.gone(tvPipMenuSelector), WAIT_TIME_MS)


fun UiDevice.findTvPipMenuCloseButton(): UiObject2? = findObject(
fun UiDevice.findTvPipMenuControls(): UiObject2? =
        By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CLOSE_BUTTON_ID))
        findObject(tvPipMenuSelector)
                ?.findObject(By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_CONTROLS_ID))


fun UiDevice.findTvPipMenuFullscreenButton(): UiObject2? = findObject(
fun UiDevice.findTvPipMenuCloseButton(): UiObject2? =
        By.res(SYSTEM_UI_PACKAGE_NAME, TV_PIP_MENU_FULLSCREEN_BUTTON_ID))
        findObject(tvPipMenuSelector)?.findObject(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR)


fun UiDevice.findTvPipMenuElementWithDescription(desc: String): UiObject2? {
fun UiDevice.clickTvPipMenuCloseButton() {
    val buttonSelector = By.desc(desc)
    focusOnObjectInTvPipMenu(TV_PIP_MENU_CLOSE_BUTTON_SELECTOR) ||
    val menuWithButtonSelector = By.copy(tvPipMenuSelector).hasDescendant(buttonSelector)
            error("Could not focus on the Close button")
    return findObject(menuWithButtonSelector)?.findObject(buttonSelector)
    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? {
fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? {
@@ -64,3 +91,32 @@ fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boole
fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run {
fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run {
    height() == uiDevice.displayHeight && width() == uiDevice.displayWidth
    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
+8 −0
Original line number Original line Diff line number Diff line
@@ -21,6 +21,12 @@
    android:orientation="vertical"
    android:orientation="vertical"
    android:background="@android:color/holo_blue_bright">
    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
    <Button
        android:id="@+id/enter_pip"
        android:id="@+id/enter_pip"
        android:layout_width="wrap_content"
        android:layout_width="wrap_content"
@@ -87,12 +93,14 @@
            android:id="@+id/media_session_start"
            android:id="@+id/media_session_start"
            android:layout_width="wrap_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_height="wrap_content"
            android:nextFocusDown="@id/media_session_stop"
            android:text="Start"/>
            android:text="Start"/>


        <Button
        <Button
            android:id="@+id/media_session_stop"
            android:id="@+id/media_session_stop"
            android:layout_width="wrap_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_height="wrap_content"
            android:nextFocusDown="@id/enter_pip"
            android:text="Stop"/>
            android:text="Stop"/>


    </LinearLayout>
    </LinearLayout>