Add tests for custom actions to TvPipMenuTests
Test that correct UI elements are displayed for the custom actions; that the set of custom actions can be updates (while PiP menu is shown); that custom actions collaborate correctly with "regular" actions (Full screen and Close), as well as with the media contol actions. Bug: 171520419 Test: atest WMShellFlickerTests:TvPipMenuTests Change-Id: I1f5b1e4896a60b8d2bfb118e548466e7f4036e61
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -28,6 +28,12 @@
|
||||
android:text="Enter PIP"
|
||||
android:onClick="enterPip"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/with_custom_actions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="With custom actions"/>
|
||||
|
||||
<RadioGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -25,7 +25,15 @@ import static android.media.session.PlaybackState.STATE_PLAYING;
|
||||
import static android.media.session.PlaybackState.STATE_STOPPED;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.PlaybackState;
|
||||
@@ -34,6 +42,12 @@ import android.util.Rational;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PipActivity extends Activity {
|
||||
/**
|
||||
@@ -52,7 +66,19 @@ public class PipActivity extends Activity {
|
||||
private static final Rational RATIO_WIDE = new Rational(2, 1);
|
||||
private static final Rational RATIO_TALL = new Rational(1, 2);
|
||||
|
||||
private PictureInPictureParams.Builder mPipParamsBuilder;
|
||||
private static final String PIP_ACTION_NO_OP = "No-Op";
|
||||
private static final String PIP_ACTION_OFF = "Off";
|
||||
private static final String PIP_ACTION_ON = "On";
|
||||
private static final String PIP_ACTION_CLEAR = "Clear";
|
||||
private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP";
|
||||
private static final String ACTION_SWITCH_OFF =
|
||||
"com.android.wm.shell.flicker.testapp.SWITCH_OFF";
|
||||
private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON";
|
||||
private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR";
|
||||
|
||||
private final PictureInPictureParams.Builder mPipParamsBuilder =
|
||||
new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(RATIO_DEFAULT);
|
||||
private MediaSession mMediaSession;
|
||||
private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder()
|
||||
.setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP)
|
||||
@@ -60,6 +86,30 @@ public class PipActivity extends Activity {
|
||||
private PlaybackState mPlaybackState = mPlaybackStateBuilder.build();
|
||||
private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder();
|
||||
|
||||
private final List<RemoteAction> mSwitchOffActions = new ArrayList<>();
|
||||
private final List<RemoteAction> 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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user