summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraberent <aberent@chromium.org>2015-12-11 02:36:25 -0800
committerCommit bot <commit-bot@chromium.org>2015-12-11 10:38:00 +0000
commit873d21a42c9a3c9e7e6556f5b7cfade679b3c568 (patch)
treef00da60df42aee22e120cdb70c78aaabd0eff2f9
parent9e1dad644c48db574407ccee4e95b8b10f5174ee (diff)
downloadchromium_src-873d21a42c9a3c9e7e6556f5b7cfade679b3c568.zip
chromium_src-873d21a42c9a3c9e7e6556f5b7cfade679b3c568.tar.gz
chromium_src-873d21a42c9a3c9e7e6556f5b7cfade679b3c568.tar.bz2
Upstream Android Cast tests
This upstreams the Android Cast instrumentation tests. To do so it also upstreams the test support apk. BUG=476669 Review URL: https://codereview.chromium.org/1508823004 Cr-Commit-Position: refs/heads/master@{#364666}
-rw-r--r--chrome/android/BUILD.gn3
-rw-r--r--chrome/android/chrome_apk.gyp1
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastNotificationTest.java104
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastPositionTransferTest.java154
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastReconnectTest.java184
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastStartStopTest.java89
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastSwitchVideoTest.java108
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastTestBase.java605
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/media/remote/CastVideoControlsTest.java53
-rw-r--r--chrome/chrome_test_support.gypi69
-rw-r--r--chrome/chrome_tests.gypi25
-rw-r--r--chrome/test/android/cast_emulator/BUILD.gn26
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/DummyPlayer.java236
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/LocalSessionManager.java199
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/MediaItem.java109
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/RemoteSessionManager.java360
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProvider.java467
-rw-r--r--chrome/test/android/cast_emulator/src/org/chromium/chrome/browser/media/remote/TestMediaRouteProviderService.java21
-rw-r--r--chrome/test/android/chrome_public_test_support/AndroidManifest.xml20
-rw-r--r--chrome/test/android/chrome_public_test_support/BUILD.gn16
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"
+}