// 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.content.Context; import android.os.Bundle; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.gms.cast.Cast; import com.google.android.gms.cast.CastStatusCodes; 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.Log; import org.chromium.chrome.browser.media.router.ChromeMediaRouter; import org.chromium.chrome.browser.media.router.MediaRoute; /** * 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> { 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 class CastListener extends Cast.Listener { private CastSession mSession; CastListener() {} void setSession(CastSession session) { mSession = session; } @Override public void onApplicationStatusChanged() { if (mSession == null) return; mSession.updateSessionStatus(); } @Override public void onApplicationMetadataChanged(ApplicationMetadata metadata) { if (mSession == null) return; mSession.updateSessionStatus(); } @Override // TODO(crbug.com/635567): Fix this properly. @SuppressLint("DefaultLocale") public void onApplicationDisconnected(int errorCode) { if (errorCode != CastStatusCodes.SUCCESS) { Log.e(TAG, String.format( "Application disconnected with: %d", errorCode)); } // This callback can be called more than once if the application is stopped from Chrome. if (mSession == null) return; mSession.stopApplication(); mSession = null; } @Override public void onVolumeChanged() { if (mSession == null) return; mSession.onVolumeChanged(); } } 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 CastMediaRouteProvider mRouteProvider; private final CastListener mCastListener = new CastListener(); private GoogleApiClient mApiClient; private int mState = STATE_IDLE; /** * 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 routeProvider The instance of {@link CastMediaRouteProvider} handling the request. */ public CreateRouteRequest( MediaSource source, MediaSink sink, String presentationId, String origin, int tabId, boolean isIncognito, int requestId, CastMediaRouteProvider routeProvider) { assert source != null; assert sink != null; mSource = source; mSink = sink; mPresentationId = presentationId; mOrigin = origin; mTabId = tabId; mIsIncognito = isIncognito; mRequestId = requestId; mRouteProvider = routeProvider; } 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; } /** * Starts the process of launching the application on the Cast device. * @param applicationContext application context * implementation provided by the caller. */ public void start(Context applicationContext) { assert applicationContext != null; if (mState != STATE_IDLE) throwInvalidState(); mApiClient = createApiClient(mCastListener, applicationContext); mApiClient.connect(); mState = STATE_CONNECTING_TO_API; } @Override public void onConnected(Bundle connectionHint) { if (mState != STATE_CONNECTING_TO_API && mState != STATE_API_CONNECTION_SUSPENDED) { throwInvalidState(); } // TODO(avayvod): switch to using ConnectedTask class for GoogleApiClient operations. // See https://crbug.com/522478 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(); } } // TODO(avayvod): switch to using ConnectedTask class for GoogleApiClient operations. // See https://crbug.com/522478 @Override public void onConnectionSuspended(int cause) { mState = STATE_API_CONNECTION_SUSPENDED; } @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(); } mState = STATE_LAUNCH_SUCCEEDED; reportSuccess(result); } // TODO(avayvod): switch to using ConnectedTask class for GoogleApiClient operations. // See https://crbug.com/522478 @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(); } private GoogleApiClient createApiClient(Cast.Listener listener, Context context) { Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions .builder(mSink.getDevice(), listener) // TODO(avayvod): hide this behind the flag or remove .setVerboseLoggingEnabled(true); return new GoogleApiClient.Builder(context) .addApi(Cast.API, apiOptionsBuilder.build()) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); } private PendingResult<Cast.ApplicationConnectionResult> launchApplication( GoogleApiClient apiClient, String appId, boolean relaunchIfRunning) { return Cast.CastApi.launchApplication(apiClient, appId, relaunchIfRunning); } // 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 = new CastSessionImpl( mApiClient, result.getSessionId(), result.getApplicationMetadata(), result.getApplicationStatus(), mSink.getDevice(), mOrigin, mTabId, mIsIncognito, mSource, mRouteProvider); mCastListener.setSession(session); mRouteProvider.onSessionCreated(session); terminate(); } private void reportError() { if (mState == STATE_TERMINATED) throwInvalidState(); assert mRouteProvider != null; mRouteProvider.onLaunchError(); terminate(); } private void terminate() { mApiClient.unregisterConnectionCallbacks(this); mApiClient.unregisterConnectionFailedListener(this); mState = STATE_TERMINATED; } }