diff options
author | aiguha@chromium.org <aiguha@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-15 16:19:37 +0000 |
---|---|---|
committer | aiguha@chromium.org <aiguha@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-15 16:20:38 +0000 |
commit | 5f87658350501f279301d1558ea70f5211451ad2 (patch) | |
tree | 2c4a4d7d780350273e8fb0f4cabda455dcd0c65d /remoting/android/cast | |
parent | 4d98ceb6af7b48246bb3844073699ad857fea2bb (diff) | |
download | chromium_src-5f87658350501f279301d1558ea70f5211451ad2.zip chromium_src-5f87658350501f279301d1558ea70f5211451ad2.tar.gz chromium_src-5f87658350501f279301d1558ea70f5211451ad2.tar.bz2 |
Capabilities + Extensions + Cast Support for Android client
Support for:
1. Capability enumeration, negotiation and managment.
2. Generic client-side extensions, similar to HostExtension model in Chromoting host.
3. Interaction with Chromoting host's CastExtension and support for Cast Sender API
(prototype for Cast support).
Note:
The android app has four new dependencies: v4 support, v7 appcompat, v7 mediarouter,
google play services. The first three are in third_party/android_tools/,
but the last isn't. See crbug.com/403948. This CL will not break any builds
since the code is behind a gyp flag in remoting.gyp.
Review URL: https://codereview.chromium.org/451973002
Cr-Commit-Position: refs/heads/master@{#289897}
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@289897 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting/android/cast')
-rw-r--r-- | remoting/android/cast/AndroidManifest.xml.jinja2 | 46 | ||||
-rw-r--r-- | remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java | 477 |
2 files changed, 523 insertions, 0 deletions
diff --git a/remoting/android/cast/AndroidManifest.xml.jinja2 b/remoting/android/cast/AndroidManifest.xml.jinja2 new file mode 100644 index 0000000..f1ed533 --- /dev/null +++ b/remoting/android/cast/AndroidManifest.xml.jinja2 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="{{ APK_PACKAGE_NAME }}"> + <uses-sdk android:minSdkVersion="14" + android:targetSdkVersion="20"/> + <uses-permission android:name="android.permission.GET_ACCOUNTS"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/> + <uses-permission android:name="android.permission.USE_CREDENTIALS"/> + <application android:label="@string/product_name_android" + android:icon="@drawable/chromoting128" + android:theme="@android:style/Theme.Holo" + android:allowBackup="false"> + <meta-data + android:name="com.google.android.gms.version" + android:value="@integer/google_play_services_version" /> + <activity android:name="org.chromium.chromoting.Chromoting" + android:configChanges="orientation|screenSize" + android:theme="@style/MainTheme" + android:launchMode="singleTask"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity + android:name="org.chromium.chromoting.ThirdPartyTokenFetcher$OAuthRedirectActivity" + android:enabled="false" + android:noHistory="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + <data android:scheme="{{ APK_PACKAGE_NAME }}"/> + <data android:path="/oauthredirect/"/> + </intent-filter> + </activity> + <activity android:name="org.chromium.chromoting.Desktop" + android:configChanges="orientation|screenSize" + android:windowSoftInputMode="adjustResize" + android:theme="@style/Theme.AppCompat"/> + <activity android:name="org.chromium.chromoting.HelpActivity" + android:configChanges="orientation|screenSize" + android:uiOptions="splitActionBarWhenNarrow"/> + </application> +</manifest> 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 0000000..053ee63 --- /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(); + } + } +} |