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:
Sergey Nikolaienkov
2020-11-19 21:23:24 +00:00
parent 679bbd5149
commit dececd5dd3
6 changed files with 220 additions and 4 deletions

View File

@@ -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(

View File

@@ -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")

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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"

View File

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