| // Copyright 2013 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.android_webview.test; |
| |
| import static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS; |
| |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.filters.MediumTest; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import org.chromium.android_webview.test.util.JSUtils; |
| import org.chromium.android_webview.test.util.JavascriptEventObserver; |
| import org.chromium.base.test.util.DisabledTest; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.content_public.browser.test.util.Criteria; |
| import org.chromium.content_public.browser.test.util.CriteriaHelper; |
| import org.chromium.content_public.browser.test.util.DOMUtils; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.content_public.browser.test.util.TouchCommon; |
| |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * Test the fullscreen API (WebChromeClient::onShow/HideCustomView). |
| * |
| * <p>Fullscreen support follows a different code path depending on whether |
| * the element is a video or not, so we test we both. For non-video elements |
| * we pick a div containing a video and custom html controls since this is a |
| * very common use case. |
| */ |
| @RunWith(AwJUnit4ClassRunner.class) |
| public class AwContentsClientFullScreenTest { |
| @Rule |
| public AwActivityTestRule mActivityTestRule = new AwActivityTestRule(); |
| |
| private static final String VIDEO_TEST_URL = |
| "file:///android_asset/full_screen_video_test.html"; |
| private static final String VIDEO_INSIDE_DIV_TEST_URL = |
| "file:///android_asset/full_screen_video_inside_div_test.html"; |
| |
| // These values must be kept in sync with the strings in |
| // full_screen_video_test.html, full_screen_video_inside_div_test.html and |
| // full_screen_video.js. |
| private static final String VIDEO_ID = "video"; |
| private static final String CUSTOM_PLAY_CONTROL_ID = "playControl"; |
| private static final String CUSTOM_FULLSCREEN_CONTROL_ID = "fullscreenControl"; |
| private static final String FULLSCREEN_ERROR_OBSERVER = "javaFullScreenErrorObserver"; |
| |
| private FullScreenVideoTestAwContentsClient mContentsClient; |
| private AwTestContainerView mTestContainerView; |
| |
| @Before |
| public void setUp() throws Exception { |
| mContentsClient = new FullScreenVideoTestAwContentsClient( |
| mActivityTestRule.getActivity(), mActivityTestRule.isHardwareAcceleratedTest()); |
| mTestContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(mTestContainerView.getAwContents()); |
| mTestContainerView.getAwContents().getSettings().setFullscreenSupported(true); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @DisableHardwareAccelerationForTest |
| @Test |
| @DisabledTest(message = "crbug.com/618749") // turning this on as an experiment. |
| public void testFullscreenVideoInSoftwareModeDoesNotDeadlock() throws Throwable { |
| // Although fullscreen video is not supported without hardware acceleration |
| // we should not deadlock if apps try to use it. |
| loadTestPageAndClickFullscreen(VIDEO_TEST_URL); |
| mContentsClient.waitForCustomViewShown(); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync( |
| () -> mContentsClient.getExitCallback().onCustomViewHidden()); |
| mContentsClient.waitForCustomViewHidden(); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @DisableHardwareAccelerationForTest |
| @Test |
| public void testFullscreenForNonVideoElementIsSupportedInSoftwareMode() throws Throwable { |
| // Fullscreen for non-video elements is supported and works as expected. Note that |
| // this test is the same as testOnShowAndHideCustomViewWithCallback_videoInsideDiv below. |
| doTestOnShowAndHideCustomViewWithCallback(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithCallback_video() throws Throwable { |
| doTestOnShowAndHideCustomViewWithCallback(VIDEO_TEST_URL); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithCallback_videoInsideDiv() throws Throwable { |
| doTestOnShowAndHideCustomViewWithCallback(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestOnShowAndHideCustomViewWithCallback(String videoTestUrl) throws Throwable { |
| doOnShowAndHideCustomViewTest(videoTestUrl, |
| () -> mContentsClient.getExitCallback().onCustomViewHidden()); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithJavascript_video() throws Throwable { |
| doTestOnShowAndHideCustomViewWithJavascript(VIDEO_TEST_URL); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithJavascript_videoInsideDiv() throws Throwable { |
| doTestOnShowAndHideCustomViewWithJavascript(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestOnShowAndHideCustomViewWithJavascript(String videoTestUrl) throws Throwable { |
| doOnShowAndHideCustomViewTest( |
| videoTestUrl, () -> DOMUtils.exitFullscreen(mTestContainerView.getWebContents())); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| // Originally flaked only in multi-process mode (http://crbug.com/616501) |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithBackKey_video() throws Throwable { |
| doTestOnShowAndHideCustomViewWithBackKey(VIDEO_TEST_URL); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| @DisabledTest(message = "crbug.com/618749") |
| public void testOnShowAndHideCustomViewWithBackKey_videoInsideDiv() throws Throwable { |
| doTestOnShowAndHideCustomViewWithBackKey(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestOnShowAndHideCustomViewWithBackKey(String videoTestUrl) throws Throwable { |
| doOnShowCustomViewTest(videoTestUrl); |
| |
| // The key event should not be propagated to mTestContainerView (the original container |
| // view). |
| mTestContainerView.setOnKeyListener((v, keyCode, event) -> { |
| Assert.fail("mTestContainerView received key event"); |
| return false; |
| }); |
| |
| InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); |
| mContentsClient.waitForCustomViewHidden(); |
| Assert.assertFalse(mContentsClient.wasOnUnhandledKeyUpEventCalled()); |
| assertWaitForIsEmbedded(); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testExitFullscreenEndsIfAppInvokesCallbackFromOnHideCustomView() throws Throwable { |
| mContentsClient.setOnHideCustomViewRunnable( |
| () -> mContentsClient.getExitCallback().onCustomViewHidden()); |
| doTestOnShowAndHideCustomViewWithCallback(VIDEO_TEST_URL); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testOnShowCustomViewAndPlayWithHtmlControl_video() throws Throwable { |
| doTestOnShowCustomViewAndPlayWithHtmlControl(VIDEO_TEST_URL); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testOnShowCustomViewAndPlayWithHtmlControl_videoInsideDiv() throws Throwable { |
| doTestOnShowCustomViewAndPlayWithHtmlControl(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestOnShowCustomViewAndPlayWithHtmlControl(String videoTestUrl) throws Throwable { |
| doOnShowCustomViewTest(videoTestUrl); |
| Assert.assertTrue(DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID)); |
| |
| playVideo(); |
| DOMUtils.waitForMediaPlay(getWebContentsOnUiThread(), VIDEO_ID); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testFullscreenNotSupported_video() throws Throwable { |
| doTestFullscreenNotSupported(VIDEO_TEST_URL); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testFullscreenNotSupported_videoInsideDiv() throws Throwable { |
| doTestFullscreenNotSupported(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestFullscreenNotSupported(String videoTestUrl) throws Throwable { |
| mTestContainerView.getAwContents().getSettings().setFullscreenSupported(false); |
| final JavascriptEventObserver fullscreenErrorObserver = registerObserver( |
| FULLSCREEN_ERROR_OBSERVER); |
| |
| loadTestPageAndClickFullscreen(videoTestUrl); |
| |
| Assert.assertTrue(fullscreenErrorObserver.waitForEvent(WAIT_TIMEOUT_MS)); |
| Assert.assertFalse(mContentsClient.wasCustomViewShownCalled()); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testPowerSaveBlockerIsEnabledDuringFullscreenPlayback_video() throws Throwable { |
| doTestPowerSaveBlockerIsEnabledDuringFullscreenPlayback(VIDEO_TEST_URL); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testPowerSaveBlockerIsEnabledDuringFullscreenPlayback_videoInsideDiv() |
| throws Throwable { |
| doTestPowerSaveBlockerIsEnabledDuringFullscreenPlayback(VIDEO_INSIDE_DIV_TEST_URL); |
| } |
| |
| public void doTestPowerSaveBlockerIsEnabledDuringFullscreenPlayback(String videoTestUrl) |
| throws Throwable { |
| // Enter fullscreen. |
| doOnShowCustomViewTest(videoTestUrl); |
| View customView = mContentsClient.getCustomView(); |
| |
| // No power save blocker is active before playback starts. |
| assertKeepScreenOnActive(customView, false); |
| |
| // Play and verify that there is an active power save blocker. |
| playVideo(); |
| assertWaitForKeepScreenOnActive(customView, true); |
| |
| // Stop the video and verify that the power save blocker is gone. |
| DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID); |
| assertWaitForKeepScreenOnActive(customView, false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testPowerSaveBlockerIsEnabledDuringEmbeddedPlayback() throws Throwable { |
| Assert.assertFalse(DOMUtils.isFullscreen(getWebContentsOnUiThread())); |
| loadTestPage(VIDEO_INSIDE_DIV_TEST_URL); |
| |
| // No power save blocker is active before playback starts. |
| assertKeepScreenOnActive(mTestContainerView, false); |
| |
| // Play and verify that there is an active power save blocker. |
| playVideoOnEmbedderView(); |
| assertWaitForKeepScreenOnActive(mTestContainerView, true); |
| |
| // Stop the video and verify that the power save blocker is gone. |
| DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID); |
| assertWaitForKeepScreenOnActive(mTestContainerView, false); |
| } |
| |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| @Test |
| public void testPowerSaveBlockerIsTransferredToFullscreen() throws Throwable { |
| Assert.assertFalse(DOMUtils.isFullscreen(getWebContentsOnUiThread())); |
| loadTestPage(VIDEO_INSIDE_DIV_TEST_URL); |
| |
| // Play and verify that there is an active power save blocker. |
| playVideoOnEmbedderView(); |
| assertWaitForKeepScreenOnActive(mTestContainerView, true); |
| |
| // Enter fullscreen and verify that the power save blocker is |
| // still there. |
| JSUtils.clickNodeWithUserGesture( |
| mTestContainerView.getWebContents(), CUSTOM_FULLSCREEN_CONTROL_ID); |
| mContentsClient.waitForCustomViewShown(); |
| assertKeepScreenOnActive(mTestContainerView, true); |
| |
| // Pause the video and the power save blocker is gone. |
| DOMUtils.pauseMedia(getWebContentsOnUiThread(), VIDEO_ID); |
| assertWaitForKeepScreenOnActive(mTestContainerView, false); |
| |
| // Exit fullscreen and the power save blocker is still gone. |
| DOMUtils.exitFullscreen(getWebContentsOnUiThread()); |
| mContentsClient.waitForCustomViewHidden(); |
| assertKeepScreenOnActive(mTestContainerView, false); |
| } |
| |
| @Test |
| @MediumTest |
| @Feature({"AndroidWebView"}) |
| public void testPowerSaveBlockerIsTransferredToEmbedded() throws Throwable { |
| // Enter fullscreen. |
| doOnShowCustomViewTest(VIDEO_INSIDE_DIV_TEST_URL); |
| View customView = mContentsClient.getCustomView(); |
| |
| // Play and verify that there is an active power save blocker |
| // in fullscreen. |
| playVideoOnEmbedderView(); |
| assertWaitForKeepScreenOnActive(customView, true); |
| |
| // Exit fullscreen and verify that the power save blocker is |
| // still there. |
| DOMUtils.exitFullscreen(getWebContentsOnUiThread()); |
| mContentsClient.waitForCustomViewHidden(); |
| assertKeepScreenOnActive(customView, true); |
| } |
| |
| private boolean shouldPlayOnFullScreenView() throws Exception { |
| String testUrl = mTestContainerView.getAwContents().getUrl(); |
| return VIDEO_TEST_URL.equals(testUrl) && DOMUtils.isFullscreen(getWebContentsOnUiThread()); |
| } |
| |
| private void playVideo() throws Exception { |
| if (shouldPlayOnFullScreenView()) { |
| playVideoOnFullScreenView(); |
| } else { |
| playVideoOnEmbedderView(); |
| } |
| } |
| |
| private void playVideoOnEmbedderView() throws Exception { |
| Assert.assertFalse(shouldPlayOnFullScreenView()); |
| waitUntilHaveEnoughDataForPlay(); |
| JSUtils.clickNodeWithUserGesture( |
| mTestContainerView.getWebContents(), CUSTOM_PLAY_CONTROL_ID); |
| } |
| |
| private void waitUntilHaveEnoughDataForPlay() throws Exception { |
| // crbug.com/936757: you are expected to wait before media playback is ready. |
| CriteriaHelper.pollInstrumentationThread(new Criteria() { |
| @Override |
| public boolean isSatisfied() { |
| try { |
| // Checking HTMLMediaElement.readyState == 4 (HAVE_ENOUGH_DATA). |
| int readyState = DOMUtils.getNodeField( |
| "readyState", getWebContentsOnUiThread(), VIDEO_ID, Integer.class); |
| updateFailureReason( |
| "Expected readyState == 4, but timed out when readyState == " |
| + readyState); |
| return readyState == 4; // HAVE_ENOUGH_DATA |
| } catch (Exception e) { |
| throw new RuntimeException(e); |
| } |
| } |
| }); |
| } |
| |
| private void playVideoOnFullScreenView() throws Exception { |
| Assert.assertTrue(shouldPlayOnFullScreenView()); |
| waitUntilHaveEnoughDataForPlay(); |
| // JSUtils.clickNodeWithUserGesture(getWebContentsOnUiThread(), nodeId); |
| // The VIDEO_TEST_URL page goes fullscreen on the <video> element. In fullscreen |
| // the button with id CUSTOM_PLAY_CONTROL_ID will not be visible, but the standard |
| // html5 video controls are. The standard html5 controls are shadow html elements |
| // without any ids so it is difficult to retrieve its precise location. However, |
| // a large play control is rendered in the center of the custom view |
| // (containing the fullscreen <video>) so we just rely on that fact here. |
| TouchCommon.singleClickView(mContentsClient.getCustomView()); |
| } |
| |
| /** |
| * Asserts that the keep screen on property in the given {@code view} is active as |
| * {@code expected}. It also verifies that it is only active when the video is playing. |
| */ |
| private void assertWaitForKeepScreenOnActive(final View view, final boolean expected) { |
| // We need to poll because it takes time to synchronize the state between the android |
| // views and Javascript. |
| CriteriaHelper.pollInstrumentationThread(new Criteria() { |
| @Override |
| public boolean isSatisfied() { |
| try { |
| return getKeepScreenOnOnInstrumentationThread(view) == expected |
| && DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID) |
| != expected; |
| } catch (InterruptedException | TimeoutException e) { |
| Assert.fail(e.getMessage()); |
| return false; |
| } |
| } |
| }); |
| } |
| |
| private void assertKeepScreenOnActive(final View view, final boolean expectKeepScreenOn) |
| throws Exception { |
| Assert.assertEquals(expectKeepScreenOn, getKeepScreenOnOnInstrumentationThread(view)); |
| |
| // Note: we're currently checking this after the above assertion to ensure that the timing |
| // of failure is as close to the assertEquals check below. |
| boolean ended = DOMUtils.isMediaEnded(getWebContentsOnUiThread(), VIDEO_ID); |
| // If this fails, consider increasing the media length. |
| Assert.assertFalse("Media playback should not end too early to test this.", ended); |
| |
| boolean expectPaused = !expectKeepScreenOn; |
| boolean paused = DOMUtils.isMediaPaused(getWebContentsOnUiThread(), VIDEO_ID); |
| Assert.assertEquals( |
| "The playback should be paused/not paused as expected.", expectPaused, paused); |
| } |
| |
| private boolean getKeepScreenOnOnInstrumentationThread(final View view) { |
| try { |
| return TestThreadUtils.runOnUiThreadBlocking(() -> getKeepScreenOnOnUiThread(view)); |
| } catch (Exception e) { |
| Assert.fail(e.getMessage()); |
| return false; |
| } |
| } |
| |
| private boolean getKeepScreenOnOnUiThread(View view) { |
| // The power save blocker is added to the container view. |
| // Search the view hierarchy for it. |
| if (view instanceof ViewGroup) { |
| ViewGroup viewGroup = (ViewGroup) view; |
| for (int i = 0; i < viewGroup.getChildCount(); i++) { |
| if (getKeepScreenOnOnUiThread(viewGroup.getChildAt(i))) { |
| return true; |
| } |
| } |
| } |
| return view.getKeepScreenOn(); |
| } |
| |
| private void assertWaitForIsFullscreen() { |
| // We need to poll because the Javascript state is updated asynchronously |
| CriteriaHelper.pollInstrumentationThread(new Criteria() { |
| @Override |
| public boolean isSatisfied() { |
| try { |
| return DOMUtils.isFullscreen(getWebContentsOnUiThread()); |
| } catch (InterruptedException | TimeoutException e) { |
| Assert.fail(e.getMessage()); |
| return false; |
| } |
| } |
| }); |
| } |
| |
| private void assertWaitForIsEmbedded() { |
| // We need to poll because the Javascript state is updated asynchronously |
| CriteriaHelper.pollInstrumentationThread(new Criteria() { |
| @Override |
| public boolean isSatisfied() { |
| try { |
| return !DOMUtils.isFullscreen(getWebContentsOnUiThread()); |
| } catch (InterruptedException | TimeoutException e) { |
| Assert.fail(e.getMessage()); |
| return false; |
| } |
| } |
| }); |
| // TODO: Test that inline video is actually displayed. |
| } |
| |
| private JavascriptEventObserver registerObserver(final String observerName) throws Throwable { |
| final JavascriptEventObserver observer = new JavascriptEventObserver(); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync( |
| () -> observer.register(mTestContainerView.getWebContents(), observerName)); |
| return observer; |
| } |
| |
| private void doOnShowAndHideCustomViewTest(String videoTestUrl, final Runnable existFullscreen) |
| throws Throwable { |
| doOnShowCustomViewTest(videoTestUrl); |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(existFullscreen); |
| mContentsClient.waitForCustomViewHidden(); |
| assertWaitForIsEmbedded(); |
| } |
| |
| private void doOnShowCustomViewTest(String videoTestUrl) throws Exception { |
| loadTestPageAndClickFullscreen(videoTestUrl); |
| mContentsClient.waitForCustomViewShown(); |
| assertWaitForIsFullscreen(); |
| } |
| |
| private void loadTestPageAndClickFullscreen(String videoTestUrl) throws Exception { |
| loadTestPage(videoTestUrl); |
| JSUtils.clickNodeWithUserGesture( |
| mTestContainerView.getWebContents(), CUSTOM_FULLSCREEN_CONTROL_ID); |
| } |
| |
| private void loadTestPage(String videoTestUrl) throws Exception { |
| mActivityTestRule.loadUrlSync(mTestContainerView.getAwContents(), |
| mContentsClient.getOnPageFinishedHelper(), videoTestUrl); |
| // As we are loading a non-trivial page, let's wait until we have something displayed. |
| mActivityTestRule.waitForVisualStateCallback(mTestContainerView.getAwContents()); |
| } |
| |
| private WebContents getWebContentsOnUiThread() { |
| try { |
| return TestThreadUtils.runOnUiThreadBlocking(() -> mTestContainerView.getWebContents()); |
| } catch (Exception e) { |
| Assert.fail(e.getMessage()); |
| return null; |
| } |
| } |
| } |