blob: 9e485967139b6bd0b00930bb9b32eef957f8c987 [file] [log] [blame]
// 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.
package org.chromium.chrome.browser.media.router.cast;
import android.annotation.SuppressLint;
import android.os.Bundle;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.LaunchOptions;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Status;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.chrome.browser.media.router.ChromeMediaRouter;
import org.chromium.chrome.browser.media.router.MediaRoute;
import org.chromium.chrome.browser.media.router.MediaSink;
import org.chromium.chrome.browser.media.router.MediaSource;
import org.chromium.chrome.browser.media.router.cast.remoting.RemotingCastSession;
import javax.annotation.Nullable;
/**
* Establishes a {@link MediaRoute} by starting a Cast application represented by the given
* presentation URL. Reports success or failure to {@link ChromeMediaRouter}.
* Since there're numerous asynchronous calls involved in getting the application to launch
* the class is implemented as a state machine.
*/
public class CreateRouteRequest implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
ResultCallback<Cast.ApplicationConnectionResult>,
ChromeCastSessionManager.CastSessionLaunchRequest {
private static final String TAG = "MediaRouter";
private static final int STATE_IDLE = 0;
private static final int STATE_CONNECTING_TO_API = 1;
private static final int STATE_API_CONNECTION_SUSPENDED = 2;
private static final int STATE_LAUNCHING_APPLICATION = 3;
private static final int STATE_LAUNCH_SUCCEEDED = 4;
private static final int STATE_TERMINATED = 5;
private final MediaSource mSource;
private final MediaSink mSink;
private final String mPresentationId;
private final String mOrigin;
private final int mTabId;
private final boolean mIsIncognito;
private final int mRequestId;
private final CastMessageHandler mMessageHandler;
private final ChromeCastSessionManager.CastSessionManagerListener mSessionListener;
private final RequestedCastSessionType mSessionType;
private GoogleApiClient mApiClient;
private int mState = STATE_IDLE;
// Used to identify whether the request should launch a CastSessionImpl or a RemotingCastSession
// (based off of wheter the route creation was requested by a RemotingMediaRouteProvider or a
// CastMediaRouteProvider).
public enum RequestedCastSessionType { CAST, REMOTE }
/**
* Initializes the request.
* @param source The {@link MediaSource} defining the application to launch on the Cast device.
* @param sink The {@link MediaSink} identifying the selected Cast device.
* @param presentationId The presentation id assigned to the route by {@link ChromeMediaRouter}.
* @param origin The origin of the frame requesting the route.
* @param tabId The id of the tab containing the frame requesting the route.
* @param isIncognito Whether the route is being requested from an Incognito profile.
* @param requestId The id of the route creation request for tracking by
* {@link ChromeMediaRouter}.
* @param listener The listener that should be notified of session/route creation changes.
* @param messageHandler A message handler (used to create CastSessionImpl instances).
*/
public CreateRouteRequest(MediaSource source, MediaSink sink, String presentationId,
String origin, int tabId, boolean isIncognito, int requestId,
ChromeCastSessionManager.CastSessionManagerListener listener,
RequestedCastSessionType sessionType, @Nullable CastMessageHandler messageHandler) {
assert source != null;
assert sink != null;
mSource = source;
mSink = sink;
mPresentationId = presentationId;
mOrigin = origin;
mTabId = tabId;
mIsIncognito = isIncognito;
mRequestId = requestId;
mSessionListener = listener;
mSessionType = sessionType;
mMessageHandler = messageHandler;
}
public MediaSource getSource() {
return mSource;
}
public MediaSink getSink() {
return mSink;
}
public String getPresentationId() {
return mPresentationId;
}
public String getOrigin() {
return mOrigin;
}
public int getTabId() {
return mTabId;
}
public boolean isIncognito() {
return mIsIncognito;
}
public int getNativeRequestId() {
return mRequestId;
}
/////////////////////////////////////////////////////////////////////////////////////////////
// ChromeCastSessionManager.CastSessionLaunchRequest implementation.
/**
* Returns the object that should be notified of session changes that result
* from this route creation request.
*/
@Override
public ChromeCastSessionManager.CastSessionManagerListener getSessionListener() {
return mSessionListener;
}
/**
* Starts the process of launching the application on the Cast device.
*/
@Override
public void start(Cast.Listener castListener) {
if (mState != STATE_IDLE) throwInvalidState();
mApiClient = createApiClient(castListener);
mApiClient.connect();
mState = STATE_CONNECTING_TO_API;
}
/////////////////////////////////////////////////////////////////////////////////////////////
// GoogleApiClient.* implementations.
@Override
public void onConnected(Bundle connectionHint) {
if (mState != STATE_CONNECTING_TO_API && mState != STATE_API_CONNECTION_SUSPENDED) {
throwInvalidState();
}
if (mState == STATE_API_CONNECTION_SUSPENDED) return;
try {
launchApplication(mApiClient, mSource.getApplicationId(), true)
.setResultCallback(this);
mState = STATE_LAUNCHING_APPLICATION;
} catch (Exception e) {
Log.e(TAG, "Launch application failed: %s", mSource.getApplicationId(), e);
reportError();
}
}
@Override
public void onConnectionSuspended(int cause) {
mState = STATE_API_CONNECTION_SUSPENDED;
}
@Override
public void onConnectionFailed(ConnectionResult result) {
if (mState != STATE_CONNECTING_TO_API) throwInvalidState();
Log.e(TAG, "GoogleApiClient connection failed: %d, %b", result.getErrorCode(),
result.hasResolution());
reportError();
}
/**
* ResultCallback<Cast.ApplicationConnectionResult> implementation.
*/
@Override
public void onResult(Cast.ApplicationConnectionResult result) {
if (mState != STATE_LAUNCHING_APPLICATION
&& mState != STATE_API_CONNECTION_SUSPENDED) {
throwInvalidState();
}
Status status = result.getStatus();
if (!status.isSuccess()) {
Log.e(TAG, "Launch application failed with status: %s, %d, %s",
mSource.getApplicationId(), status.getStatusCode(), status.getStatusMessage());
reportError();
return;
}
mState = STATE_LAUNCH_SUCCEEDED;
reportSuccess(result);
}
private GoogleApiClient createApiClient(Cast.Listener listener) {
Cast.CastOptions.Builder apiOptionsBuilder =
new Cast.CastOptions.Builder(mSink.getDevice(), listener)
// TODO(avayvod): hide this behind the flag or remove
.setVerboseLoggingEnabled(true);
return new GoogleApiClient.Builder(ContextUtils.getApplicationContext())
.addApi(Cast.API, apiOptionsBuilder.build())
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
private PendingResult<Cast.ApplicationConnectionResult> launchApplication(
GoogleApiClient apiClient,
String appId,
boolean relaunchIfRunning) {
LaunchOptions.Builder builder = new LaunchOptions.Builder();
return Cast.CastApi.launchApplication(apiClient, appId,
builder.setRelaunchIfRunning(relaunchIfRunning)
.build());
}
// TODO(crbug.com/635567): Fix this properly.
@SuppressLint("DefaultLocale")
private void throwInvalidState() {
throw new RuntimeException(String.format("Invalid state: %d", mState));
}
private void reportSuccess(Cast.ApplicationConnectionResult result) {
if (mState != STATE_LAUNCH_SUCCEEDED) throwInvalidState();
CastSession session = null;
switch (mSessionType) {
case CAST:
session = new CastSessionImpl(mApiClient, result.getSessionId(),
result.getApplicationMetadata(), result.getApplicationStatus(),
mSink.getDevice(), mOrigin, mTabId, mIsIncognito, mSource, mMessageHandler);
break;
case REMOTE:
session = new RemotingCastSession(mApiClient, result.getSessionId(),
result.getApplicationMetadata(), result.getApplicationStatus(),
mSink.getDevice(), mOrigin, mTabId, mIsIncognito, mSource);
break;
}
ChromeCastSessionManager.get().onSessionStarted(session);
terminate();
}
private void reportError() {
if (mState == STATE_TERMINATED) throwInvalidState();
ChromeCastSessionManager.get().onSessionStartFailed();
terminate();
}
private void terminate() {
mApiClient.unregisterConnectionCallbacks(this);
mApiClient.unregisterConnectionFailedListener(this);
mState = STATE_TERMINATED;
}
}