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; |
+ } |
+} |