diff options
Diffstat (limited to 'chrome/test')
9 files changed, 1454 insertions, 0 deletions
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" +} |