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());
}