Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(92)

Unified Diff: remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java

Issue 451973002: Capabilities + Extensions + Cast Host Extension Support for Android client (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Minor Fixes Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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..28dd16d7595b52574eee3b9976d17bad53f6de69
--- /dev/null
+++ b/remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java
@@ -0,0 +1,460 @@
+// 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 start our registered Cast Receiver App on a nearby Cast
+ * device, if the user chooses to do so.
+ */
+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 Context mContext = 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);
Lambros 2014/08/08 23:20:45 Yeah, I think this is too deeply nested. It's an a
aiguha 2014/08/14 18:35:05 Done.
+ Toast.makeText(
+ mContext, "Failed to connect to Cast device.",
+ Toast.LENGTH_SHORT).show();
+ } catch (IllegalStateException e) {
+ Toast.makeText(
+ mContext, "Failed to connect to Cast device.",
+ 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 {
Lambros 2014/08/08 23:20:45 private?
aiguha 2014/08/14 18:35:05 Done.
+ 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;
+ }
+ mContext = 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(
+ mContext, "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 (mContext == 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(
+ mContext, "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(
+ mContext, "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(mContext)
+ .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(
+ mContext, "Failed to reconnect to Cast device.", Toast.LENGTH_SHORT).show();
+ } catch (IllegalStateException e) {
+ Toast.makeText(
+ mContext, "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;
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698