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

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 Fix 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
« no previous file with comments | « remoting/android/cast/AndroidManifest.xml.jinja2 ('k') | remoting/android/java/AndroidManifest.xml.jinja2 » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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..053ee6345fad2fdaf6b1087e5fa6cdf8914a8849
--- /dev/null
+++ b/remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java
@@ -0,0 +1,477 @@
+// 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.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.cast.CastStatusCodes;
+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.
+ * TODO(aiguha): Use com.google.chromeremotedesktop for official builds.
+ */
+ 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 objects 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;
+
+ /** Current status of the application, if any. */
+ private String mApplicationStatus;
+
+ /**
+ * A callback class for receiving events about media routing.
+ */
+ private class CustomMediaRouterCallback extends MediaRouter.Callback {
+ @Override
+ public void onRouteSelected(MediaRouter router, RouteInfo info) {
+ mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
+ connectApiClient();
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, RouteInfo info) {
+ tearDown();
+ mSelectedDevice = null;
+ }
+ }
+
+ /**
+ * A callback class for receiving the result of launching an application on the user-selected
+ * Google Cast device.
+ */
+ private class ApplicationConnectionResultCallback implements
+ ResultCallback<Cast.ApplicationConnectionResult> {
+ @Override
+ public void onResult(Cast.ApplicationConnectionResult result) {
+ Status status = result.getStatus();
+ if (!status.isSuccess()) {
+ tearDown();
+ return;
+ }
+
+ mSessionId = result.getSessionId();
+ mApplicationStatus = result.getApplicationStatus();
+ mApplicationStarted = result.getWasLaunched();
+ mChromotocastChannel = new ChromotocastChannel();
+
+ try {
+ Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
+ mChromotocastChannel.getNamespace(), mChromotocastChannel);
+ sendPendingMessagesToCastDevice();
+ } catch (IOException e) {
+ showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
+ tearDown();
+ } catch (IllegalStateException e) {
+ showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
+ tearDown();
+ }
+ }
+ }
+
+ /**
+ * 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();
+ return;
+ }
+ Cast.CastApi.launchApplication(mApiClient, RECEIVER_APP_ID, false).setResultCallback(
+ new ApplicationConnectionResultCallback());
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ mWaitingForReconnect = true;
+ }
+ }
+
+ /**
+ * A listener for failures to connect with Google Play Services.
+ */
+ private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener {
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ Log.e(TAG, String.format("Google Play Service connection failed: %s", result));
+
+ tearDown();
+ }
+
+ }
+
+ /**
+ * A channel for communication with the Cast device on the CHROMOTOCAST_NAMESPACE.
+ */
+ private class ChromotocastChannel implements Cast.MessageReceivedCallback {
+
+ /**
+ * Returns the namespace associated with this channel.
+ */
+ public String getNamespace() {
+ return CHROMOTOCAST_NAMESPACE;
+ }
+
+ @Override
+ public void onMessageReceived(CastDevice castDevice, String namespace, String message) {
+ if (namespace.equals(CHROMOTOCAST_NAMESPACE)) {
+ sendMessageToHost(message);
+ }
+ }
+ }
+
+ /**
+ * A listener for changes when connected to a Google Cast device.
+ */
+ private class CastClientListener extends Cast.Listener {
+ @Override
+ public void onApplicationStatusChanged() {
+ try {
+ if (mApiClient != null) {
+ mApplicationStatus = Cast.CastApi.getApplicationStatus(mApiClient);
+ }
+ } catch (IllegalStateException e) {
+ showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
+ tearDown();
+ }
+ }
+
+ @Override
+ public void onVolumeChanged() {} // Changes in volume do not affect us.
+
+ @Override
+ public void onApplicationDisconnected(int errorCode) {
+ if (errorCode != CastStatusCodes.SUCCESS) {
+ Log.e(TAG, String.format("Application disconnected with: %d", errorCode));
+ }
+ tearDown();
+ }
+ }
+
+ /**
+ * Constructs a 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) {
+ 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();
+ }
+
+ @Override
+ public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu) {
+ // Find the cast button in the menu.
+ MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
+ if (mediaRouteMenuItem == null) {
+ return false;
+ }
+
+ // Setup a MediaRouteActionProvider using the button.
+ 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();
+ showToast(R.string.connection_to_cast_closed, Toast.LENGTH_SHORT);
+ 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 Google Play Services.
+ */
+ private void connectApiClient() {
+ if (mContext == null) {
+ return;
+ }
+ mCastClientListener = new CastClientListener();
+ mConnectionCallbacks = new ConnectionCallbacks();
+ mConnectionFailedListener = new ConnectionFailedListener();
+
+ Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
+ .builder(mSelectedDevice, mCastClientListener)
+ .setVerboseLoggingEnabled(true);
+
+ 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) {
+ return;
+ }
+ 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 the chromotocast message channel, so we can continue communicating with the
+ * Google Cast device. This must be called when resuming a connection.
+ */
+ private void reconnectChannels() {
+ if (mApiClient == null && mChromotocastChannel == null) {
+ return;
+ }
+ try {
+ Cast.CastApi.setMessageReceivedCallbacks(
+ mApiClient,mChromotocastChannel.getNamespace(),mChromotocastChannel);
+ sendPendingMessagesToCastDevice();
+ } catch (IOException e) {
+ showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
+ } catch (IllegalStateException e) {
+ showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
+ }
+ }
+
+ /**
+ * Stops the running application on the Google Cast device and performs the required tearDown
+ * sequence.
+ */
+ private void tearDown() {
+ if (mApiClient != null && mApplicationStarted && mApiClient.isConnected()) {
+ Cast.CastApi.stopApplication(mApiClient, mSessionId);
+ if (mChromotocastChannel != null) {
+ try {
+ Cast.CastApi.removeMessageReceivedCallbacks(
+ mApiClient, mChromotocastChannel.getNamespace());
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to remove chromotocast channel.");
+ }
+ }
+ mApiClient.disconnect();
+ }
+ mChromotocastChannel = null;
+ mApplicationStarted = false;
+ mApiClient = null;
+ mSelectedDevice = null;
+ mWaitingForReconnect = false;
+ mSessionId = null;
+ }
+
+ /**
+ * Makes a toast using the given message and duration.
+ */
+ private void showToast(int messageId, int duration) {
+ if (mContext != null) {
+ Toast.makeText(mContext, mContext.getString(messageId), duration).show();
+ }
+ }
+}
« no previous file with comments | « remoting/android/cast/AndroidManifest.xml.jinja2 ('k') | remoting/android/java/AndroidManifest.xml.jinja2 » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698