blob: 4a7889a68175223da217a9d6696ff22193157a67 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.media;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import android.app.Activity;
import android.app.RemoteAction;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import android.util.Rational;
import android.view.View;
import androidx.annotation.RequiresApi;
import androidx.test.filters.MediumTest;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.overlay_window.PlaybackState;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.browser.test.util.WebContentsUtils;
import org.chromium.media_session.mojom.MediaSessionAction;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
/**
* Tests for PictureInPictureActivity.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@RequiresApi(Build.VERSION_CODES.O)
public class PictureInPictureActivityTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
@Rule
public JniMocker mMocker = new JniMocker();
private static final long NATIVE_OVERLAY = 100L;
private static final long PIP_TIMEOUT_MILLISECONDS = 10000L;
@Mock
private PictureInPictureActivity.Natives mNativeMock;
private Tab mTab;
// Source rect hint that we'll provide as the video element position.
private Rect mSourceRectHint = new Rect(100, 200, 300, 400);
// Helper to capture the source rect hint bounds that PictureInPictureActivity would like to use
// for `makeEnterIntoPip`, if any.
private PictureInPictureActivity.LaunchIntoPipHelper mLaunchIntoPipHelper =
new PictureInPictureActivity.LaunchIntoPipHelper() {
@Override
public Bundle build(Context activityContext, Rect bounds) {
// Store the bounds in the parent class for easy reference.
mBounds = bounds;
return null;
}
};
// Helper that we replace with `mLaunchIntoPipHelper`, so that we can restore it.
private PictureInPictureActivity.LaunchIntoPipHelper mOriginalHelper;
// Most recently provided bounds, if any.
private Rect mBounds;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mActivityTestRule.startMainActivityOnBlankPage();
mTab = mActivityTestRule.getActivity().getActivityTab();
mMocker.mock(PictureInPictureActivityJni.TEST_HOOKS, mNativeMock);
mOriginalHelper = PictureInPictureActivity.setLaunchIntoPipHelper(mLaunchIntoPipHelper);
}
@After
public void teardown() {
// Restore the original helper.
PictureInPictureActivity.setLaunchIntoPipHelper(mOriginalHelper);
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
public void testStartActivity() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
// The LaunchIntoPipHelper should have been called.
Assert.assertTrue(mBounds != null);
Criteria.checkThat(mBounds, Matchers.is(mSourceRectHint));
testExitOn(activity, () -> activity.close());
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
public void testExitOnClose() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
testExitOn(activity, () -> activity.close());
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
public void testExitOnCrash() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
testExitOn(activity, () -> WebContentsUtils.simulateRendererKilled(getWebContents()));
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testMakeEnterPictureInPictureWithBadSourceRect() throws Throwable {
mSourceRectHint.left = -1;
PictureInPictureActivity activity = startPictureInPictureActivity();
// The pip helper should not be called with trivially bad bounds.
Assert.assertTrue(mBounds == null);
testExitOn(activity, () -> activity.close());
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testExitOnBackToTab() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
Configuration newConfig = activity.getResources().getConfiguration();
testExitOn(activity,
()
-> activity.onPictureInPictureModeChanged(
/*isInPictureInPictureMode=*/false, newConfig));
verify(mNativeMock, times(1)).onBackToTab(NATIVE_OVERLAY);
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testResize() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
// Resize to some reasonable size, and verify that native is told about it.
final int reasonableSize = 10;
View view = activity.getViewForTesting();
TestThreadUtils.runOnUiThreadBlocking(
() -> view.layout(0, 0, reasonableSize, reasonableSize));
verify(mNativeMock, times(1))
.onViewSizeChanged(NATIVE_OVERLAY, reasonableSize, reasonableSize);
// An unreasonably large size should not generate a resize event.
final int unreasonableSize = activity.getWindowAndroid().getDisplay().getDisplayWidth();
TestThreadUtils.runOnUiThreadBlocking(
() -> view.layout(0, 0, unreasonableSize, unreasonableSize));
verify(mNativeMock, times(0)).onViewSizeChanged(anyInt(), anyInt(), anyInt());
testExitOn(activity, () -> activity.close());
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testMediaActions() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
PictureInPictureActivity.MediaActionButtonsManager manager =
activity.mMediaActionsButtonsManager;
activity.updateVisibleActions(new int[] {MediaSessionAction.PLAY});
activity.setPlaybackState(PlaybackState.PAUSED);
ArrayList<RemoteAction> actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 1);
Assert.assertEquals(actions.get(0), manager.mPlay);
activity.setPlaybackState(PlaybackState.PLAYING);
actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.get(0), manager.mPause);
// Both next track and previous track button should be visible when only one of them is
// enabled. The one that is not handled should be visible and disabled.
activity.updateVisibleActions(
new int[] {MediaSessionAction.PLAY, MediaSessionAction.PREVIOUS_TRACK});
actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 3);
Assert.assertEquals(actions.get(0), manager.mPreviousTrack);
Assert.assertEquals(actions.get(2), manager.mNextTrack);
Assert.assertTrue(actions.get(0).isEnabled());
Assert.assertFalse(actions.get(2).isEnabled());
// When all actions are not handled, there should be a dummy action presented to prevent
// android picture-in-picture from using default MediaSession.
activity.updateVisibleActions(new int[] {});
actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 1);
Assert.assertFalse(actions.get(0).isEnabled());
testExitOn(activity, () -> activity.close());
}
@Test
@MediumTest
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
public void testMediaActionsForVideoConferencing() throws Throwable {
PictureInPictureActivity activity = startPictureInPictureActivity();
PictureInPictureActivity.MediaActionButtonsManager manager =
activity.mMediaActionsButtonsManager;
activity.updateVisibleActions(new int[] {MediaSessionAction.TOGGLE_MICROPHONE});
ArrayList<RemoteAction> actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 1);
Assert.assertEquals(actions.get(0), manager.mMicrophone.getAction());
activity.updateVisibleActions(new int[] {MediaSessionAction.TOGGLE_CAMERA});
actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 1);
Assert.assertEquals(actions.get(0), manager.mCamera.getAction());
activity.updateVisibleActions(new int[] {MediaSessionAction.HANG_UP});
actions = manager.getActionsForPictureInPictureParams();
Assert.assertEquals(actions.size(), 1);
Assert.assertEquals(actions.get(0), manager.mHangUp);
testExitOn(activity, () -> activity.close());
}
private WebContents getWebContents() {
return mActivityTestRule.getActivity().getCurrentWebContents();
}
private void testExitOn(Activity activity, Runnable runnable) throws Throwable {
runnable.run();
CriteriaHelper.pollUiThread(() -> {
Criteria.checkThat(activity == null || activity.isDestroyed(), Matchers.is(true));
}, PIP_TIMEOUT_MILLISECONDS, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
}
private PictureInPictureActivity startPictureInPictureActivity() throws Exception {
PictureInPictureActivity activity =
ActivityTestUtils.waitForActivity(InstrumentationRegistry.getInstrumentation(),
PictureInPictureActivity.class, new Callable<Void>() {
@Override
public Void call() throws TimeoutException {
TestThreadUtils.runOnUiThreadBlocking(
()
-> PictureInPictureActivity.createActivity(
NATIVE_OVERLAY, mTab, mSourceRectHint.left,
mSourceRectHint.top,
mSourceRectHint.width(),
mSourceRectHint.height()));
return null;
}
});
verify(mNativeMock, times(1)).onActivityStart(eq(NATIVE_OVERLAY), eq(activity), any());
CriteriaHelper.pollUiThread(() -> {
Criteria.checkThat(activity.isInPictureInPictureMode(), Matchers.is(true));
}, PIP_TIMEOUT_MILLISECONDS, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
Rational ratio = activity.getAspectRatio();
Criteria.checkThat(ratio,
Matchers.is(new Rational(mSourceRectHint.width(), mSourceRectHint.height())));
return activity;
}
}