diff options
20 files changed, 2849 insertions, 0 deletions
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn index 97f8c53..d880717 100644 --- a/chrome/android/BUILD.gn +++ b/chrome/android/BUILD.gn @@ -531,6 +531,9 @@ instrumentation_test_apk("chrome_public_test_apk") { ":chrome_public_test_apk_manifest", ":chrome_shared_test_java", ] + data_deps = [ + "//chrome/test/android/chrome_public_test_support:chrome_public_test_support_apk", + ] isolate_file = "../chrome_public_test_apk.isolate" proguard_enabled = !is_debug } diff --git a/chrome/android/chrome_apk.gyp b/chrome/android/chrome_apk.gyp index 143397b..a38a52d 100644 --- a/chrome/android/chrome_apk.gyp +++ b/chrome/android/chrome_apk.gyp @@ -337,6 +337,7 @@ 'dependencies': [ 'chrome_shared_test_java', 'chrome_public_apk_java', + '../../chrome/chrome.gyp:require_chrome_public_test_support_apk', '../../testing/android/on_device_instrumentation.gyp:broker_java', '../../testing/android/on_device_instrumentation.gyp:require_driver_apk', ], diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastNotificationTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastNotificationTest.java new file mode 100644 index 0000000..6d2210d --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastNotificationTest.java @@ -0,0 +1,104 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent.CanceledException; +import android.test.suitebuilder.annotation.LargeTest; +import android.view.View; + +import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.media.remote.NotificationTransportControl.ListenerService; +import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState; +import org.chromium.chrome.test.util.ActivityUtils; + +import java.util.concurrent.TimeoutException; + +/** + * Tests of the notification. + */ +public class CastNotificationTest extends CastTestBase { + + private static final long PAUSE_TEST_TIME_MS = 1000; + + /** + * Test the pause button on the notification. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testNotificationPause() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + // Get the notification + NotificationTransportControl notificationTransportControl = waitForCastNotification(); + assertNotNull("No notificationTransportControl", notificationTransportControl); + // We can't actually click the notification's buttons, since they are owned by a different + // process and hence is not accessible through instrumentation, so send the stop event + // instead. + NotificationTransportControl.ListenerService service = + waitForCastNotificationService(notificationTransportControl); + assertNotNull("No notification service", service); + + try { + service.getPendingIntent(ListenerService.ACTION_ID_PAUSE).send(); + } catch (CanceledException e) { + fail(); + } + assertTrue("Not paused", waitForState(PlayerState.PAUSED)); + + // The new position is sent in a separate message, so we have to wait a bit before + // fetching it. + Thread.sleep(STABILIZE_TIME_MS); + int position = getRemotePositionMs(); + // Position should not change while paused + Thread.sleep(PAUSE_TEST_TIME_MS); + assertEquals("Pause didn't stop playback", position, getRemotePositionMs()); + try { + service.getPendingIntent(ListenerService.ACTION_ID_PLAY).send(); + } catch (CanceledException e) { + fail(); + } + assertTrue("Not playing", waitForState(PlayerState.PLAYING)); + + // Should now be running again. + Thread.sleep(PAUSE_TEST_TIME_MS); + assertTrue("Run didn't restart playback", position < getRemotePositionMs()); + } + + /** + * Test select (pressing the body) on the notification. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testNotificationSelect() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + // Get the notification + NotificationTransportControl notificationTransportControl = waitForCastNotification(); + assertNotNull("No notificationTransportControl", notificationTransportControl); + // We can't actually click the notification's buttons, since they are owned by a different + // process and hence is not accessible through instrumentation, so send the select event + // instead. + final NotificationTransportControl.ListenerService service = + waitForCastNotificationService(notificationTransportControl); + assertNotNull("No notification service", service); + + ExpandedControllerActivity fullscreenControls = ActivityUtils.waitForActivityWithTimeout( + getInstrumentation(), + ExpandedControllerActivity.class, new Runnable() { + @Override + public void run() { + try { + service.getPendingIntent(ListenerService.ACTION_ID_SELECT).send(); + } catch (CanceledException e) { + fail(); + } + } + }, MAX_VIEW_TIME_MS); + assertNotNull("No expanded controller activity", fullscreenControls); + View rootView = fullscreenControls.findViewById(android.R.id.content); + assertNotNull("No root view for fullscreen controls", rootView); + assertEquals("Fullscreen controls not shown", View.VISIBLE, rootView.getVisibility()); + } +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastPositionTransferTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastPositionTransferTest.java new file mode 100644 index 0000000..ff44b17 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastPositionTransferTest.java @@ -0,0 +1,154 @@ +// Copyright 2014 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.remote; + +import android.graphics.Rect; +import android.test.suitebuilder.annotation.LargeTest; + +import org.chromium.base.test.util.DisabledTest; +import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.tab.Tab; +import org.chromium.content.browser.test.util.JavaScriptUtils; + +import java.util.Date; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Tests related to the transfer of the playback position between the local and + * the remote player. + */ +public class CastPositionTransferTest extends CastTestBase { + + /** Reference position in the video where we should start casting */ + private static final int CAST_START_TIME_MS = 3000; + + /** Max accepted error for position comparisons. */ + private static final int SEEK_EPSILON_MS = 250; + + /** Used to wait for the UI to properly respond. It is smaller than the default + * {@link CastTestBase#STABILIZE_TIME_MS} to make sure the video doesn't finish while waiting.*/ + private static final int SMALL_STABILIZE_TIME_MS = 250; + + + /** Returns the current time of the video, in milliseconds, by getting it from JavaScript. */ + private static long getLocalPositionMillis(Tab tab, String videoElementId) + throws InterruptedException, TimeoutException, NumberFormatException { + StringBuilder sb = new StringBuilder(); + sb.append("(function() {"); + sb.append(" var node = document.getElementById('" + videoElementId + "');"); + sb.append(" if (node) return node.currentTime;"); + sb.append(" return null"); + sb.append("})();"); + String jsResult = JavaScriptUtils.executeJavaScriptAndWaitForResult( + tab.getWebContents(), sb.toString()); + return Math.round(Double.parseDouble(jsResult) * 1000); + } + + private static void seekFromJS(Tab tab, long seekToMs) + throws InterruptedException, TimeoutException, NumberFormatException { + final long seekToSec = TimeUnit.MILLISECONDS.toSeconds(seekToMs); + StringBuilder sb = new StringBuilder(); + sb.append("(function() {"); + sb.append(" var node = document.getElementById('" + VIDEO_ELEMENT + "');"); + sb.append(" if (node) node.currentTime = " + seekToSec + ";"); + sb.append(" return 0;"); + sb.append("})();"); + JavaScriptUtils.executeJavaScriptAndWaitForResult(tab.getWebContents(), sb.toString()); + } + + /** Test for crbug.com/428409 */ + @DisabledTest +// BUG:http://crbug/517597 +// @Feature({"VideoFling"}) +// @LargeTest + public void testLocalToRemotePositionTransfer() throws InterruptedException, TimeoutException { + final Tab tab = getActivity().getActivityTab(); + final Rect videoRect = prepareDefaultVideofromPage(DEFAULT_VIDEO_PAGE, tab); + + // Jump to the position + seekFromJS(tab, CAST_START_TIME_MS); + final double localPositionMs = getLocalPositionMillis(tab, VIDEO_ELEMENT); + assertTrue("Local playback position (" + localPositionMs + ") did not advance past " + + CAST_START_TIME_MS, localPositionMs >= CAST_START_TIME_MS); + + // Start cast + final long castStartTimeMs = new Date().getTime(); + castVideoAndWaitUntilPlaying(CAST_TEST_ROUTE, tab, videoRect); + + // Test the position + final long remotePositionMs = getRemotePositionMs(); + final long castDelayMs = new Date().getTime() - castStartTimeMs; + + assertTrue("The remote playback position (" + remotePositionMs + ") did not advance past " + + CAST_START_TIME_MS, remotePositionMs >= CAST_START_TIME_MS); + assertTrue("The remote playback position (" + remotePositionMs + ") went too far.", + remotePositionMs <= CAST_START_TIME_MS + castDelayMs); + } + + /** Test for crbug.com/428409 */ + @Feature({"VideoFling"}) + @LargeTest + public void testRemoteToLocalPositionTransfer() throws InterruptedException, TimeoutException { + final Tab tab = getActivity().getActivityTab(); + final Rect videoRect = prepareDefaultVideofromPage(DEFAULT_VIDEO_PAGE, tab); + + // Start cast + castVideoAndWaitUntilPlaying(CAST_TEST_ROUTE, tab, videoRect); + + tapPlayPauseButton(tab, videoRect); + int pausePosition = getRemotePositionMs(); + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + Thread.sleep(VIEW_RETRY_MS); + int newPosition = getRemotePositionMs(); + if (newPosition == pausePosition) { + break; + } + pausePosition = newPosition; + } + // Jump to the position + seekFromJS(tab, CAST_START_TIME_MS); + for (int time = 0; time < MAX_VIEW_TIME_MS + && getRemotePositionMs() != pausePosition; time += VIEW_RETRY_MS) { + Thread.sleep(VIEW_RETRY_MS); + } + int remotePositionMs = getRemotePositionMs(); + assertEquals("The remote player did not seek", + CAST_START_TIME_MS, remotePositionMs, SEEK_EPSILON_MS); + + // Stop cast and play locally + final long castStopTimeMs = new Date().getTime(); + clickDisconnectFromRoute(tab, videoRect); + tapPlayPauseButton(tab, videoRect); + sleepNoThrow(SMALL_STABILIZE_TIME_MS); + + // Test the position + final double localPositionMs = getLocalPositionMillis(tab, VIDEO_ELEMENT); + final long castDelayMs = new Date().getTime() - castStopTimeMs; + assertTrue("The local playback position (" + localPositionMs + ") did not advance past " + + CAST_START_TIME_MS, localPositionMs >= CAST_START_TIME_MS); + assertTrue("The local playback position (" + localPositionMs + ") went too far.", + localPositionMs <= CAST_START_TIME_MS + castDelayMs); + } + + /** Test for crbug.com/425105 */ + @Feature({"VideoFling"}) + @LargeTest + public void testPositionUpdate() throws InterruptedException, TimeoutException { + final Tab tab = getActivity().getActivityTab(); + final Rect videoRect = prepareDefaultVideofromPage(DEFAULT_VIDEO_PAGE, tab); + castVideoAndWaitUntilPlaying(CAST_TEST_ROUTE, tab, videoRect); + + sleepNoThrow(CAST_START_TIME_MS); + + final long remotePositionMs = getRemotePositionMs(); + final long localPositionMs = getLocalPositionMillis(tab, VIDEO_ELEMENT); + + assertEquals("Remote playback position was not properly updated.", + CAST_START_TIME_MS, remotePositionMs, SEEK_EPSILON_MS); + assertEquals("The remote playback position was updated, but the local one was not.", + CAST_START_TIME_MS, localPositionMs, SEEK_EPSILON_MS); + } +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastReconnectTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastReconnectTest.java new file mode 100644 index 0000000..e3de4fe --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastReconnectTest.java @@ -0,0 +1,184 @@ +// Copyright 2014 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.remote; + +import android.app.Activity; +import android.app.Instrumentation; +import android.app.Instrumentation.ActivityMonitor; +import android.graphics.Rect; +import android.view.View; + +import org.chromium.base.test.util.DisabledTest; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.tab.Tab; +import org.chromium.chrome.test.util.ActivityUtils; +import org.chromium.chrome.test.util.TestHttpServerClient; +import org.chromium.content.browser.test.util.DOMUtils; +import org.chromium.content_public.browser.WebContents; + +import java.util.concurrent.TimeoutException; + +/** + * Test casting when Chrome shuts down and restarts. + * + * These tests are run from the python wrapper test/host_driven_tests/ReconnectCastTest.py. They + * will not run correctly in isolation, and hence are marked as disabled to prevent the test runner + * from running them automatically outside the wrapper. + */ +public class CastReconnectTest extends CastTestBase { + + private int mVideoDurationMs; + + /** + * Cast and pause a video. This doesn't test anything itself, but just sets up the state for + * when we restart Chrome + * + * @throws InterruptedException + * @throws TimeoutException + */ + @DisabledTest + public void testReconnectSetup() throws InterruptedException, TimeoutException { + castAndPauseDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + } + + /** + * Test the state after reconnection, and confirm that pressing play starts the video. + * + * @throws InterruptedException + * @throws TimeoutException + */ + @DisabledTest + public void testVideoContinuing() throws InterruptedException, TimeoutException { + // TODO(aberent) Check that the notification is still present. Will fail at + // the moment, and is difficult to do with the way the notification service + // currently works. + + // Put the video into fullscreen and check that it casts automatically + final Tab tab = getActivity().getActivityTab(); + + WebContents webContents = tab.getWebContents(); + waitUntilVideoReady(VIDEO_ELEMENT, webContents); + + final Rect videoRect = DOMUtils.getNodeBounds(webContents, VIDEO_ELEMENT); + ExpandedControllerActivity fullscreenControls = ActivityUtils.waitForActivityWithTimeout( + getInstrumentation(), ExpandedControllerActivity.class, new Runnable() { + @Override + public void run() { + tapVideoFullscreenButton(tab, videoRect); + } + }, MAX_VIEW_TIME_MS); + assertNotNull("Fullscreen controls not recreated", fullscreenControls); + View pauseButton = fullscreenControls.findViewById(R.id.pause); + assertNotNull("No pause/play button", pauseButton); + clickButton(pauseButton); + + // Give it time to start + Thread.sleep(STABILIZE_TIME_MS); + + checkVideoStarted(DEFAULT_VIDEO); + } + + /** + * Test going into fullscreen on a new video after reconnection. + * + * @throws InterruptedException + * @throws TimeoutException + */ + @DisabledTest + public void testNewVideo() throws InterruptedException, TimeoutException { + + // Load a new video on the page + loadUrl(TestHttpServerClient.getUrl(TEST_VIDEO_PAGE_2)); + + // Put the video into fullscreen + final Tab tab = getActivity().getActivityTab(); + + WebContents webContents = tab.getWebContents(); + waitUntilVideoReady(VIDEO_ELEMENT, webContents); + + final Rect videoRect = DOMUtils.getNodeBounds(webContents, VIDEO_ELEMENT); + + // Video should cast automatically + ExpandedControllerActivity fullscreenControls = ActivityUtils.waitForActivityWithTimeout( + getInstrumentation(), ExpandedControllerActivity.class, new Runnable() { + @Override + public void run() { + tapVideoFullscreenButton(tab, videoRect); + } + }, MAX_VIEW_TIME_MS); + assertNotNull("Fullscreen controls not recreated", fullscreenControls); + + // Wait for it to start (may take some time, hence use a busy wait) + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + if (isPlayingRemotely()) break; + sleepNoThrow(VIEW_RETRY_MS); + } + + checkVideoStarted(TEST_VIDEO_2); + } + + /** + * Cast a video but don't pause it. Use for testing reconnection after video finished + * + * @throws InterruptedException + * @throws TimeoutException + */ + @DisabledTest + public void testVideoFinishedSetup() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + // Get the duration of the video + mVideoDurationMs = getRemoteDurationMs(); + } + + /** + * Test that once the video has finished we don't automatically reconnect. + * + * @throws InterruptedException + * @throws TimeoutException + */ + @DisabledTest + public void testVideoFinished() throws InterruptedException, TimeoutException { + // Give the video time to finish (actually will probably have already finished, + // but this makes sure) + sleepNoThrow(mVideoDurationMs); + + // TODO(aberent) Check that the notification has gone away. Difficult to do until + // the notification handling is refactored. + + // Put the video into fullscreen and check that the fullscreen controls are not recreated + Tab tab = getActivity().getActivityTab(); + + WebContents webContents = tab.getWebContents(); + waitUntilVideoReady(VIDEO_ELEMENT, webContents); + + Rect videoRect = DOMUtils.getNodeBounds(webContents, VIDEO_ELEMENT); + Instrumentation instrumentation = getInstrumentation(); + ActivityMonitor monitor = instrumentation.addMonitor( + ExpandedControllerActivity.class.getCanonicalName(), null, false); + + tapVideoFullscreenButton(tab, videoRect); + + instrumentation.waitForIdleSync(); + Activity fullScreenControls = monitor.getLastActivity(); + if (fullScreenControls == null) { + fullScreenControls = monitor.waitForActivityWithTimeout(MAX_VIEW_TIME_MS); + } + assertNull(fullScreenControls); + } + + + @Override + protected void setUp() throws Exception { + mSkipClearAppData = true; + super.setUp(); + } + + @Override + public void startMainActivity() throws InterruptedException { + startMainActivityFromLauncher(); + } + +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastStartStopTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastStartStopTest.java new file mode 100644 index 0000000..cd4c7ea --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastStartStopTest.java @@ -0,0 +1,89 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent.CanceledException; +import android.graphics.Rect; +import android.test.suitebuilder.annotation.LargeTest; + +import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.media.remote.NotificationTransportControl.ListenerService; +import org.chromium.chrome.browser.tab.Tab; + +import java.util.concurrent.TimeoutException; + +/** + * Simple tests of casting videos. These tests all use the same page, containing the same non-YT + * video. + */ +public class CastStartStopTest extends CastTestBase { + + /* + * Test that we can cast a video, and that we get the ExpandedControllerActivity when we do. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testCastingGenericVideo() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + checkVideoStarted(DEFAULT_VIDEO); + } + + /* + * Test that we can disconnect a cast session from the expanded controller activity overlay. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testStopFromVideoControls() throws InterruptedException, TimeoutException { + Rect videoRect = castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + final Tab tab = getActivity().getActivityTab(); + + clickDisconnectFromRoute(tab, videoRect); + + checkDisconnected(); + } + + /* + * Test that we can stop a cast session from the notification. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testStopFromNotification() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + // Get the notification + NotificationTransportControl notificationTransportControl = waitForCastNotification(); + + // We can't actually click the notification's stop button, since it is owned by a different + // process and hence is not accessible through instrumentation, so send the stop event + // instead. + NotificationTransportControl.ListenerService service = + waitForCastNotificationService(notificationTransportControl); + + try { + service.getPendingIntent(ListenerService.ACTION_ID_STOP).send(); + } catch (CanceledException e) { + fail(); + } + checkDisconnected(); + } + + /* + * Test that a cast session disconnects when the video ends + */ + @Feature({"VideoFling"}) + @LargeTest + public void testStopWhenVideoEnds() throws InterruptedException, TimeoutException { + castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + // Wait for the video to finish (this assumes the video is short, the test video + // is 8 seconds). + sleepNoThrow(STABILIZE_TIME_MS); + + Thread.sleep(getRemoteDurationMs()); + + // Everything should now have disconnected + checkDisconnected(); + } +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastSwitchVideoTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastSwitchVideoTest.java new file mode 100644 index 0000000..705a5e5 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastSwitchVideoTest.java @@ -0,0 +1,108 @@ +// Copyright 2014 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.remote; + +import android.test.FlakyTest; +import android.test.suitebuilder.annotation.LargeTest; + +import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.tab.Tab; +import org.chromium.chrome.browser.util.FeatureUtilities; +import org.chromium.chrome.test.util.TestHttpServerClient; +import org.chromium.content.browser.test.util.DOMUtils; +import org.chromium.content_public.browser.WebContents; + +import java.util.concurrent.TimeoutException; + +/** + * Test switching videos when casting + */ +public class CastSwitchVideoTest extends CastTestBase { + + private static final String VIDEO_ELEMENT_2 = "video2"; + + @Feature({"VideoFling"}) + // Appears to be flaky, see crbug.com/515085 + // @LargeTest + @FlakyTest + public void testNewVideoInNewTab() throws InterruptedException, TimeoutException { + // This won't currently work in document mode because we can't create new tabs + if (FeatureUtilities.isDocumentMode(getActivity())) return; + checkSwitchVideo(DEFAULT_VIDEO_PAGE, new Runnable() { + @Override + public void run() { + try { + loadUrlInNewTab(TestHttpServerClient.getUrl(TEST_VIDEO_PAGE_2)); + playVideoFromCurrentTab(VIDEO_ELEMENT); + } catch (Exception e) { + fail("Failed to start second video; " + e.getMessage()); + } + } + }); + } + + @Feature({"VideoFling"}) + @LargeTest + public void testNewVideoNewPageSameTab() throws InterruptedException, TimeoutException { + checkSwitchVideo(DEFAULT_VIDEO_PAGE, new Runnable() { + @Override + public void run() { + try { + loadUrl(TestHttpServerClient.getUrl(TEST_VIDEO_PAGE_2)); + playVideoFromCurrentTab(VIDEO_ELEMENT); + } catch (Exception e) { + fail("Failed to start second video; " + e.getMessage()); + } + } + }); + } + + @Feature({"VideoFling"}) + @LargeTest + public void testTwoVideosSamePage() throws InterruptedException, TimeoutException { + checkSwitchVideo(TWO_VIDEO_PAGE, new Runnable() { + @Override + public void run() { + try { + playVideoFromCurrentTab(VIDEO_ELEMENT_2); + } catch (Exception e) { + fail("Failed to start second video; " + e.getMessage()); + } + } + }); + } + + private void checkSwitchVideo(String firstVideoPage, final Runnable startSecondVideo) + throws InterruptedException, TimeoutException { + // TODO(aberent) Checking position is flaky, because it is timing dependent, but probably + // a good idea in principle. Need to find a way of unflaking it. + // int position = castAndPauseDefaultVideoFromPage(firstVideoPage); + castAndPauseDefaultVideoFromPage(firstVideoPage); + + startSecondVideo.run(); + + // Check that we switch to playing the right video + checkVideoStarted(TEST_VIDEO_2); + + // Check we are back at the start of the video + RemoteMediaPlayerController controller = RemoteMediaPlayerController.getIfExists(); + assertNotNull("No controller", controller); + + // TODO(aberent) Check position. + // assertTrue("Position in video wrong", getPosition() < position); + } + + private void playVideoFromCurrentTab(String videoElement) throws InterruptedException, + TimeoutException { + // Start playing the video by tapping at its centre + final Tab tab = getActivity().getActivityTab(); + WebContents webContents = tab.getWebContents(); + + waitUntilVideoReady(videoElement, webContents); + + DOMUtils.clickNode(this, tab.getContentViewCore(), videoElement); + } + +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastTestBase.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastTestBase.java new file mode 100644 index 0000000..6f45e86 --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastTestBase.java @@ -0,0 +1,605 @@ +// Copyright 2014 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.remote; + +import android.app.Dialog; +import android.app.Instrumentation; +import android.graphics.Rect; +import android.os.SystemClock; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentManager; +import android.view.MotionEvent; +import android.view.View; + +import junit.framework.Assert; + +import org.chromium.base.Log; +import org.chromium.base.ThreadUtils; +import org.chromium.chrome.R; +import org.chromium.chrome.browser.ChromeActivity; +import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState; +import org.chromium.chrome.browser.tab.Tab; +import org.chromium.chrome.test.ChromeActivityTestCaseBase; +import org.chromium.chrome.test.util.TestHttpServerClient; +import org.chromium.content.browser.ContentViewCore; +import org.chromium.content.browser.test.util.DOMUtils; +import org.chromium.content.browser.test.util.JavaScriptUtils; +import org.chromium.content.browser.test.util.TestTouchUtils; +import org.chromium.content.browser.test.util.UiUtils; +import org.chromium.content_public.browser.WebContents; + +import java.util.ArrayList; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeoutException; + +/** + * Base class for tests of Clank Cast. Contains functions for setting up a cast connection and other + * utility functions. + */ +public abstract class CastTestBase extends ChromeActivityTestCaseBase<ChromeActivity> { + private class TestListener implements MediaRouteController.UiListener { + @Override + public void onRouteSelected(String name, MediaRouteController mediaRouteController) { + } + + @Override + public void onRouteUnselected(MediaRouteController mediaRouteController) { + } + + @Override + public void onPrepared(MediaRouteController mediaRouteController) { + } + + @Override + public void onError(int errorType, String message) { + } + + @Override + public void onPlaybackStateChanged(PlayerState oldState, PlayerState newState) { + if (newState == PlayerState.PLAYING) { + mPlaying = true; + } else if (newState != PlayerState.PLAYING) { + mPlaying = false; + } + } + + @Override + public void onDurationUpdated(int durationMillis) { + } + + @Override + public void onPositionChanged(int positionMillis) { + } + + @Override + public void onTitleChanged(String title) { + } + + } + + // The name of the route provided by the dummy cast device. + protected static final String CAST_TEST_ROUTE = "Cast Test Route"; + + // URLs of the default test page and video. + protected static final String DEFAULT_VIDEO_PAGE = + "chrome/test/data/android/media/simple_video.html"; + protected static final String DEFAULT_VIDEO = "chrome/test/data/android/media/test.mp4"; + + // Constants used to find the default video and maximise button on the page + protected static final String VIDEO_ELEMENT = "video"; + + // Max time to open a view. + protected static final int MAX_VIEW_TIME_MS = 10000; + + // Time to let a video run to ensure that it has started. + protected static final int RUN_TIME_MS = 1000; + // Time to allow for the UI to react to video controls, + protected static final int STABILIZE_TIME_MS = 3000; + + // Retry interval when looking for a view. + protected static final int VIEW_RETRY_MS = 100; + + protected static final String TEST_VIDEO_PAGE_2 = + "chrome/test/data/android/media/simple_video2.html"; + + protected static final String TEST_VIDEO_2 = "chrome/test/data/android/media/test2.mp4"; + + protected static final String TWO_VIDEO_PAGE = "chrome/test/data/android/media/two_videos.html"; + + private static final String TAG = "CastTestBase"; + + private boolean mPlaying = false; + + public CastTestBase() { + super(ChromeActivity.class); + } + + @Override + public void startMainActivity() throws InterruptedException { + startMainActivityOnBlankPage(); + } + + protected void castAndPauseDefaultVideoFromPage(String pagePath) throws InterruptedException, + TimeoutException { + Rect videoRect = castDefaultVideoFromPage(pagePath); + + final Tab tab = getActivity().getActivityTab(); + + Rect pauseButton = playPauseButton(videoRect); + + // Make sure the video has made some progress + Thread.sleep(RUN_TIME_MS); + + tapButton(tab, pauseButton); + assertTrue("Not paused", waitForState(PlayerState.PAUSED)); + } + + private boolean videoReady(String videoElement, WebContents webContents) { + // Create a javascript function to check if the video meta-data has been loaded. + StringBuilder sb = new StringBuilder(); + sb.append("(function() {"); + sb.append(" var node = document.getElementById('" + videoElement + "');"); + sb.append(" if (!node) return null;"); + // Any video readyState value greater than 0 means that at least the meta-data has been + // loaded but we also need the a document readyState of complete to ensure that page has + // been laid out with the correct video size, and everything is drawn. + sb.append(" return node.readyState > 0 && document.readyState == 'complete';"); + sb.append("})();"); + String javascriptResult; + try { + javascriptResult = JavaScriptUtils.executeJavaScriptAndWaitForResult( + webContents, sb.toString()); + Assert.assertFalse("Failed to retrieve contents for " + videoElement, + javascriptResult.trim().equalsIgnoreCase("null")); + + Boolean ready = javascriptResult.trim().equalsIgnoreCase("true"); + return ready; + } catch (InterruptedException e) { + Assert.fail("Interrupted"); + } catch (TimeoutException e) { + Assert.fail("Javascript execution timed out"); + } + return false; + } + + protected void waitUntilVideoReady(String videoElement, WebContents webContents) { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + try { + if (videoReady(videoElement, webContents)) return; + } catch (Exception e) { + fail(e.toString()); + } + sleepNoThrow(VIEW_RETRY_MS); + } + Assert.fail("Video not ready"); + } + + protected Rect prepareDefaultVideofromPage(String pagePath, Tab currentTab) + throws InterruptedException, TimeoutException { + // Ensure that we don't try to reconnect + RemotePlaybackSettings.setShouldReconnectToRemote(getActivity(), false); + + loadUrl(TestHttpServerClient.getUrl(pagePath)); + + WebContents webContents = currentTab.getWebContents(); + + waitUntilVideoReady(VIDEO_ELEMENT, webContents); + + return DOMUtils.getNodeBounds(webContents, VIDEO_ELEMENT); + } + + protected Rect castDefaultVideoFromPage(String pagePath) + throws InterruptedException, TimeoutException { + final Tab tab = getActivity().getActivityTab(); + final Rect videoRect = prepareDefaultVideofromPage(pagePath, tab); + + castVideoAndWaitUntilPlaying(CAST_TEST_ROUTE, tab, videoRect); + + return videoRect; + } + + protected void castVideoAndWaitUntilPlaying(final String chromecastName, final Tab tab, + final Rect videoRect) { + castVideo(chromecastName, tab, videoRect); + + assertTrue("Video didn't start playing", waitUntilPlaying()); + + } + + protected void castVideo(final String chromecastName, final Tab tab, final Rect videoRect) { + Log.i(TAG, "castVideo, videoRect = " + videoRect); + + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + RemoteMediaPlayerController playerController = + RemoteMediaPlayerController.instance(); + + // Because of the way we fake YouTube this will get the YouTube controller if we are + // testing YouTube. + MediaRouteController routeController = playerController.getMediaRouteController( + TestHttpServerClient.getUrl(DEFAULT_VIDEO), + TestHttpServerClient.getUrl(DEFAULT_VIDEO_PAGE)); + assertNotNull("Could not get MediaRouteController", routeController); + routeController.addUiListener(new TestListener()); + } + }); + tapCastButton(tab, videoRect); + + // Wait for the test device to appear in the device list. + try { + UiUtils.settleDownUI(getInstrumentation()); + } catch (InterruptedException e) { + fail(); + } + + View testRouteButton = waitForRouteButton(chromecastName); + assertNotNull("Test route not found", testRouteButton); + + mouseSingleClickView(getInstrumentation(), testRouteButton); + } + + protected View waitForRouteButton(final String chromecastName) { + return waitForView(new Callable<View>() { + @Override + public View call() { + FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm == null) return null; + DialogFragment mediaRouteListFragment = (DialogFragment) fm.findFragmentByTag( + "android.support.v7.mediarouter:MediaRouteChooserDialogFragment"); + if (mediaRouteListFragment == null || mediaRouteListFragment.getDialog() == null) { + return null; + } + View mediaRouteList = + mediaRouteListFragment.getDialog().findViewById(R.id.mr_chooser_list); + if (mediaRouteList == null) return null; + ArrayList<View> routesWanted = new ArrayList<View>(); + mediaRouteList.findViewsWithText(routesWanted, chromecastName, + View.FIND_VIEWS_WITH_TEXT); + if (routesWanted.size() == 0) return null; + + return routesWanted.get(0); + } + }, MAX_VIEW_TIME_MS); + } + + protected void checkDisconnected() { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + if (isDisconnected()) break; + sleepNoThrow(VIEW_RETRY_MS); + } + // Could use assertTrue(isDisconnected()) here, but retesting the individual aspects of + // disconnection gives more specific error messages. + NotificationTransportControl notificationTransportControl = + NotificationTransportControl.getIfExists(); + if (notificationTransportControl != null && notificationTransportControl.isShowing()) { + fail("Failed to close notification"); + } + assertEquals("Video still playing?", null, + RemotePlaybackSettings.getUriPlaying(getActivity())); + assertTrue("RemoteMediaPlayerController not stopped", !isPlayingRemotely()); + } + + protected void clickDisconnectFromRoute(Tab tab, Rect videoRect) { + // Click on the cast control button to stop casting + tapCastButton(tab, videoRect); + + // Wait for the disconnect button + final View disconnectButton = waitForView(new Callable<View>() { + @Override + public View call() { + FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm == null) return null; + DialogFragment mediaRouteControllerFragment = (DialogFragment) fm.findFragmentByTag( + "android.support.v7.mediarouter:MediaRouteControllerDialogFragment"); + if (mediaRouteControllerFragment == null) return null; + Dialog dialog = mediaRouteControllerFragment.getDialog(); + if (dialog == null) return null; + // The stop button (previously called disconnect) simply uses 'button1' in the + // latest version of the support library. See: + // https://cs.corp.google.com/#android/frameworks/support/v7/mediarouter/src/android/support/v7/app/MediaRouteControllerDialog.java&l=90. + // TODO(aberent) remove dependency on internals of support library + // https://crbug/548599 + return dialog.findViewById(android.R.id.button1); + } + }, MAX_VIEW_TIME_MS); + + assertNotNull("No disconnect button", disconnectButton); + + clickButton(disconnectButton); + } + + /* + * Check that a (non-YouTube) video has started playing, and that all the controls have been + * correctly set up. + */ + protected void checkVideoStarted(String testVideo) { + // Check we have a notification + NotificationTransportControl notificationTransportControl = waitForCastNotification(); + assertNotNull("No notification controller", notificationTransportControl); + waitForCastNotificationService(notificationTransportControl); + assertTrue("No notification", notificationTransportControl.isShowing()); + // Check that we are playing the right video + waitUntilVideoCurrent(testVideo); + assertEquals("Wrong video playing", TestHttpServerClient.getUrl(testVideo), + RemotePlaybackSettings.getUriPlaying(getActivity())); + + // Check that the RemoteMediaPlayerController and the (YouTube)MediaRouteController have + // been set up correctly + waitUntilPlaying(); + RemoteMediaPlayerController playerController = RemoteMediaPlayerController.getIfExists(); + assertNotNull("No RemoteMediaPlayerController", playerController); + assertTrue("Video not playing", isPlayingRemotely()); + assertTrue("Wrong sort of MediaRouteController", (playerController + .getCurrentlyPlayingMediaRouteController() instanceof DefaultMediaRouteController)); + } + + /** + * Click a button. Unlike {@link CastTestBase#mouseSingleClickView} this directly accesses the + * view and does not send motion events though the message queue. As such it doesn't require the + * view to have been created by the instrumented activity, but gives less flexibility than + * mouseSingleClickView. For example, if the view is hierachical, then clickButton will always + * act on specified view, whereas mouseSingleClickView will send the events to the appropriate + * child view. It is hence only really appropriate for simple views such as buttons. + * + * @param button the button to be clicked. + */ + protected void clickButton(final View button) { + ThreadUtils.runOnUiThreadBlocking(new Runnable() { + @Override + public void run() { + // Post the actual click to the button's message queue, to ensure that it has been + // inflated before the click is received. + button.post(new Runnable() { + @Override + public void run() { + button.performClick(); + } + }); + } + }); + } + + protected void sleepNoThrow(int timeout) { + try { + Thread.sleep(timeout); + } catch (InterruptedException e) { + fail(e.toString()); + } + } + + protected void tapVideoFullscreenButton(final Tab tab, final Rect videoRect) { + tapButton(tab, fullscreenButton(videoRect)); + } + + protected void tapCastButton(final Tab tab, final Rect videoRect) { + tapButton(tab, castButton(videoRect)); + } + + protected void tapPlayPauseButton(final Tab tab, final Rect videoRect) { + tapButton(tab, playPauseButton(videoRect)); + } + + protected View waitForView(Callable<View> getViewCallable, int timeoutMs) { + for (int time = 0; time < timeoutMs; time += VIEW_RETRY_MS) { + try { + View result = getViewCallable.call(); + if (result != null) return result; + } catch (Exception e) { + fail(e.toString()); + } + sleepNoThrow(VIEW_RETRY_MS); + } + return null; + } + + protected NotificationTransportControl waitForCastNotification() { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + NotificationTransportControl result = NotificationTransportControl.getIfExists(); + if (result != null) { + return result; + } + sleepNoThrow(VIEW_RETRY_MS); + } + return null; + } + + protected NotificationTransportControl.ListenerService waitForCastNotificationService( + NotificationTransportControl notification) { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + NotificationTransportControl.ListenerService service = notification.getService(); + if (service != null) { + return service; + } + sleepNoThrow(VIEW_RETRY_MS); + } + return null; + + } + + protected boolean waitForState(RemoteVideoInfo.PlayerState state) { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + RemoteMediaPlayerController playerController = RemoteMediaPlayerController + .getIfExists(); + if (playerController != null + && playerController.getCurrentlyPlayingMediaRouteController() != null + && playerController.getCurrentlyPlayingMediaRouteController().getPlayerState() + == state) { + return true; + } + sleepNoThrow(VIEW_RETRY_MS); + } + return false; + } + + protected boolean waitUntilPlaying() { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + if (mPlaying) return true; + sleepNoThrow(VIEW_RETRY_MS); + } + return false; + } + + private boolean isDisconnected() { + NotificationTransportControl notificationTransportControl = + NotificationTransportControl.getIfExists(); + if (notificationTransportControl != null && notificationTransportControl.isShowing()) { + return false; + } + if (RemotePlaybackSettings.getUriPlaying(getActivity()) != null) return false; + return !isPlayingRemotely(); + } + + private boolean waitUntilVideoCurrent(String testVideo) { + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + if (TestHttpServerClient.getUrl(testVideo).equals( + RemotePlaybackSettings.getUriPlaying(getActivity()))) { + return true; + } + sleepNoThrow(VIEW_RETRY_MS); + } + return false; + } + + protected int getRemotePositionMs() { + return getMediaRouteController().getPosition(); + } + + protected int getRemoteDurationMs() { + return getMediaRouteController().getDuration(); + } + + protected boolean isPlayingRemotely() { + RemoteMediaPlayerController playerController = RemoteMediaPlayerController.getIfExists(); + if (playerController == null) return false; + MediaRouteController routeController = playerController + .getCurrentlyPlayingMediaRouteController(); + if (routeController == null) return false; + return routeController.isPlaying(); + } + + protected MediaRouteController getMediaRouteController() { + RemoteMediaPlayerController playerController = RemoteMediaPlayerController.getIfExists(); + assertNotNull("No RemoteMediaPlayerController", playerController); + MediaRouteController routeController = playerController + .getCurrentlyPlayingMediaRouteController(); + assertNotNull("No MediaRouteController", routeController); + return routeController; + } + + /* + * Functions to find the controls Unfortunately the controls are invisible to the code outside + * Blink, so this is highly dependent on the geometry defined in Blink css (see + * MediaControls.css & MediaControlsAndroid.css). + */ + private static final int CONTROLS_HEIGHT = 35; + private static final int BUTTON_WIDTH = 35; + private static final int CONTROL_BAR_MARGIN = 5; + private static final int BUTTON_RIGHT_MARGIN = 9; + private static final int PLAY_BUTTON_LEFT_MARGIN = 9; + private static final int FULLSCREEN_BUTTON_LEFT_MARGIN = -5; + + private Rect controlBar(Rect videoRect) { + int left = videoRect.left + CONTROL_BAR_MARGIN; + int right = videoRect.right - CONTROL_BAR_MARGIN; + int bottom = videoRect.bottom - CONTROL_BAR_MARGIN; + int top = videoRect.bottom - CONTROLS_HEIGHT; + return new Rect(left, top, right, bottom); + } + + private Rect playPauseButton(Rect videoRect) { + Rect bar = controlBar(videoRect); + int left = bar.left + PLAY_BUTTON_LEFT_MARGIN; + int right = left + BUTTON_WIDTH; + return new Rect(left, bar.top, right, bar.bottom); + } + + private Rect fullscreenButton(Rect videoRect) { + Rect bar = controlBar(videoRect); + int right = bar.right - BUTTON_RIGHT_MARGIN; + int left = right - BUTTON_WIDTH; + return new Rect(left, bar.top, right, bar.bottom); + } + + private Rect castButton(Rect videoRect) { + Rect fullscreenButton = fullscreenButton(videoRect); + int right = fullscreenButton.left - BUTTON_RIGHT_MARGIN - FULLSCREEN_BUTTON_LEFT_MARGIN; + int left = right - BUTTON_WIDTH; + return new Rect(left, fullscreenButton.top, right, fullscreenButton.bottom); + } + + private void tapButton(Tab tab, Rect rect) { + ContentViewCore core = tab.getContentViewCore(); + int clickX = + (int) core.getRenderCoordinates().fromLocalCssToPix( + ((float) (rect.left + rect.right)) / 2) + + core.getViewportSizeOffsetWidthPix(); + int clickY = + (int) core.getRenderCoordinates().fromLocalCssToPix( + ((float) (rect.top + rect.bottom)) / 2) + + core.getViewportSizeOffsetHeightPix(); + // Click using a virtual mouse, since a touch may result in a disambiguation pop-up. + mouseSingleClickView(getInstrumentation(), tab.getView(), clickX, clickY); + } + + private static void sendMouseAction(Instrumentation instrumentation, int action, long downTime, + float x, float y) { + long eventTime = SystemClock.uptimeMillis(); + MotionEvent.PointerCoords coords[] = new MotionEvent.PointerCoords[1]; + coords[0] = new MotionEvent.PointerCoords(); + coords[0].x = x; + coords[0].y = y; + MotionEvent.PointerProperties properties[] = new MotionEvent.PointerProperties[1]; + properties[0] = new MotionEvent.PointerProperties(); + properties[0].id = 0; + properties[0].toolType = MotionEvent.TOOL_TYPE_MOUSE; + MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, 1, properties, coords, + 0, 0, 0.0f, 0.0f, 0, 0, 0, 0); + instrumentation.sendPointerSync(event); + instrumentation.waitForIdleSync(); + } + + /** + * Sends (synchronously) a single mosue click to an absolute screen coordinates. + * + * @param instrumentation Instrumentation object used by the test. + * @param x Screen absolute x location. + * @param y Screen absolute y location. + */ + private static void mouseSingleClick(Instrumentation instrumentation, float x, float y) { + long downTime = SystemClock.uptimeMillis(); + sendMouseAction(instrumentation, MotionEvent.ACTION_DOWN, downTime, x, y); + sendMouseAction(instrumentation, MotionEvent.ACTION_UP, downTime, x, y); + } + + /** + * Sends (synchronously) a single mouse click to the View at the specified coordinates. + * + * @param instrumentation Instrumentation object used by the test. + * @param v The view the coordinates are relative to. + * @param x Relative x location to the view. + * @param y Relative y location to the view. + */ + private static void mouseSingleClickView(Instrumentation instrumentation, View v, int x, + int y) { + int location[] = TestTouchUtils.getAbsoluteLocationFromRelative(v, x, y); + int absoluteX = location[0]; + int absoluteY = location[1]; + mouseSingleClick(instrumentation, absoluteX, absoluteY); + } + + /** + * Sends (synchronously) a single mouse click to the center of the View. + * + * @param instrumentation Instrumentation object used by the test. + * @param v The view the coordinates are relative to. + */ + private static void mouseSingleClickView(Instrumentation instrumentation, View v) { + int x = v.getWidth() / 2; + int y = v.getHeight() / 2; + mouseSingleClickView(instrumentation, v, x, y); + } + +} diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastVideoControlsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastVideoControlsTest.java new file mode 100644 index 0000000..3ea1a7d --- /dev/null +++ b/chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastVideoControlsTest.java @@ -0,0 +1,53 @@ +// Copyright 2014 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.remote; + +import android.graphics.Rect; +import android.test.suitebuilder.annotation.LargeTest; + +import org.chromium.base.test.util.Feature; +import org.chromium.chrome.browser.tab.Tab; + +import java.util.concurrent.TimeoutException; + +/** + * Instrumentation tests for the fullscreen cast controls. + */ +public class CastVideoControlsTest extends CastTestBase { + + private static final long PAUSE_TEST_TIME_MS = 1000; + + /* + * Test the pause button. + */ + @Feature({"VideoFling"}) + @LargeTest + public void testPauseButton() throws InterruptedException, TimeoutException { + Rect videoRect = castDefaultVideoFromPage(DEFAULT_VIDEO_PAGE); + + final Tab tab = getActivity().getActivityTab(); + + tapPlayPauseButton(tab, videoRect); + // The new position is sent in a separate message, so we have to wait a bit before + // fetching it. + int position = getRemotePositionMs(); + boolean paused = false; + for (int time = 0; time < MAX_VIEW_TIME_MS; time += VIEW_RETRY_MS) { + Thread.sleep(VIEW_RETRY_MS); + int newPosition = getRemotePositionMs(); + if (newPosition == position) { + paused = true; + break; + } + position = newPosition; + } + // Check we have paused before the end of the video (with a fudge factor for timing + // variation) + assertTrue("Pause didn't stop playback", paused || position < getRemoteDurationMs() - 100); + tapPlayPauseButton(tab, videoRect); + Thread.sleep(PAUSE_TEST_TIME_MS); + assertTrue("Run didn't restart playback", position < getRemotePositionMs()); + } +} diff --git a/chrome/chrome_test_support.gypi b/chrome/chrome_test_support.gypi new file mode 100644 index 0000000..b90bf11 --- /dev/null +++ b/chrome/chrome_test_support.gypi @@ -0,0 +1,69 @@ +# Copyright 2015 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. + +# Create a test support APK, and allow a test apk to depend on it, without the test +# apk incorporating the test support apk's Java code. +# +# Inputs: +# test_support_apk_name - the name of the test support apk +# test_support_apk_target - a GYP target name to use internally in this file. +# test_support_apk_manifest_path - the full path of the manifest file for the test +# support apk +# test_support_dependencies - The dependencies of the test support APK. This should +# include all the APK's code. +# +# The corresponding test apk should depend on "require_<(test_support_apk_target)" +# It should not depend on "<(test_support_apk_target)" since, if it does, the test +# apk will incorporate the code of the test support apk. +{ + 'conditions': [ + ['OS=="android"', { + 'variables' : { + 'test_support_apk_path': '<(PRODUCT_DIR)/apks/<(test_support_apk_name).apk' + }, + 'targets': [ + { + 'target_name': '<(test_support_apk_target)', + 'type': 'none', + 'dependencies': ['<@(test_support_apk_dependencies)',], + 'variables': { + 'apk_name': '<(test_support_apk_name)', + 'final_apk_path': '<(test_support_apk_path)', + 'java_in_dir': '<(DEPTH)/chrome/android/javatests', + 'java_in_dir_suffix': '/src_dummy', + 'android_manifest_path': '<(test_support_apk_manifest_path)', + }, + 'includes': [ + '../build/java_apk.gypi', + ], + }, + { + # This emulates gn's datadeps fields, allowing other APKs to declare + # that they require that this APK be built without including the + # test_support's code. + 'target_name': 'require_<(test_support_apk_target)', + 'type': 'none', + 'actions': [ + { + 'action_name': 'require_<(test_support_apk_name)', + 'message': 'Making sure <(test_support_apk_path) has been built.', + 'variables': { + 'required_file': '<(PRODUCT_DIR)/test_support_apk/<(test_support_apk_name).apk.required', + }, + 'inputs': [ + '<(test_support_apk_path)', + ], + 'outputs': [ + '<(required_file)', + ], + 'action': [ + 'python', '<(DEPTH)/build/android/gyp/touch.py', '<(required_file)', + ], + }, + ], + }, + ], + }], + ], +} diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 63c5bed..8ba0756 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -3119,6 +3119,16 @@ ], 'conditions': [ ['OS == "android"', { + 'variables' : { + 'test_support_apk_target' : 'chrome_public_test_support_apk', + 'test_support_apk_name' : 'ChromePublicTestSupport', + 'test_support_apk_manifest_path' : '../chrome/test/android/chrome_public_test_support/AndroidManifest.xml', + 'test_support_apk_dependencies' : ['cast_emulator',], + + }, + 'includes' : [ + 'chrome_test_support.gypi', + ], 'targets': [ { # GN: //chrome/android:chrome_junit_tests @@ -3173,6 +3183,21 @@ ], 'includes': [ '../build/java.gypi' ], }, + { + # GN: //chrome/test/android/cast_emulator:cast_emulator + 'target_name': 'cast_emulator', + 'type': 'none', + 'dependencies': [ + '../base/base.gyp:base_java', + '../third_party/android_tools/android_tools.gyp:android_support_v7_appcompat_javalib', + '../third_party/android_tools/android_tools.gyp:android_support_v7_mediarouter_javalib', + '../third_party/android_tools/android_tools.gyp:google_play_services_javalib', + ], + 'variables': { + 'java_in_dir': '../chrome/test/android/cast_emulator', + }, + 'includes': [ '../build/java.gypi' ], + }, ], 'conditions': [ ['test_isolation_mode != "noop"', diff --git a/chrome/test/android/cast_emulator/BUILD.gn b/chrome/test/android/cast_emulator/BUILD.gn new file mode 100644 index 0000000..9545047 --- /dev/null +++ b/chrome/test/android/cast_emulator/BUILD.gn @@ -0,0 +1,26 @@ +# Copyright 2015 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. + +import("//build/config/android/config.gni") +import("//build/config/android/rules.gni") + +# GYP: //clank/native/clank.gyp:cast_emulator +android_library("cast_emulator") { + chromium_code = true + + java_files = [ + "src/org/chromium/chrome/browser/media/remote/DummyPlayer.java", + "src/org/chromium/chrome/browser/media/remote/LocalSessionManager.java", + "src/org/chromium/chrome/browser/media/remote/MediaItem.java", + "src/org/chromium/chrome/browser/media/remote/RemoteSessionManager.java", + "src/org/chromium/chrome/browser/media/remote/TestMediaRouteProvider.java", + "src/org/chromium/chrome/browser/media/remote/TestMediaRouteProviderService.java", + ] + deps = [ + "//base:base_java", + "//third_party/android_tools:android_support_v7_appcompat_java", + "//third_party/android_tools:android_support_v7_mediarouter_java", + google_play_services_library, + ] +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/DummyPlayer.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/DummyPlayer.java new file mode 100644 index 0000000..1267317 --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/DummyPlayer.java @@ -0,0 +1,236 @@ +// Copyright 2014 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.remote; + +import android.content.Context; +import android.media.MediaPlayer; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; + +import java.io.IOException; + +/** + * Handles playback of a single media item using MediaPlayer. + */ +public class DummyPlayer implements MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener, + MediaPlayer.OnSeekCompleteListener { + private static final String TAG = "CastDummyPlayer"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final int STATE_IDLE = 0; + private static final int STATE_PLAY_PENDING = 1; + private static final int STATE_READY = 2; + private static final int STATE_PLAYING = 3; + private static final int STATE_PAUSED = 4; + + private final Context mContext; + private final Handler mHandler = new Handler(); + private MediaPlayer mMediaPlayer; + private int mState = STATE_IDLE; + private int mSeekToPos; + private Callback mCallback; + + /** + * Callback interface for the session manager + */ + public static interface Callback { + void onError(); + void onCompletion(); + void onSeekComplete(); + void onPrepared(); + } + + public DummyPlayer(Context context) { + // reset media player + reset(); + mContext = context; + } + + public void connect(RouteInfo route) { + if (DEBUG) Log.d(TAG, "connecting to: " + route); + } + + public void release() { + if (DEBUG) Log.d(TAG, "releasing"); + // release media player + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + // Player + public void play(final MediaItem item) { + if (DEBUG) Log.d(TAG, "play: item=" + item); + reset(); + mSeekToPos = (int) item.getPosition(); + try { + mMediaPlayer.setDataSource(mContext, item.getUri()); + mMediaPlayer.prepare(); + } catch (IllegalStateException e) { + Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri()); + } catch (IOException e) { + Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri()); + } catch (SecurityException e) { + Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri()); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + resume(); + } else { + pause(); + } + } + + public void seek(final MediaItem item) { + if (DEBUG) Log.d(TAG, "seek: item=" + item); + int pos = (int) item.getPosition(); + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.seekTo(pos); + mSeekToPos = pos; + } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) { + // Seek before onPrepared() arrives, + // need to performed delayed seek in onPrepared() + mSeekToPos = pos; + } + } + + public void getStatus(final MediaItem item, final boolean update) { + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + // use mSeekToPos if we're currently seeking (mSeekToPos is reset + // when seeking is completed) + item.setDuration(mMediaPlayer.getDuration()); + item.setPosition(mSeekToPos > 0 ? mSeekToPos : mMediaPlayer.getCurrentPosition()); + item.setTimestamp(SystemClock.uptimeMillis()); + } + } + + public void pause() { + if (DEBUG) Log.d(TAG, "pause"); + if (mState == STATE_PLAYING) { + mMediaPlayer.pause(); + mState = STATE_PAUSED; + } + } + + public void resume() { + if (DEBUG) Log.d(TAG, "resume"); + if (mState == STATE_READY || mState == STATE_PAUSED) { + mMediaPlayer.start(); + mState = STATE_PLAYING; + } else if (mState == STATE_IDLE) { + mState = STATE_PLAY_PENDING; + } + } + + public void stop() { + if (DEBUG) Log.d(TAG, "stop"); + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.stop(); + mState = STATE_IDLE; + } + } + + // MediaPlayer Listeners + @Override + public void onPrepared(MediaPlayer mp) { + if (DEBUG) Log.d(TAG, "onPrepared"); + mHandler.post(new Runnable() { + @Override + public void run() { + if (mState == STATE_IDLE) { + mState = STATE_READY; + } else if (mState == STATE_PLAY_PENDING) { + mState = STATE_PLAYING; + if (mSeekToPos > 0) { + if (DEBUG) Log.d(TAG, "seek to initial pos: " + mSeekToPos); + mMediaPlayer.seekTo(mSeekToPos); + } + mMediaPlayer.start(); + } + if (mCallback != null) { + mCallback.onPrepared(); + } + } + }); + } + + @Override + public void onCompletion(MediaPlayer mp) { + if (DEBUG) Log.d(TAG, "onCompletion"); + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onCompletion(); + } + } + }); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + if (DEBUG) Log.d(TAG, "onError"); + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onError(); + } + } + }); + // return true so that onCompletion is not called + return true; + } + + @Override + public void onSeekComplete(MediaPlayer mp) { + if (DEBUG) Log.d(TAG, "onSeekComplete"); + mHandler.post(new Runnable() { + @Override + public void run() { + mSeekToPos = 0; + if (mCallback != null) { + mCallback.onSeekComplete(); + } + } + }); + } + + protected Context getContext() { + return mContext; + } + + private void reset() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + mState = STATE_IDLE; + mSeekToPos = 0; + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public static DummyPlayer create(Context context, RouteInfo route) { + DummyPlayer player = new DummyPlayer(context); + player.connect(route); + return player; + } +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/LocalSessionManager.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/LocalSessionManager.java new file mode 100644 index 0000000..e66d2e1 --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/LocalSessionManager.java @@ -0,0 +1,199 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; +import android.support.v7.media.MediaSessionStatus; + +/** + * LocalSessionManager emulates the local management of the playback of media items on Chromecast. + * It only handles one item at a time, and does not support queuing. + * + * Most members simply forward their calls to a RemoteSessionManager, which emulates the session + * management on the Chromecast, however this class also controls connection to and disconnection + * from the RemoteSessionManager. + */ +public class LocalSessionManager { + /** + * Callbacks for MediaRouteProvider object. + */ + public interface Callback { + void onItemChanged(MediaItem item); + } + + private Callback mCallback; + private RemoteSessionManager mRemoteManager; + + private final Context mContext; + + /** + * @param context + */ + public LocalSessionManager(Context context) { + mContext = context; + } + + /** + * Add a video we want to play + * @param uri the URI of the video + * @param mime the mime type + * @param receiver the pending intent to use to send state changes + * @return the new media item + */ + public MediaItem add(Uri uri, String mime, PendingIntent receiver) { + if (!hasSession()) mRemoteManager = RemoteSessionManager.connect(this, mContext); + return mRemoteManager.add(uri, mime, receiver); + } + + /** + * End the current session + * @return whether there was a current session + */ + public boolean endSession() { + if (hasSession()) { + mRemoteManager.disconnect(); + mRemoteManager = null; + return true; + } + return false; + } + + /** + * Get the currently playing item + * @return the currently playing item, or null if none. + */ + public MediaItem getCurrentItem() { + return hasSession() ? mRemoteManager.getCurrentItem() : null; + } + + /** + * Get the session id of the current session + * @return the session id, or null if none. + */ + public String getSessionId() { + return hasSession() ? mRemoteManager.getSessionId() : null; + } + + /** + * Get the status of a session + * @param sid the session id of session being asked about + * @return the status + */ + public MediaSessionStatus getSessionStatus(String sid) { + if (!hasSession()) { + return new MediaSessionStatus.Builder(MediaSessionStatus.SESSION_STATE_INVALIDATED) + .setQueuePaused(false).build(); + } + return mRemoteManager.getSessionStatus(sid); + } + + /** + * Get a printable string describing the status of the session + * @return the string + */ + public String getSessionStatusString() { + if (hasSession()) { + return mRemoteManager.getSessionStatusString(); + } else { + return "No remote session connection"; + } + } + + /** + * Get the status of a media item + * @param iid - the id of the item + * @return the MediaItem, from which its status can be read. + */ + public MediaItem getStatus(String iid) { + if (!hasSession()) { + throw new IllegalStateException("Session not set!"); + } + return mRemoteManager.getStatus(iid); + } + + /** + * @return whether there is a current session + */ + public boolean hasSession() { + return mRemoteManager != null; + } + + /** + * @return whether the current video is paused + */ + public boolean isPaused() { + return hasSession() && mRemoteManager.isPaused(); + } + + /** + * Forward the item changed callback to the UI + * @param item the item that has changed. + */ + public void onItemChanged(MediaItem item) { + if (mCallback != null) mCallback.onItemChanged(item); + } + + /** + * Pause the current video + */ + public void pause() { + if (hasSession()) mRemoteManager.pause(); + } + + /** + * Resume the current video + */ + public void resume() { + if (hasSession()) mRemoteManager.resume(); + } + + /** + * Seek to a position in a video + * @param iid the id of the video + * @param pos the position in ms + * @return the Media item. + */ + public MediaItem seek(String iid, long pos) { + return hasSession() ? mRemoteManager.seek(iid, pos) : null; + } + + /** + * provide a callback interface to tell the UI when significant state changes occur + * @param callback the callback object + */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * Start a new local session + * @param relaunch relaunch the remote session (the emulation of the Chromecast app) even if it + * is already running. + * @return The new session id + */ + public String startSession(boolean relaunch) { + if (!relaunch) endSession(); + if (!hasSession()) mRemoteManager = RemoteSessionManager.connect(this, mContext); + return mRemoteManager.startSession(relaunch); + } + + /** + * Stop the current video + */ + public void stop() { + if (hasSession()) mRemoteManager.stop(); + endSession(); + } + + /** + * Updates the session status. + */ + public void updateStatus() { + if (hasSession()) mRemoteManager.updateStatus(); + } + +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/MediaItem.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/MediaItem.java new file mode 100644 index 0000000..732d9df --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/MediaItem.java @@ -0,0 +1,109 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent; +import android.net.Uri; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; +import android.util.Log; + +/** + * PlaylistItem helps keep track of the current status of an media item. + */ +final class MediaItem { + // immutables + private final String mSessionId; + private final String mItemId; + private final Uri mUri; + private final PendingIntent mUpdateReceiver; + // changeable states + private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING; + private long mContentPosition; + private long mContentDuration; + private long mTimestamp; + private String mRemoteItemId; + private static final String TAG = "MediaItem"; + + + public MediaItem(String qid, String iid, Uri uri, String mime, PendingIntent pi) { + mSessionId = qid; + mItemId = iid; + mUri = uri; + mUpdateReceiver = pi; + setTimestamp(SystemClock.uptimeMillis()); + } + + public void setRemoteItemId(String riid) { + mRemoteItemId = riid; + } + + public void setState(int state) { + mPlaybackState = state; + Log.d(TAG, "State set to " + state); + } + + public void setPosition(long pos) { + mContentPosition = pos; + } + + public void setTimestamp(long ts) { + mTimestamp = ts; + } + + public void setDuration(long duration) { + mContentDuration = duration; + } + + public String getSessionId() { + return mSessionId; + } + + public String getItemId() { + return mItemId; + } + + public String getRemoteItemId() { + return mRemoteItemId; + } + + public Uri getUri() { + return mUri; + } + + public PendingIntent getUpdateReceiver() { + return mUpdateReceiver; + } + + public int getState() { + return mPlaybackState; + } + + public long getPosition() { + return mContentPosition; + } + + public long getDuration() { + return mContentDuration; + } + + public long getTimestamp() { + return mTimestamp; + } + + public MediaItemStatus getStatus() { + return new MediaItemStatus.Builder(mPlaybackState).setContentPosition(mContentPosition) + .setContentDuration(mContentDuration).setTimestamp(mTimestamp).build(); + } + + @Override + public String toString() { + String state[] = {"PENDING", "PLAYING", "PAUSED", "BUFFERING", "FINISHED", "CANCELED", + "INVALIDATED", "ERROR"}; + return "[" + mSessionId + "|" + mItemId + "|" + + (mRemoteItemId != null ? mRemoteItemId : "-") + "|" + state[mPlaybackState] + "] " + + mUri.toString(); + } +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/RemoteSessionManager.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/RemoteSessionManager.java new file mode 100644 index 0000000..a0835fb --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/RemoteSessionManager.java @@ -0,0 +1,360 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.Uri; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +/** + * RemoteSessionManager emulates the session management of the playback of media items on + * Chromecast. This can be seen as emulating the cast receiver application. It only handles one item + * at a time, and does not support queuing. + * + * Actual playback of a single media item is abstracted into the DummyPlayer class, and is handled + * outside this class. + */ +public class RemoteSessionManager implements DummyPlayer.Callback { + private static final String TAG = "RemoteSessionManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * Connect a local session manager to the unique remote session manager, creating it if needed. + * @param localSessionManager the local session manager being connected + * @param player the player to use + * @return the remote session manager + */ + public static RemoteSessionManager connect(LocalSessionManager localSessionManager, + Context context) { + if (sInstance == null) { + sInstance = new RemoteSessionManager("remote", context); + } + sInstance.mLocalSessionManager = localSessionManager; + return sInstance; + } + + private String mName; + private int mSessionId; + private int mItemId; + private boolean mPaused; + private boolean mSessionValid; + private DummyPlayer mPlayer; + private MediaItem mCurrentItem; + private static RemoteSessionManager sInstance; + + private LocalSessionManager mLocalSessionManager; + private Context mContext; + + private RemoteSessionManager(String name, Context context) { + mName = name; + mContext = context; + } + + /** + * Add a video we want to play + * + * @param uri the URI of the video + * @param mime the mime type + * @param receiver the pending intent to use to send state changes + * @return the new media item + */ + public MediaItem add(Uri uri, String mime, PendingIntent receiver) { + if (DEBUG) log("add: uri=" + uri + ", receiver=" + receiver); + // create new session if needed + startSession(false); + checkPlayerAndSession(); + + // create new item with initial status PLAYBACK_STATE_PENDING + mItemId++; + mCurrentItem = new MediaItem(Integer.toString(mSessionId), Integer.toString(mItemId), uri, + mime, receiver); + if (DEBUG) log("add: new item id = " + mCurrentItem); + return mCurrentItem; + } + + /** + * Disconnect from the local session + */ + public void disconnect() { + mLocalSessionManager = null; + } + + /** + * Get the currently playing item + * + * @return the currently playing item, or null if none. + */ + public MediaItem getCurrentItem() { + return mCurrentItem; + } + + /** + * Get the session id of the current session + * + * @return the session id, or null if none. + */ + public String getSessionId() { + return mSessionValid ? Integer.toString(mSessionId) : null; + } + + /** + * Get the status of a session + * + * @param sid the session id of session being asked about + * @return the status + */ + public MediaSessionStatus getSessionStatus(String sid) { + Log.d(TAG, "Getting session status for session " + sid); + int sessionState = + (sid != null && sid.equals(Integer.toString(mSessionId))) + ? MediaSessionStatus.SESSION_STATE_ACTIVE + : MediaSessionStatus.SESSION_STATE_INVALIDATED; + + Log.d(TAG, "Session state is " + sessionState); + + return new MediaSessionStatus.Builder(sessionState).setQueuePaused(mPaused).build(); + } + + /** + * Get a printable string describing the status of the session + * @return the string + */ + public String getSessionStatusString() { + if (mCurrentItem != null) { + return "Current media item: " + mCurrentItem.toString(); + } else { + return "No current media item"; + } + } + + /** + * Get the status of a media item + * + * @param iid - the id of the item + * @return the MediaItem, from which its status can be read. + */ + public MediaItem getStatus(String iid) { + checkPlayerAndSession(); + checkItemCurrent(iid); + + mPlayer.getStatus(mCurrentItem, false); + return mCurrentItem; + } + + /** + * @return whether the current video is paused + */ + public boolean isPaused() { + return mSessionValid && mPaused; + } + + @Override + public void onCompletion() { + finishItem(false); + } + + // Player.Callback + @Override + public void onError() { + finishItem(true); + } + + @Override + public void onSeekComplete() { + // Playlist has changed, update the cached playlist + updateStatus(); + } + + @Override + public void onPrepared() { + // Item is ready to play, update the status. + updateStatus(); + // Send the new status to the local session manager. + onItemChanged(); + } + + /** + * Pause the current video + */ + public void pause() { + if (DEBUG) log("pause"); + if (!mSessionValid) { + return; + } + checkPlayer(); + mPaused = true; + updatePlaybackState(); + } + + /** + * Resume the current video + */ + public void resume() { + if (DEBUG) log("resume"); + if (!mSessionValid) { + return; + } + checkPlayer(); + mPaused = false; + updatePlaybackState(); + } + + /** + * Seek to a position in a video + * + * @param iid the id of the video + * @param pos the position in ms + * @return the Media item. + */ + public MediaItem seek(String iid, long pos) { + if (DEBUG) log("seek: iid=" + iid + ", pos=" + pos); + checkPlayerAndSession(); + checkItemCurrent(iid); + + if (pos != mCurrentItem.getPosition()) { + mCurrentItem.setPosition(pos); + if (mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.seek(mCurrentItem); + } + } + return mCurrentItem; + } + + /** + * Start a new emulated Chromecast session if needed. + * + * @param relaunch relaunch the remote session (the emulation of the Chromecast app) even if it + * is already running. + * @return The new session id + */ + public String startSession(boolean relaunch) { + if (!mSessionValid || relaunch) { + if (mPlayer != null) mPlayer.setCallback(null); + finishItem(false); + if (mPlayer != null) mPlayer.release(); + mSessionId++; + mItemId = 0; + mPaused = false; + mSessionValid = true; + mPlayer = DummyPlayer.create(mContext, null); + mPlayer.setCallback(this); + mCurrentItem = null; + Log.d(TAG, "Starting session " + mSessionId); + } + return Integer.toString(mSessionId); + } + + /** + * Stop the current video + */ + public void stop() { + if (DEBUG) log("stop"); + if (!mSessionValid) { + return; + } + checkPlayer(); + mPlayer.stop(); + mCurrentItem = null; + mPaused = false; + updateStatus(); + } + + // Updates the playlist. + public void updateStatus() { + if (DEBUG) log("updateStatus"); + checkPlayer(); + + if (mCurrentItem != null) { + mPlayer.getStatus(mCurrentItem, true /* update */); + } + } + + private void checkItemCurrent(String iid) { + if (mCurrentItem == null || !mCurrentItem.getItemId().equals(iid)) { + throw new IllegalArgumentException("Item is not current!"); + } + } + + private void checkPlayer() { + if (mPlayer == null) { + throw new IllegalStateException("Player not set!"); + } + } + + private void checkPlayerAndSession() { + checkPlayer(); + checkSession(); + } + + private void checkSession() { + if (!mSessionValid) { + throw new IllegalStateException("Session not set!"); + } + } + + private void finishItem(boolean error) { + if (mCurrentItem != null) { + removeItem(mCurrentItem.getItemId(), error ? MediaItemStatus.PLAYBACK_STATE_ERROR + : MediaItemStatus.PLAYBACK_STATE_FINISHED); + updateStatus(); + } + } + + private void log(String message) { + Log.d(TAG, mName + ": " + message); + } + + private MediaItem removeItem(String iid, int state) { + checkPlayerAndSession(); + + checkItemCurrent(iid); + + if (mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.stop(); + } + mCurrentItem.setState(state); + onItemChanged(); + updatePlaybackState(); + + MediaItem item = mCurrentItem; + + mCurrentItem = null; + + return item; + } + + private void updatePlaybackState() { + if (mCurrentItem != null) { + if (mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) { + mCurrentItem.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED + : MediaItemStatus.PLAYBACK_STATE_PLAYING); + mPlayer.play(mCurrentItem); + } else if (mPaused + && mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + mPlayer.pause(); + mCurrentItem.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED); + } else if (!mPaused + && mCurrentItem.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.resume(); + mCurrentItem.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING); + } + // notify client that item playback status has changed + onItemChanged(); + } + updateStatus(); + } + + private void onItemChanged() { + if (mLocalSessionManager != null) { + mLocalSessionManager.onItemChanged(mCurrentItem); + } + } + +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProvider.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProvider.java new file mode 100644 index 0000000..e05586b --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProvider.java @@ -0,0 +1,467 @@ +// Copyright 2014 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.remote; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.media.AudioManager; +import android.media.MediaRouter; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter.ControlRequestCallback; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +import com.google.android.gms.cast.CastMediaControlIntent; + +import org.chromium.base.annotations.SuppressFBWarnings; + +import java.util.ArrayList; + +/** + * Dummy media route provider for testing casting from Chrome. + * + * @see TestMediaRouteProviderService + */ +final class TestMediaRouteProvider extends MediaRouteProvider { + private static final String MANIFEST_CAST_KEY = + "com.google.android.apps.chrome.tests.support.CAST_ID"; + + private static final String TAG = "TestMediaRouteProvider"; + + private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session"; + private static final int VOLUME_MAX = 10; + + private int mVolume = 5; + + public TestMediaRouteProvider(Context context) { + super(context); + + publishRoutes(); + } + + @Override + public RouteController onCreateRouteController(String routeId) { + return new TestMediaController(routeId); + } + + private void publishRoutes() { + + String castId = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; + try { + // Downstream cast uses a different, private, castId; so read this from + // the manifest. + ApplicationInfo ai; + ai = getContext().getPackageManager().getApplicationInfo( + getContext().getPackageName(), PackageManager.GET_META_DATA); + Bundle bundle = ai.metaData; + if (bundle != null) { + castId = bundle.getString(MANIFEST_CAST_KEY, castId); + } + } catch (NameNotFoundException e) { + // Should never happen, do nothing - use default + } + + IntentFilter f1 = new IntentFilter(); + f1.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f1.addCategory(CastMediaControlIntent.categoryForRemotePlayback(castId)); + f1.addAction(MediaControlIntent.ACTION_PLAY); + f1.addDataScheme("http"); + f1.addDataScheme("https"); + addDataTypeUnchecked(f1, "video/*"); + + IntentFilter f2 = new IntentFilter(); + f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f2.addCategory(CastMediaControlIntent.categoryForRemotePlayback()); + f2.addAction(MediaControlIntent.ACTION_SEEK); + f2.addAction(MediaControlIntent.ACTION_GET_STATUS); + f2.addAction(MediaControlIntent.ACTION_PAUSE); + f2.addAction(MediaControlIntent.ACTION_RESUME); + f2.addAction(MediaControlIntent.ACTION_STOP); + f2.addAction(MediaControlIntent.ACTION_START_SESSION); + f2.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS); + f2.addAction(MediaControlIntent.ACTION_END_SESSION); + f2.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS); + + ArrayList<IntentFilter> controlFilters = new ArrayList<IntentFilter>(); + controlFilters.add(f1); + controlFilters.add(f2); + + MediaRouteDescriptor testRouteDescriptor = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_SESSION_ROUTE_ID, "Cast Test Route") + .setDescription("Cast Test Route").addControlFilters(controlFilters) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX).setVolume(mVolume).build(); + + MediaRouteProviderDescriptor providerDescriptor = new MediaRouteProviderDescriptor.Builder() + .addRoute(testRouteDescriptor).build(); + setDescriptor(providerDescriptor); + } + + private final class TestMediaController extends MediaRouteProvider.RouteController { + private final String mRouteId; + private final LocalSessionManager mSessionManager = new LocalSessionManager(getContext()); + private PendingIntent mSessionReceiver; + private Bundle mMetadata; + + public TestMediaController(String routeId) { + mRouteId = routeId; + mSessionManager.setCallback(new LocalSessionManager.Callback() { + @Override + public void onItemChanged(MediaItem item) { + handleStatusChange(item); + } + }); + setVolumeInternal(mVolume); + Log.d(TAG, mRouteId + ": Controller created"); + } + + @Override + public void onRelease() { + Log.d(TAG, mRouteId + ": Controller released"); + } + + @Override + public void onSelect() { + Log.d(TAG, mRouteId + ": Selected"); + } + + @Override + public void onUnselect() { + Log.d(TAG, mRouteId + ": Unselected"); + } + + @Override + public void onSetVolume(int volume) { + Log.d(TAG, mRouteId + ": Set volume to " + volume); + setVolumeInternal(volume); + } + + @Override + public void onUpdateVolume(int delta) { + Log.d(TAG, mRouteId + ": Update volume by " + delta); + setVolumeInternal(mVolume + delta); + } + + @Override + public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { + Log.d(TAG, mRouteId + ": Received control request " + intent); + String action = intent.getAction(); + if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) + || intent.hasCategory(CastMediaControlIntent.categoryForRemotePlayback())) { + boolean success = false; + if (action.equals(MediaControlIntent.ACTION_PLAY)) { + success = handlePlay(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_SEEK)) { + success = handleSeek(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) { + success = handleGetStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) { + success = handlePause(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_RESUME)) { + success = handleResume(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_STOP)) { + success = handleStop(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) { + success = handleStartSession(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) { + success = handleGetSessionStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) { + success = handleEndSession(intent, callback); + } else if (action.equals(CastMediaControlIntent.ACTION_SYNC_STATUS)) { + success = handleSyncStatus(intent, callback); + } + Log.d(TAG, mSessionManager.getSessionStatusString()); + return success; + } + + return false; + } + + /** + * @param intent + * @param callback + * @return + */ + private boolean handleSyncStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + Log.d(TAG, mRouteId + ": Received syncStatus request, sid=" + sid); + + MediaItem item = mSessionManager.getCurrentItem(); + if (callback != null) { + Bundle result = new Bundle(); + if (item != null) { + String iid = item.getItemId(); + result.putString(MediaControlIntent.EXTRA_ITEM_ID, iid); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + if (mMetadata != null) { + result.putBundle(MediaControlIntent.EXTRA_ITEM_METADATA, mMetadata); + } + } + callback.onResult(result); + } + return true; + } + + private void setVolumeInternal(int volume) { + if (volume >= 0 && volume <= VOLUME_MAX) { + mVolume = volume; + Log.d(TAG, mRouteId + ": New volume is " + mVolume); + AudioManager audioManager = (AudioManager) getContext() + .getSystemService(Context.AUDIO_SERVICE); + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); + publishRoutes(); + } + } + + private boolean handlePlay(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid != null && !sid.equals(mSessionManager.getSessionId())) { + Log.d(TAG, "handlePlay fails because of bad sid=" + sid); + return false; + } + if (mSessionManager.hasSession()) { + mSessionManager.stop(); + } + + Uri uri = intent.getData(); + if (uri == null) { + Log.d(TAG, "handlePlay fails because of null uri"); + return false; + } + + String mime = intent.getType(); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + mMetadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA); + Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS); + PendingIntent receiver = (PendingIntent) intent.getParcelableExtra( + MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER); + + Log.d(TAG, mRouteId + ": Received play request, uri=" + uri + ", mime=" + mime + + ", sid=" + sid + ", pos=" + pos + ", metadata=" + mMetadata + ", headers=" + + headers + ", receiver=" + receiver); + // Add the video to the session manager. + MediaItem item = mSessionManager.add(uri, mime, receiver); + // And start it playing. + mSessionManager.resume(); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to open " + uri.toString(), null); + } + } + return true; + } + + private boolean handleSeek(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid == null || !sid.equals(mSessionManager.getSessionId())) { + return false; + } + + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos); + MediaItem item = mSessionManager.seek(iid, pos); + if (callback != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, item.getStatus().asBundle()); + callback.onResult(result); + } + return true; + } + + private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid); + MediaItem item = mSessionManager.getStatus(iid); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get status" + ", sid=" + sid + ", iid=" + iid, + null); + } + } + return (item != null); + } + + private boolean handlePause(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.pause(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to pause, sid=" + sid, null); + } + } + return success; + } + + private boolean handleResume(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.resume(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to resume, sid=" + sid, null); + } + } + return success; + } + + private boolean handleStop(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.stop(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to stop, sid=" + sid, null); + } + } + return success; + } + + @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE") + private boolean handleStartSession(Intent intent, ControlRequestCallback callback) { + boolean relaunch = intent.getBooleanExtra( + CastMediaControlIntent.EXTRA_CAST_RELAUNCH_APPLICATION, true); + String sid = mSessionManager.startSession(relaunch); + Log.d(TAG, "StartSession returns sessionId " + sid); + if (callback != null) { + if (sid != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + Log.d(TAG, "StartSession sends result of " + result); + callback.onResult(result); + mSessionReceiver = (PendingIntent) intent.getParcelableExtra( + MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to start session.", null); + } + } + return (sid != null); + } + + private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + + MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid); + if (callback != null) { + if (sessionStatus != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get session status, sid=" + sid, null); + } + } + return (sessionStatus != null); + } + + private boolean handleEndSession(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()) + && mSessionManager.endSession(); + if (callback != null) { + if (success) { + Bundle result = new Bundle(); + MediaSessionStatus sessionStatus = new MediaSessionStatus.Builder( + MediaSessionStatus.SESSION_STATE_ENDED).build(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + sessionStatus.asBundle()); + callback.onResult(result); + handleSessionStatusChange(sid); + mSessionReceiver = null; + } else { + callback.onError("Failed to end session, sid=" + sid, null); + } + } + return success; + } + + private void handleStatusChange(MediaItem item) { + if (item == null) { + item = mSessionManager.getCurrentItem(); + } + if (item != null) { + PendingIntent receiver = item.getUpdateReceiver(); + if (receiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + try { + receiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send status update!"); + } + } + } + } + + private void handleSessionStatusChange(String sid) { + if (mSessionReceiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + try { + mSessionReceiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending session status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send session status update!"); + } + } + } + } + + private static void addDataTypeUnchecked(IntentFilter filter, String type) { + try { + filter.addDataType(type); + } catch (MalformedMimeTypeException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProviderService.java b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProviderService.java new file mode 100644 index 0000000..2867ab9 --- /dev/null +++ b/chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProviderService.java @@ -0,0 +1,21 @@ +// Copyright 2014 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.remote; + +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderService; + +/** + * Demonstrates how to register a custom media route provider service + * using the support library. + * + * @see TestMediaRouteProvider + */ +public class TestMediaRouteProviderService extends MediaRouteProviderService { + @Override + public MediaRouteProvider onCreateMediaRouteProvider() { + return new TestMediaRouteProvider(this); + } +} diff --git a/chrome/test/android/chrome_public_test_support/AndroidManifest.xml b/chrome/test/android/chrome_public_test_support/AndroidManifest.xml new file mode 100644 index 0000000..0d60c71 --- /dev/null +++ b/chrome/test/android/chrome_public_test_support/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> + <!-- Copyright (c) 2014 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. --> + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="org.chromium.chrome.tests.support"> + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" /> + <uses-permission android:name="android.permission.INTERNET" /> + <application> + <service android:name="org.chromium.chrome.browser.media.remote.TestMediaRouteProviderService" + android:label="testMediaRouteProviderService" + android:process=":mrp" + tools:ignore="ExportedService" > + <intent-filter> + <action android:name="android.media.MediaRouteProviderService" /> + </intent-filter> + </service> + </application> +</manifest> diff --git a/chrome/test/android/chrome_public_test_support/BUILD.gn b/chrome/test/android/chrome_public_test_support/BUILD.gn new file mode 100644 index 0000000..d65584b --- /dev/null +++ b/chrome/test/android/chrome_public_test_support/BUILD.gn @@ -0,0 +1,16 @@ +# Copyright 2015 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. + +import("//build/config/android/config.gni") +import("//build/config/android/rules.gni") + +# GYP: //clank/native/clank.gyp:chrome_public_test_support_apk +android_apk("chrome_public_test_support_apk") { + deps = [ + "//chrome/test/android/cast_emulator:cast_emulator", + ] + + apk_name = "ChromePublicTestSupport" + android_manifest = "AndroidManifest.xml" +} |