Chromium Code Reviews| Index: remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java |
| diff --git a/remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java b/remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..62d40f06fbef45d4464f6dc3c477446bfe4cc916 |
| --- /dev/null |
| +++ b/remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java |
| @@ -0,0 +1,459 @@ |
| +// 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.chromoting; |
| + |
| +import android.app.Activity; |
| +import android.content.Context; |
| +import android.os.Bundle; |
| +import android.support.v4.view.MenuItemCompat; |
| +import android.support.v7.app.MediaRouteActionProvider; |
| +import android.support.v7.media.MediaRouteSelector; |
| +import android.support.v7.media.MediaRouter; |
| +import android.support.v7.media.MediaRouter.RouteInfo; |
| +import android.util.Log; |
| +import android.view.Menu; |
| +import android.view.MenuItem; |
| +import android.widget.Toast; |
| + |
| +import com.google.android.gms.cast.ApplicationMetadata; |
| +import com.google.android.gms.cast.Cast; |
| +import com.google.android.gms.cast.Cast.Listener; |
| +import com.google.android.gms.cast.CastDevice; |
| +import com.google.android.gms.cast.CastMediaControlIntent; |
| +import com.google.android.gms.common.ConnectionResult; |
| +import com.google.android.gms.common.api.GoogleApiClient; |
| +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; |
| +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; |
| +import com.google.android.gms.common.api.ResultCallback; |
| +import com.google.android.gms.common.api.Status; |
| + |
| +import org.chromium.chromoting.jni.JniInterface; |
| + |
| +import java.io.IOException; |
| +import java.util.ArrayList; |
| +import java.util.List; |
| + |
| +/** |
| + * A handler that interacts with the Cast Extension of the Chromoting host using extension messages. |
| + * It uses the Cast Android Sender API to setup the related cast Cast Receiver App on a nearby, |
| + * user-selected cast device. |
| + */ |
| +public class CastExtensionHandler implements ClientExtension, ActivityLifecycleListener { |
| + |
| + /** Extension messages of this type will be handled by the CastExtensionHandler. */ |
| + public static final String EXTENSION_MSG_TYPE = "cast_message"; |
| + |
| + /** Tag used for logging. */ |
| + private static final String TAG = "CastExtensionHandler"; |
| + |
| + /** Application Id of the Cast Receiver App that will be run on the Cast device. */ |
| + private static final String RECEIVER_APP_ID = "8A1211E3"; |
| + |
| + /** Custom namespace that will be used to communicate with the Cast device. */ |
| + private static final String CHROMOTOCAST_NAMESPACE = "urn:x-cast:com.chromoting.cast.all"; |
| + |
| + /** Context that wil be used to initialize the MediaRouter and the GoogleApiClient. */ |
| + private static Context sContext = null; |
| + |
| + /** True if the application has been launched on the Cast device. */ |
| + private boolean mApplicationStarted; |
| + |
| + /** True if the client is temporarily in a disconnected state. */ |
| + private boolean mWaitingForReconnect; |
| + |
| + /** Object that allows routing of media to external devices including Google Cast devices. */ |
| + private MediaRouter mMediaRouter; |
| + |
| + /** Describes the capabilities of routes that the application might want to use. */ |
| + private MediaRouteSelector mMediaRouteSelector; |
| + |
| + /** Cast device selected by the user. */ |
| + private CastDevice mSelectedDevice; |
| + /** Object to receive callbacks about media routing changes. */ |
| + private MediaRouter.Callback mMediaRouterCallback; |
| + |
| + /** Listener for events related to the connected Cast device.*/ |
| + private Listener mCastClientListener; |
| + |
| + /** Object that handles Google Play services integration. */ |
| + private GoogleApiClient mApiClient; |
| + |
| + /** Callback object for connection changes with Google Play services. */ |
| + private ConnectionCallbacks mConnectionCallbacks; |
| + private OnConnectionFailedListener mConnectionFailedListener; |
| + |
| + /** Channel for receiving messages from the Cast device. */ |
| + private ChromotocastChannel mChromotocastChannel; |
| + |
| + /** Current session ID, if there is one. */ |
| + private String mSessionId; |
| + |
| + /** Queue of messages that are yet to be delivered to the Receiver App. */ |
| + private List<String> mChromotocastMessageQueue; |
| + |
| + /** |
| + * A callback class for receiving events about media routing changes. |
| + */ |
| + private class CustomMediaRouterCallback extends MediaRouter.Callback { |
| + |
| + @Override |
| + public void onRouteSelected(MediaRouter router, RouteInfo info) { |
| + mSelectedDevice = CastDevice.getFromBundle(info.getExtras()); |
| + String routeId = info.getId(); |
| + connectApiClient(); |
| + } |
| + |
| + @Override |
| + public void onRouteUnselected(MediaRouter router, RouteInfo info) { |
| + teardown(); |
| + mSelectedDevice = null; |
| + } |
| + } |
| + |
| + /** |
| + * A callback class for receiving events about client connections and disconnections from |
| + * Google Play services. |
| + */ |
| + private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { |
| + |
| + @Override |
| + public void onConnected(Bundle connectionHint) { |
| + if (mWaitingForReconnect) { |
| + mWaitingForReconnect = false; |
| + reconnectChannels(); |
| + } else { |
| + Cast.CastApi.launchApplication(mApiClient, "8A1211E3", false) |
| + .setResultCallback( |
| + new ResultCallback<Cast.ApplicationConnectionResult>() { |
| + @Override |
| + public void onResult(Cast.ApplicationConnectionResult result) { |
| + Status status = result.getStatus(); |
| + if (status.isSuccess()) { |
| + ApplicationMetadata applicationMetadata = |
| + result.getApplicationMetadata(); |
| + String sessionId = result.getSessionId(); |
| + String applicationStatus = result.getApplicationStatus(); |
| + boolean wasLaunched = result.getWasLaunched(); |
| + |
| + mApplicationStarted = true; |
| + mSessionId = sessionId; |
| + mChromotocastChannel = new ChromotocastChannel(); |
| + try { |
| + Cast.CastApi.setMessageReceivedCallbacks(mApiClient, |
| + mChromotocastChannel.getNamespace(), |
| + mChromotocastChannel); |
| + sendPendingMessagesToCastDevice(); |
| + } catch (IOException e) { |
| + Log.e(TAG, "Exception while creating channel.", e); |
| + Toast.makeText( |
| + sContext, "Failed to connect to Cast device.", |
| + Toast.LENGTH_SHORT).show(); |
| + } catch (IllegalStateException e) { |
| + Toast.makeText( |
| + sContext, "Failed to connect to Cast device.", |
|
aiguha
2014/08/08 05:13:50
This (and some others like it) looks awkward, but
|
| + Toast.LENGTH_SHORT).show(); |
| + } |
| + } else { |
| + teardown(); |
| + } |
| + } |
| + }); |
| + } |
| + } |
| + |
| + @Override |
| + public void onConnectionSuspended(int cause) { |
| + mWaitingForReconnect = true; |
| + } |
| + } |
| + |
| + /** |
| + * A listener for connection failures. |
| + */ |
| + private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { |
| + |
| + @Override |
| + public void onConnectionFailed(ConnectionResult arg0) { |
| + teardown(); |
| + } |
| + |
| + } |
| + |
| + /** |
| + * A channel for communication with the Cast device on the CHROMOTOCAST_NAMESPACE. |
| + */ |
| + class ChromotocastChannel implements Cast.MessageReceivedCallback { |
| + public String getNamespace() { |
| + return CHROMOTOCAST_NAMESPACE; |
| + } |
| + |
| + @Override |
| + public void onMessageReceived(CastDevice castDevice, String namespace, |
| + String message) { |
| + Log.d(TAG, "onMessageReceived: " + namespace + ":" + message); |
| + if (namespace.equals(CHROMOTOCAST_NAMESPACE)) { |
| + sendMessageToHost(message); |
| + } |
| + else { |
| + Log.i(TAG, "Unknown namespace message: " + message); |
| + } |
| + } |
| + } |
| + |
| + /** Constructs the CastExtensionHandler with an empty message queue. */ |
| + public CastExtensionHandler() { |
| + mChromotocastMessageQueue = new ArrayList<String>(); |
| + } |
| + |
| + /** ClientExtension implementation. */ |
| + |
| + @Override |
| + public String getCapability() { |
| + return Capabilities.CAST_CAPABILITY; |
| + } |
| + |
| + @Override |
| + public boolean onExtensionMessage(String type, String data) { |
| + if (type.equals(EXTENSION_MSG_TYPE)) { |
| + mChromotocastMessageQueue.add(data); |
| + if (mApplicationStarted) { |
| + sendPendingMessagesToCastDevice(); |
| + } |
| + return true; |
| + } |
| + return false; |
| + } |
| + |
| + @Override |
| + public ActivityLifecycleListener onActivityAcceptingListener(Activity activity) { |
| + return this; |
| + } |
| + |
| + /** ActivityLifecycleListener implementation. */ |
| + |
| + /** Initializes the MediaRouter and related objects using the provided activity Context. */ |
| + @Override |
| + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { |
| + if (activity == null) { |
| + Log.w(TAG, "Initialization attemped without activity."); |
| + return; |
| + } |
| + sContext = activity; |
| + mMediaRouter = MediaRouter.getInstance(activity); |
| + mMediaRouteSelector = new MediaRouteSelector.Builder() |
| + .addControlCategory(CastMediaControlIntent.categoryForCast(RECEIVER_APP_ID)) |
| + .build(); |
| + mMediaRouterCallback = new CustomMediaRouterCallback(); |
| + } |
| + |
| + @Override |
| + public void onActivityDestroyed(Activity activity) { |
| + teardown(); |
| + } |
| + |
| + @Override |
| + public void onActivityPaused(Activity activity) { |
| + removeMediaRouterCallback(); |
| + } |
| + |
| + @Override |
| + public void onActivityResumed(Activity activity) { |
| + addMediaRouterCallback(); |
| + } |
| + |
| + @Override |
| + public void onActivitySaveInstanceState (Activity activity, Bundle outState) {} |
| + |
| + @Override |
| + public void onActivityStarted(Activity activity) { |
| + addMediaRouterCallback(); |
| + } |
| + |
| + @Override |
| + public void onActivityStopped(Activity activity) { |
| + removeMediaRouterCallback(); |
| + } |
| + |
| + /** Sets up the cast menu item on the application. */ |
| + @Override |
| + public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu) { |
| + MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item); |
| + if (mediaRouteMenuItem == null) { |
| + Log.w(TAG, "Cannot setup route selector without menu item."); |
| + return false; |
| + } |
| + MediaRouteActionProvider mediaRouteActionProvider = |
| + (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem); |
| + mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector); |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onActivityOptionsItemSelected(Activity activity, MenuItem item) { |
| + if (item.getItemId() == R.id.actionbar_disconnect) { |
| + removeMediaRouterCallback(); |
| + Toast.makeText( |
| + sContext, "Closing connection to Cast device.", Toast.LENGTH_SHORT).show(); |
| + teardown(); |
| + return true; |
| + } |
| + return false; |
| + } |
| + |
| + |
| + /** Extension Message Handling logic */ |
| + |
| + /** Sends a message to the Chromoting host. */ |
| + private void sendMessageToHost(String data) { |
| + JniInterface.sendExtensionMessage(EXTENSION_MSG_TYPE, data); |
| + } |
| + |
| + /** Sends any messages in the message queue to the Cast device. */ |
| + private void sendPendingMessagesToCastDevice() { |
| + for (String msg : mChromotocastMessageQueue) { |
| + sendMessageToCastDevice(msg); |
| + } |
| + mChromotocastMessageQueue.clear(); |
| + } |
| + |
| + /** Cast Sender API logic */ |
| + |
| + /** |
| + * Initializes and connects to the Google Play Services service. |
| + */ |
| + private void connectApiClient() { |
| + if (sContext == null) { |
| + Log.e(TAG, "Cannot connect Api Client without context."); |
| + return; |
| + } |
| + mCastClientListener = new Cast.Listener() { |
| + @Override |
| + public void onApplicationStatusChanged() { |
| + try { |
| + if (mApiClient != null) { |
| + Log.d(TAG, "onApplicationStatusChanged: " |
| + + Cast.CastApi.getApplicationStatus(mApiClient)); |
| + } |
| + } catch (IllegalStateException e) { |
| + Toast.makeText( |
| + sContext, "Lost connection to Cast device.", Toast.LENGTH_SHORT).show(); |
| + teardown(); |
| + } |
| + } |
| + |
| + @Override |
| + public void onVolumeChanged() { |
| + try { |
| + if (mApiClient != null) { |
| + Log.d(TAG, "onVolumeChanged: " + Cast.CastApi.getVolume(mApiClient)); |
| + } |
| + } catch (IllegalStateException e) { |
| + Toast.makeText( |
| + sContext, "Lost connection to Cast device.", Toast.LENGTH_SHORT).show(); |
| + teardown(); |
| + } |
| + } |
| + |
| + @Override |
| + public void onApplicationDisconnected(int errorCode) { |
| + teardown(); |
| + } |
| + }; |
| + Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions |
| + .builder(mSelectedDevice, mCastClientListener) |
| + .setVerboseLoggingEnabled(true); |
| + mConnectionCallbacks = new ConnectionCallbacks(); |
| + mConnectionFailedListener = new ConnectionFailedListener(); |
| + mApiClient = new GoogleApiClient.Builder(sContext) |
| + .addApi(Cast.API, apiOptionsBuilder.build()) |
| + .addConnectionCallbacks(mConnectionCallbacks) |
| + .addOnConnectionFailedListener(mConnectionFailedListener) |
| + .build(); |
| + mApiClient.connect(); |
| + } |
| + |
| + /** |
| + * Adds the callback object to the MediaRouter. Called when the owning activity starts/resumes. |
| + */ |
| + private void addMediaRouterCallback() { |
| + if (mMediaRouter != null && mMediaRouteSelector != null && mMediaRouterCallback != null) { |
| + mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, |
| + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); |
| + } |
| + } |
| + |
| + /** |
| + * Removes the callback object from the MediaRouter. Called when the owning activity |
| + * stops/pauses. |
| + */ |
| + private void removeMediaRouterCallback() { |
| + if (mMediaRouter != null && mMediaRouterCallback != null) { |
| + mMediaRouter.removeCallback(mMediaRouterCallback); |
| + } |
| + } |
| + |
| + |
| + /** Sends a message to the Cast device on the CHROMOTOCAST_NAMESPACE. */ |
| + private void sendMessageToCastDevice(String message) { |
| + if (mApiClient != null && mChromotocastChannel != null) { |
| + Cast.CastApi.sendMessage(mApiClient, mChromotocastChannel.getNamespace(), message) |
| + .setResultCallback( |
| + new ResultCallback<Status>() { |
| + @Override |
| + public void onResult(Status result) { |
| + if (!result.isSuccess()) { |
| + Log.e(TAG, "Failed to send message to cast device."); |
| + } |
| + } |
| + }); |
| + } |
| + } |
| + |
| + /** Restablishes connected channels, which must be done when resuming a connection. */ |
| + private void reconnectChannels() { |
| + if (mApiClient != null && mChromotocastChannel != null) { |
| + try { |
| + Cast.CastApi.setMessageReceivedCallbacks(mApiClient, |
| + mChromotocastChannel.getNamespace(), |
| + mChromotocastChannel); |
| + sendPendingMessagesToCastDevice(); |
| + } catch (IOException e) { |
| + Log.e(TAG, "Exception while creating channel.", e); |
| + Toast.makeText( |
| + sContext, "Failed to reconnect to Cast device.", Toast.LENGTH_SHORT).show(); |
| + } catch (IllegalStateException e) { |
| + Toast.makeText( |
| + sContext, "Lost connection to Cast device.", Toast.LENGTH_SHORT).show(); |
| + } |
| + } |
| + } |
| + |
| + /** Stops the running application on the Cast device and tears down relevant objects. */ |
| + private void teardown() { |
| + if (mApiClient != null) { |
| + if (mApplicationStarted) { |
| + if (mApiClient.isConnected()) { |
| + try { |
| + Cast.CastApi.stopApplication(mApiClient, mSessionId); |
| + if (mChromotocastChannel != null) { |
| + Cast.CastApi.removeMessageReceivedCallbacks( |
| + mApiClient, |
| + mChromotocastChannel.getNamespace()); |
| + mChromotocastChannel = null; |
| + } |
| + } catch (IOException e) { |
| + Log.e(TAG, "Exception while removing channel.", e); |
| + } |
| + mApiClient.disconnect(); |
| + } |
| + mApplicationStarted = false; |
| + } |
| + mApiClient = null; |
| + } |
| + mSelectedDevice = null; |
| + mWaitingForReconnect = false; |
| + mSessionId = null; |
| + } |
| +} |