// Copyright 2012 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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaItemMetadata;
import android.support.v7.media.MediaItemStatus;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.support.v7.media.MediaSessionStatus;
import com.google.android.gms.cast.CastMediaControlIntent;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Log;
import org.chromium.base.annotations.RemovableInRelease;
import org.chromium.base.annotations.UsedByReflection;
import org.chromium.chrome.browser.media.remote.RemoteVideoInfo.PlayerState;
import java.net.URI;
import java.net.URISyntaxException;
import javax.annotation.Nullable;
/**
* Class that abstracts all communication to and from the Android MediaRoutes. This class is
* responsible for connecting to the MRs as well as sending commands and receiving status updates
* from the remote player.
*
* We have two main scenarios for Cast:
*
* - the first cast: user plays the first video on the Chromecast so we start a new session with
* the player and fling the video
*
* - the consequent cast: users plays another video while the previous one is still playing
* remotely meaning that we don't have to start the session but to replace the current video with
* the new one
*
* Casting the first video takes two intents sent to the selected media route:
* ACTION_START_SESSION and ACTION_PLAY. The first one is sent before anything else. We get the
* session id from the result bundle of the intent but need to wait until the session becomes
* active before sending the video URL via the ACTION_PLAY intent.
*
* Casting the second video to the same target device only takes one ACTION_PLAY intent if
* the session is still active. Otherwise, the scenario is the same as for the first video.
*/
@UsedByReflection("RemoteMediaPlayerController.java")
public class DefaultMediaRouteController extends AbstractMediaRouteController {
/**
* Interface for MediaRouter intents result handlers.
*/
protected interface ResultBundleHandler {
void onResult(Bundle data);
void onError(String message, Bundle data);
}
private static final String TAG = "MediaFling";
private static final String ACTION_RECEIVE_SESSION_STATUS_UPDATE =
"com.google.android.apps.chrome.videofling.RECEIVE_SESSION_STATUS_UPDATE";
private static final String ACTION_RECEIVE_MEDIA_STATUS_UPDATE =
"com.google.android.apps.chrome.videofling.RECEIVE_MEDIA_STATUS_UPDATE";
private static final String MIME_TYPE = "video/mp4";
private String mCurrentSessionId;
private String mCurrentItemId;
private boolean mSeeking;
private final String mIntentCategory;
private PendingIntent mSessionStatusUpdateIntent;
private BroadcastReceiver mSessionStatusBroadcastReceiver;
private PendingIntent mMediaStatusUpdateIntent;
private BroadcastReceiver mMediaStatusBroadcastReceiver;
private String mPreferredTitle;
private long mStartPositionMillis;
private final PositionExtrapolator mPositionExtrapolator = new PositionExtrapolator();
private Uri mLocalVideoUri;
private int mSessionState = MediaSessionStatus.SESSION_STATE_INVALIDATED;
private final ApplicationStatus.ApplicationStateListener mApplicationStateListener =
new ApplicationStatus.ApplicationStateListener() {
@Override
public void onApplicationStateChange(int newState) {
switch (newState) {
// HAS_DESTROYED_ACTIVITIES means all Chrome activities have been destroyed.
case ApplicationState.HAS_DESTROYED_ACTIVITIES:
onActivitiesDestroyed();
break;
default:
break;
}
}
};
/**
* Default and only constructor.
*/
public DefaultMediaRouteController() {
mIntentCategory = getContext().getPackageName();
}
@Override
public boolean initialize() {
if (mediaRouterInitializationFailed()) return false;
ApplicationStatus.registerApplicationStateListener(mApplicationStateListener);
if (mSessionStatusUpdateIntent == null) {
Intent sessionStatusUpdateIntent = new Intent(ACTION_RECEIVE_SESSION_STATUS_UPDATE);
sessionStatusUpdateIntent.addCategory(mIntentCategory);
mSessionStatusUpdateIntent = PendingIntent.getBroadcast(getContext(), 0,
sessionStatusUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
if (mMediaStatusUpdateIntent == null) {
Intent mediaStatusUpdateIntent = new Intent(ACTION_RECEIVE_MEDIA_STATUS_UPDATE);
mediaStatusUpdateIntent.addCategory(mIntentCategory);
mMediaStatusUpdateIntent = PendingIntent.getBroadcast(getContext(), 0,
mediaStatusUpdateIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
return true;
}
@Override
public boolean canPlayMedia(String sourceUrl, String frameUrl) {
if (mediaRouterInitializationFailed()) return false;
if (sourceUrl == null) return false;
try {
String scheme = new URI(sourceUrl).getScheme();
if (scheme == null) return false;
return scheme.equals("http") || scheme.equals("https");
} catch (URISyntaxException e) {
return false;
}
}
@Override
public void setRemoteVolume(int delta) {
boolean canChangeRemoteVolume = (getCurrentRoute().getVolumeHandling()
== MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE);
if (currentRouteSupportsRemotePlayback() && canChangeRemoteVolume) {
getCurrentRoute().requestUpdateVolume(delta);
}
}
@Override
public MediaRouteSelector buildMediaRouteSelector() {
return new MediaRouteSelector.Builder().addControlCategory(
CastMediaControlIntent.categoryForRemotePlayback(getCastReceiverId())).build();
}
protected String getCastReceiverId() {
return CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
}
@Override
public void resume() {
if (mCurrentItemId == null) return;
Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
sendIntentToRoute(intent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
processMediaStatusBundle(data);
}
@Override
public void onError(String message, Bundle data) {
release();
}
});
setDisplayedPlayerState(PlayerState.LOADING);
}
@Override
public void pause() {
if (mCurrentItemId == null) return;
Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
sendIntentToRoute(intent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
processMediaStatusBundle(data);
}
@Override
public void onError(String message, Bundle data) {
// Do not release the player just because of a failed pause
// request. This can happen when pausing more than once for
// example.
}
});
// Update the last known position to the current one so that we don't
// jump back in time discarding whatever we extrapolated from the last
// time the position was updated.
mPositionExtrapolator.onPaused();
setDisplayedPlayerState(PlayerState.PAUSED);
}
/**
* Plays the given Uri on the currently selected player. This will replace any currently playing
* video
*
* @param preferredTitle the preferred title of the current playback session to display
* @param startPositionMillis from which to start playing.
*/
private void playUri(@Nullable final String preferredTitle, final long startPositionMillis) {
RecordCastAction.castMediaType(MediaUrlResolver.getMediaType(mLocalVideoUri));
installBroadcastReceivers();
// If the session is already started (meaning we are casting a video already), we simply
// load the new URL with one ACTION_PLAY intent.
if (mCurrentSessionId != null) {
Log.d(TAG, "Playing a new url: %s", mLocalVideoUri);
// We keep the same session so only clear the playing item status.
clearItemState();
startPlayback(preferredTitle, startPositionMillis);
return;
}
Log.d(TAG, "Sending stream to app: %s", getCastReceiverId());
Log.d(TAG, "Url: %s", mLocalVideoUri);
startSession(true, null, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
configureNewSession(data);
mPreferredTitle = preferredTitle;
updateTitle(mPreferredTitle);
mStartPositionMillis = startPositionMillis;
// Make sure we get a session status. If the session becomes active
// immediately then the broadcast session status can arrive before we have
// the session id, so this ensures we get it whatever happens.
getSessionStatus(mCurrentSessionId);
}
@Override
public void onError(String message, Bundle data) {
release();
RecordCastAction.castDefaultPlayerResult(false);
}
});
}
/**
* Send a start session intent.
*
* @param relaunch Whether we should relaunch the cast application.
* @param resultBundleHandler BundleHandler to handle reply.
*/
private void startSession(boolean relaunch, String sessionId,
ResultBundleHandler resultBundleHandler) {
Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.putExtra(CastMediaControlIntent.EXTRA_CAST_STOP_APPLICATION_WHEN_SESSION_ENDS, true);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
mSessionStatusUpdateIntent);
intent.putExtra(CastMediaControlIntent.EXTRA_CAST_APPLICATION_ID, getCastReceiverId());
intent.putExtra(CastMediaControlIntent.EXTRA_CAST_RELAUNCH_APPLICATION, relaunch);
if (sessionId != null) intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
addIntentExtraForDebugLogging(intent);
sendIntentToRoute(intent, resultBundleHandler);
}
@RemovableInRelease
private void addIntentExtraForDebugLogging(Intent intent) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
intent.putExtra(CastMediaControlIntent.EXTRA_DEBUG_LOGGING_ENABLED, true);
}
}
private void getSessionStatus(String sessionId) {
Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
sendIntentToRoute(intent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
logBundle("getSessionStatus result :", data);
processSessionStatusBundle(data);
}
@Override
public void onError(String message, Bundle data) {
release();
}
});
}
private void startPlayback(
@Nullable final String preferredTitle, final long startPositionMillis) {
setUnprepared();
Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.setDataAndType(mLocalVideoUri, MIME_TYPE);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
mMediaStatusUpdateIntent);
intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, startPositionMillis);
if (preferredTitle != null) {
Bundle metadata = new Bundle();
metadata.putString(MediaItemMetadata.KEY_TITLE, preferredTitle);
intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
}
sendIntentToRoute(intent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
mCurrentItemId = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
processMediaStatusBundle(data);
RecordCastAction.castDefaultPlayerResult(true);
}
@Override
public void onError(String message, Bundle data) {
release();
RecordCastAction.castDefaultPlayerResult(false);
}
});
}
@Override
public long getPosition() {
return mPositionExtrapolator.getPosition();
}
@Override
public long getDuration() {
return mPositionExtrapolator.getDuration();
}
@Override
public void seekTo(long msec) {
if (msec == getPosition()) return;
// Update the position now since the MRP will update it only once the video is playing
// remotely. In particular, if the video is paused, the MRP doesn't send the command until
// the video is resumed.
mPositionExtrapolator.onSeek(msec);
mSeeking = true;
Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, mCurrentItemId);
intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, msec);
sendIntentToRoute(intent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
if (getMediaStateListener() != null) getMediaStateListener().onSeekCompleted();
processMediaStatusBundle(data);
}
@Override
public void onError(String message, Bundle data) {
release();
}
});
}
@Override
public void release() {
super.release();
for (UiListener listener : getUiListeners()) {
listener.onRouteUnselected(this);
}
if (getMediaStateListener() != null) getMediaStateListener().onRouteUnselected();
setMediaStateListener(null);
if (mediaRouterInitializationFailed()) return;
if (mCurrentSessionId == null) {
// This can happen if we disconnect after a failure (because the
// media could not be casted).
disconnect();
return;
}
Intent stopIntent = new Intent(MediaControlIntent.ACTION_STOP);
stopIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
stopIntent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
sendIntentToRoute(stopIntent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
processMediaStatusBundle(data);
}
@Override
public void onError(String message, Bundle data) {}
});
Intent endSessionIntent = new Intent(MediaControlIntent.ACTION_END_SESSION);
endSessionIntent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
endSessionIntent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, mCurrentSessionId);
sendIntentToRoute(endSessionIntent, new ResultBundleHandler() {
@Override
public void onResult(Bundle data) {
logMediaSessionStatus(data);
for (UiListener listener : getUiListeners()) {
listener.onPlaybackStateChanged(PlayerState.FINISHED);
}
if (getMediaStateListener() != null) {
getMediaStateListener().onPlaybackStateChanged(PlayerState.FINISHED);
}
recordRemainingTimeUMA();
disconnect();
}
@Override
public void onError(String message, Bundle data) {
disconnect();
}
});
}
/**
* Disconnect from the remote screen without stopping the media playing. use release() for
* disconnect + stop.
*/
private void disconnect() {
clearStreamState();
clearMediaRoute();
if (mSessionStatusBroadcastReceiver != null) {
getContext().unregisterReceiver(mSessionStatusBroadcastReceiver);
mSessionStatusBroadcastReceiver = null;
}
if (mMediaStatusBroadcastReceiver != null) {
getContext().unregisterReceiver(mMediaStatusBroadcastReceiver);
mMediaStatusBroadcastReceiver = null;
}
clearConnectionFailureCallback();
stopWatchingRouteSelection();
removeAllListeners();
}
@Override
protected void onRouteSelectedEvent(MediaRouter router, RouteInfo route) {
Log.d(TAG, "Selected route %s", route);
if (!route.isSelected()) return;
RecordCastAction.castPlayRequested();
RecordCastAction.remotePlaybackDeviceSelected(
RecordCastAction.DEVICE_TYPE_CAST_GENERIC);
installBroadcastReceivers();
if (getMediaStateListener() == null) {
showCastError(route.getName());
release();
return;
}
if (route != getCurrentRoute()) {
registerRoute(route);
clearStreamState();
}
mPositionExtrapolator.clear();
notifyRouteSelected(route);
}
/*
* Although our custom implementation of the disconnect button doesn't need this, it is needed
* when the route is released due to, for example, another application stealing the route, or
* when we switch to a YouTube video on the same device.
*/
@Override
protected void onRouteUnselectedEvent(MediaRouter router, RouteInfo route) {
Log.d(TAG, "Unselected route %s", route);
// Preserve our best guess as to the final position; this is needed to reset the
// local position while switching back to local playback.
mPositionExtrapolator.onPaused();
if (getCurrentRoute() != null && route.getId().equals(getCurrentRoute().getId())) {
clearStreamState();
}
}
private void installBroadcastReceivers() {
if (mSessionStatusBroadcastReceiver == null) {
mSessionStatusBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
logIntent("Got a session broadcast intent from the MRP: ", intent);
Bundle statusBundle = intent.getExtras();
// Ignore null status bundles.
if (statusBundle == null) return;
// Ignore the status of old sessions.
String sessionId = statusBundle.getString(MediaControlIntent.EXTRA_SESSION_ID);
if (mCurrentSessionId == null || !mCurrentSessionId.equals(sessionId)) return;
processSessionStatusBundle(statusBundle);
}
};
IntentFilter sessionBroadcastIntentFilter =
new IntentFilter(ACTION_RECEIVE_SESSION_STATUS_UPDATE);
sessionBroadcastIntentFilter.addCategory(mIntentCategory);
getContext().registerReceiver(mSessionStatusBroadcastReceiver,
sessionBroadcastIntentFilter);
}
if (mMediaStatusBroadcastReceiver == null) {
mMediaStatusBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
logIntent("Got a broadcast intent from the MRP: ", intent);
processMediaStatusBundle(intent.getExtras());
}
};
IntentFilter mediaBroadcastIntentFilter =
new IntentFilter(ACTION_RECEIVE_MEDIA_STATUS_UPDATE);
mediaBroadcastIntentFilter.addCategory(mIntentCategory);
getContext().registerReceiver(mMediaStatusBroadcastReceiver,
mediaBroadcastIntentFilter);
}
}
/**
* Called when the main activity receives an onDestroy() call.
*/
protected void onActivitiesDestroyed() {
ApplicationStatus.unregisterApplicationStateListener(mApplicationStateListener);
release();
}
/**
* Clear the session and the currently playing item (if any).
*/
protected void clearStreamState() {
mLocalVideoUri = null;
mCurrentSessionId = null;
clearItemState();
}
@Override
protected void clearItemState() {
// Note: do not clear the stream position, since this is still needed so
// that we can reset the local stream position to match.
super.clearItemState();
mCurrentItemId = null;
mPositionExtrapolator.clear();
mSeeking = false;
}
private void processSessionStatusBundle(Bundle statusBundle) {
MediaSessionStatus status = MediaSessionStatus.fromBundle(
statusBundle.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
int sessionState = status.getSessionState();
// If no change do nothing
if (sessionState == mSessionState) return;
mSessionState = sessionState;
switch (sessionState) {
case MediaSessionStatus.SESSION_STATE_ACTIVE:
if (mLocalVideoUri != null) {
startPlayback(mPreferredTitle, mStartPositionMillis);
}
break;
case MediaSessionStatus.SESSION_STATE_ENDED:
case MediaSessionStatus.SESSION_STATE_INVALIDATED:
for (UiListener listener : getUiListeners()) {
listener.onPlaybackStateChanged(PlayerState.INVALIDATED);
}
if (getMediaStateListener() != null) {
getMediaStateListener().onPlaybackStateChanged(PlayerState.INVALIDATED);
}
// Record the remaining time UMA first, otherwise the playback state will be cleared
// in release().
recordRemainingTimeUMA();
// Set the current session id to null so we don't send the stop intent.
mCurrentSessionId = null;
release();
break;
default:
break;
}
}
private void processMediaStatusBundle(Bundle statusBundle) {
if (statusBundle == null) return;
logBundle("processMediaStatusBundle: ", statusBundle);
String itemId = statusBundle.getString(MediaControlIntent.EXTRA_ITEM_ID);
if (itemId == null || !itemId.equals(mCurrentItemId)) return;
// Extract item metadata, if available.
if (statusBundle.containsKey(MediaControlIntent.EXTRA_ITEM_METADATA)) {
Bundle metadataBundle =
(Bundle) statusBundle.getParcelable(MediaControlIntent.EXTRA_ITEM_METADATA);
updateTitle(metadataBundle.getString(MediaItemMetadata.KEY_TITLE, mPreferredTitle));
}
// Extract the item status, if available.
if (statusBundle.containsKey(MediaControlIntent.EXTRA_ITEM_STATUS)) {
Bundle itemStatusBundle =
(Bundle) statusBundle.getParcelable(MediaControlIntent.EXTRA_ITEM_STATUS);
MediaItemStatus itemStatus = MediaItemStatus.fromBundle(itemStatusBundle);
logBundle("Received item status: ", itemStatusBundle);
updateState(itemStatus.getPlaybackState());
// Update the PositionExtrapolator that the playback state has changed.
if (itemStatus.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
mPositionExtrapolator.onResumed();
} else if (itemStatus.getPlaybackState() == MediaItemStatus.PLAYBACK_STATE_FINISHED) {
mPositionExtrapolator.onFinished();
} else {
mPositionExtrapolator.onPaused();
}
if ((getRemotePlayerState() == PlayerState.PAUSED)
|| (getRemotePlayerState() == PlayerState.PLAYING)
|| (getRemotePlayerState() == PlayerState.LOADING)) {
this.mCurrentItemId = itemId;
// duration can possibly be -1 if it's unknown, so cap to 0
long duration = Math.max(itemStatus.getContentDuration(), 0);
// update the position using the remote player's position
// duration can possibly be -1 if it's unknown, so cap to 0
long position = Math.min(Math.max(itemStatus.getContentPosition(), 0), duration);
// TODO(zqzhang): The GMS core currently uses SystemClock.uptimeMillis() as
// timestamp, which does not conform to the MediaRouter support library docs. See
// b/28378525 and
// http://developer.android.com/reference/android/support/v7/media/MediaItemStatus.html#getTimestamp().
// Override the timestamp with elapsedRealtime() by assuming the delay between the
// GMS core produces the MediaItemStatus and the code reaches here is short enough.
// long timestamp = itemStatus.getTimestamp();
long timestamp = SystemClock.elapsedRealtime();
notifyDurationUpdated(duration);
notifyPositionUpdated(position);
mPositionExtrapolator.onPositionInfoUpdated(duration, position, timestamp);
if (mSeeking) {
mSeeking = false;
if (getMediaStateListener() != null) getMediaStateListener().onSeekCompleted();
}
}
logExtraHttpInfo(itemStatus.getExtras());
}
}
/**
* Send the given intent to the current route. The result will be returned in the given
* ResultBundleHandler. This function will also check to see if the current route can handle the
* intent before sending it.
*
* @param intent the intent to send to the current route.
* @param bundleHandler contains the result of sending the intent
*/
private void sendIntentToRoute(final Intent intent, final ResultBundleHandler bundleHandler) {
if (getCurrentRoute() == null) {
logIntent("sendIntentToRoute ", intent);
Log.d(TAG, "The current route is null.");
if (bundleHandler != null) bundleHandler.onError(null, null);
return;
}
if (!getCurrentRoute().supportsControlRequest(intent)) {
logIntent("sendIntentToRoute ", intent);
Log.d(TAG, "The intent is not supported by the route: %s", getCurrentRoute());
if (bundleHandler != null) bundleHandler.onError(null, null);
return;
}
sendControlIntent(intent, bundleHandler);
}
private void sendControlIntent(final Intent intent, final ResultBundleHandler bundleHandler) {
Log.d(TAG, "Sending intent to %s %s", getCurrentRoute().getName(),
getCurrentRoute().getId());
logIntent("sendControlIntent ", intent);
if (getCurrentRoute().isDefault()) {
Log.d(TAG, "Route is default, not sending");
return;
}
getCurrentRoute().sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
@Override
public void onResult(Bundle data) {
if (data != null && bundleHandler != null) bundleHandler.onResult(data);
}
@Override
public void onError(String message, Bundle data) {
logControlRequestError(intent, message, data);
int errorCode = 0;
if (data != null) {
errorCode = data.getInt(CastMediaControlIntent.EXTRA_ERROR_CODE);
}
sendErrorToListeners(errorCode);
if (bundleHandler != null) bundleHandler.onError(message, data);
}
});
}
private void notifyDurationUpdated(long durationMillis) {
for (UiListener listener : getUiListeners()) {
listener.onDurationUpdated(durationMillis);
}
}
private void notifyPositionUpdated(long position) {
for (UiListener listener : getUiListeners()) {
listener.onPositionChanged(position);
}
}
private void recordRemainingTimeUMA() {
long duration = getDuration();
long remainingTime = Math.max(0, duration - getPosition());
// Duration has already been cleared.
if (getDuration() <= 0) return;
RecordCastAction.castEndedTimeRemaining(duration, remainingTime);
}
private String bundleToString(Bundle bundle) {
if (bundle == null) return "";
StringBuilder extras = new StringBuilder();
extras.append("[");
for (String key : bundle.keySet()) {
Object value = bundle.get(key);
String valueText = value == null ? "null" : value.toString();
if (value instanceof Bundle) valueText = bundleToString((Bundle) value);
extras.append(key).append("=").append(valueText).append(",");
}
extras.append("]");
return extras.toString();
}
@Override
protected void startCastingVideo() {
MediaStateListener listener = getMediaStateListener();
if (listener == null) return;
String url = listener.getSourceUrl();
Log.d(TAG, "startCastingVideo called, url = %s", url);
// checkIfPlayableRemotely will have rejected null URLs.
assert url != null;
RecordCastAction.castDomainAndRegistry(listener.getFrameUrl().toString());
mLocalVideoUri = Uri.parse(url);
mStartPositionMillis = listener.getStartPositionMillis();
playUri(listener.getTitle(), mStartPositionMillis);
}
private void configureNewSession(Bundle data) {
mCurrentSessionId = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
mSessionState = MediaSessionStatus.SESSION_STATE_INVALIDATED;
Log.d(TAG, "Got a session id: %s", mCurrentSessionId);
}
@Override
public void checkIfPlayableRemotely(final String sourceUrl, final String frameUrl,
final String cookies, String userAgent, final MediaValidationCallback callback) {
new MediaUrlResolver(new MediaUrlResolver.Delegate() {
@Override
public Uri getUri() {
return Uri.parse(sourceUrl);
}
@Override
public String getCookies() {
return cookies;
}
@Override
public void deliverResult(Uri uri, boolean playable) {
callback.onResult(playable, uri.toString(), frameUrl);
}
}, userAgent).execute();
}
@Override
public String getUriPlaying() {
if (mLocalVideoUri == null) return null;
return mLocalVideoUri.toString();
}
@RemovableInRelease
private void logBundle(String message, Bundle bundle) {
Log.d(TAG, message + bundleToString(bundle));
}
@RemovableInRelease
private void logControlRequestError(Intent intent, String message, Bundle data) {
// The intent may contain some PII so we don't want to log it in the released
// version by default.
Log.e(TAG, String.format(
"Error sending control request %s %s. Data: %s Error: %s", intent,
bundleToString(intent.getExtras()), bundleToString(data), message));
}
@RemovableInRelease
private void logExtraHttpInfo(Bundle extras) {
if (extras != null) {
if (extras.containsKey(MediaItemStatus.EXTRA_HTTP_STATUS_CODE)) {
int httpStatus = extras.getInt(MediaItemStatus.EXTRA_HTTP_STATUS_CODE);
Log.d(TAG, "HTTP status: %s", httpStatus);
}
if (extras.containsKey(MediaItemStatus.EXTRA_HTTP_RESPONSE_HEADERS)) {
Bundle headers = extras.getBundle(MediaItemStatus.EXTRA_HTTP_RESPONSE_HEADERS);
Log.d(TAG, "HTTP headers: %s", bundleToString(headers));
}
}
}
@RemovableInRelease
private void logIntent(String prefix, Intent intent) {
Log.d(TAG, prefix + intent + " extras: " + bundleToString(intent.getExtras()));
}
@RemovableInRelease
private void logMediaSessionStatus(Bundle data) {
MediaSessionStatus status = MediaSessionStatus.fromBundle(
data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
int sessionState = status.getSessionState();
Log.d(TAG, "Session state after ending session: %s", sessionState);
}
}