diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 3c222e7d8b56d..bbf5afcff67a0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -31,6 +31,10 @@ val TEST_APP_PIP_ACTIVITY_COMPONENT_NAME: ComponentName = ComponentName.createRe TEST_APP_PACKAGE_NAME, ".PipActivity") const val TEST_APP_PIP_ACTIVITY_LABEL = "PipApp" const val TEST_APP_PIP_ACTIVITY_WINDOW_NAME = "PipActivity" +const val TEST_APP_PIP_MENU_ACTION_NO_OP = "No-Op" +const val TEST_APP_PIP_MENU_ACTION_ON = "On" +const val TEST_APP_PIP_MENU_ACTION_OFF = "Off" +const val TEST_APP_PIP_MENU_ACTION_CLEAR = "Clear" // Test App > Ime Activity val TEST_APP_IME_ACTIVITY_COMPONENT_NAME: ComponentName = ComponentName.createRelative( 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 532b3de6c99e6..e85ba9ef6da2a 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 @@ -70,6 +70,12 @@ class PipAppHelper( startButton.click() } + fun checkWithCustomActionsCheckbox() = uiDevice + .findObject(By.res(packageName, "with_custom_actions")) + ?.takeIf { it.isCheckable } + ?.apply { if (!isChecked) click() } + ?: error("'With custom actions' checkbox not found") + fun pauseMedia() = mediaController?.transportControls?.pause() ?: error("No active media session found") 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 871732cf74609..4cb6447f7d7e6 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 @@ -20,7 +20,12 @@ import android.graphics.Rect import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.UiObject2 import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_CLEAR +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_NO_OP +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_OFF +import com.android.wm.shell.flicker.TEST_APP_PIP_MENU_ACTION_ON import com.android.wm.shell.flicker.wait +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -128,6 +133,7 @@ class TvPipMenuTests : TvPipTestBase() { testApp.clickStartMediaSessionButton() enterPip_openMenu_assertShown() + assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Pause button val pauseButton = uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription) @@ -137,6 +143,7 @@ class TvPipMenuTests : TvPipTestBase() { // When we pause media, the button should change from Pause to Play pauseButton.click() + assertFullscreenAndCloseButtonsAreShown() // PiP menu should contain the Play button now uiDevice.waitForTvPipMenuElementWithDescription(playButtonDescription) ?: fail("\"Play\" button should be shown in Pip menu if there is an active " + @@ -145,10 +152,99 @@ class TvPipMenuTests : TvPipTestBase() { testApp.closePipWindow() } + @Test + fun pipMenu_withCustomActions() { + // Enter PiP with custom actions. + testApp.checkWithCustomActionsCheckbox() + enterPip_openMenu_assertShown() + + // 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) + ?: 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() + // 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() + // Invoking the "Clear" action should remove all the custom actions and their corresponding + // buttons, ... + uiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(TEST_APP_PIP_MENU_ACTION_ON)?.also { + isGone -> if (!isGone) fail("\"On\" button should not be shown in Pip menu") + } + assertNull("\"Off\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_OFF)) + assertNull("\"Clear\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_CLEAR)) + assertNull("\"No-Op\" button should not be shown in Pip menu", + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP)) + // ... but the menu should still contain the "Full screen" and "Close" buttons. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + + @Test + fun pipMenu_customActions_override_mediaControls() { + // Start media session before entering PiP with custom actions. + testApp.clickStartMediaSessionButton() + testApp.checkWithCustomActionsCheckbox() + enterPip_openMenu_assertShown() + + // PiP menu should contain "No-Op", "Off" and "Clear" buttons for the custom actions... + uiDevice.findTvPipMenuElementWithDescription(TEST_APP_PIP_MENU_ACTION_NO_OP) + ?: 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, ... + assertFullscreenAndCloseButtonsAreShown() + // ... but should not contain media buttons. + assertNull("\"Play\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(playButtonDescription)) + assertNull("\"Pause\" button should not be shown in menu when there are custom actions", + uiDevice.findTvPipMenuElementWithDescription(pauseButtonDescription)) + + clearButton.click() + // Invoking the "Clear" action should remove all the custom actions, which should bring up + // media buttons... + uiDevice.waitForTvPipMenuElementWithDescription(pauseButtonDescription) + ?: fail("\"Pause\" button should be shown in Pip menu if there is an active " + + "playing media session.") + // ... while the "Full screen" and "Close" buttons should remain in the menu. + assertFullscreenAndCloseButtonsAreShown() + + testApp.closePipWindow() + } + private fun enterPip_openMenu_assertShown(): UiObject2 { testApp.clickEnterPipButton() // Pressing the Window key should bring up Pip menu uiDevice.pressWindowKey() return uiDevice.waitForTvPipMenu() ?: fail("Pip menu should have been shown") } + + private fun assertFullscreenAndCloseButtonsAreShown() { + uiDevice.findTvPipMenuCloseButton() + ?: fail("\"Close PIP\" button should be shown in Pip menu") + uiDevice.findTvPipMenuFullscreenButton() + ?: fail("\"Full screen\" button should be shown in Pip menu") + } } \ No newline at end of file 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 8db8bc67da141..0732794903b7c 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 @@ -58,6 +58,9 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { ?.findObject(buttonSelector) } +fun UiDevice.waitUntilTvPipMenuElementWithDescriptionIsGone(desc: String): Boolean? = + wait(Until.gone(By.copy(tvPipMenuSelector).hasDescendant(By.desc(desc))), WAIT_TIME_MS) + fun UiObject2.isFullscreen(uiDevice: UiDevice): Boolean = visibleBounds.run { height() == uiDevice.displayHeight && width() == uiDevice.displayWidth } \ 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 b4a4c165cc7b0..e5d2f82080a25 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 @@ -28,6 +28,12 @@ android:text="Enter PIP" android:onClick="enterPip"/> + + mSwitchOffActions = new ArrayList<>(); + private final List mSwitchOnActions = new ArrayList<>(); + private final BroadcastReceiver mCustomActionReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_SWITCH_ON: + mPipParamsBuilder.setActions(mSwitchOnActions); + break; + case ACTION_SWITCH_OFF: + mPipParamsBuilder.setActions(mSwitchOffActions); + break; + case ACTION_CLEAR: + mPipParamsBuilder.setActions(Collections.emptyList()); + break; + case ACTION_NO_OP: + default: + return; + } + setPictureInPictureParams(mPipParamsBuilder.build()); + } + }; + private boolean mIsReceiverRegistered = false; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -72,9 +122,6 @@ public class PipActivity extends Activity { setContentView(R.layout.activity_pip); - mPipParamsBuilder = new PictureInPictureParams.Builder() - .setAspectRatio(RATIO_DEFAULT); - findViewById(R.id.media_session_start) .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING)); findViewById(R.id.media_session_stop) @@ -98,9 +145,63 @@ public class PipActivity extends Activity { updateMediaSessionState(STATE_STOPPED); } }); + + // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off' + // action is invoked. + // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear. + // The second set consists of 2 actions: 1) On; 2) Clear. + // Upon invocation 'Clear' action clear-off all the custom actions, including itself. + final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help); + final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP); + final RemoteAction switchOnAction = + buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON); + final RemoteAction switchOffAction = + buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF); + final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR); + mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction)); + mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction)); + } + + @Override + protected void onDestroy() { + if (mIsReceiverRegistered) { + unregisterReceiver(mCustomActionReceiver); + mIsReceiverRegistered = false; + } + super.onDestroy(); + } + + @Override + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, + Configuration newConfig) { + if (isInPictureInPictureMode && !mIsReceiverRegistered) { + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_NO_OP); + filter.addAction(ACTION_SWITCH_ON); + filter.addAction(ACTION_SWITCH_OFF); + filter.addAction(ACTION_CLEAR); + registerReceiver(mCustomActionReceiver, filter); + + mIsReceiverRegistered = true; + } else if (!isInPictureInPictureMode && mIsReceiverRegistered) { + unregisterReceiver(mCustomActionReceiver); + + mIsReceiverRegistered = false; + } + } + + private RemoteAction buildRemoteAction(Icon icon, String label, String action) { + final Intent intent = new Intent(action); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + return new RemoteAction(icon, label, label, pendingIntent); } public void enterPip(View v) { + final boolean withCustomActions = + ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked(); + mPipParamsBuilder.setActions( + withCustomActions ? mSwitchOnActions : Collections.emptyList()); enterPictureInPictureMode(mPipParamsBuilder.build()); }