summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraiguha@chromium.org <aiguha@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-15 16:19:37 +0000
committeraiguha@chromium.org <aiguha@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-08-15 16:20:38 +0000
commit5f87658350501f279301d1558ea70f5211451ad2 (patch)
tree2c4a4d7d780350273e8fb0f4cabda455dcd0c65d
parent4d98ceb6af7b48246bb3844073699ad857fea2bb (diff)
downloadchromium_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
-rw-r--r--remoting/android/cast/AndroidManifest.xml.jinja246
-rw-r--r--remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java477
-rw-r--r--remoting/android/java/AndroidManifest.xml.jinja23
-rw-r--r--remoting/android/java/res/menu/desktop_actionbar.xml17
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/ActivityLifecycleListener.java38
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/Capabilities.java15
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/CapabilityManager.java157
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/ClientExtension.java32
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/Desktop.java43
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/DummyActivityLifecycleListener.java48
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/DummyClientExtension.java28
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java41
-rw-r--r--remoting/client/jni/chromoting_jni_instance.cc35
-rw-r--r--remoting/client/jni/chromoting_jni_instance.h10
-rw-r--r--remoting/client/jni/chromoting_jni_runtime.cc42
-rw-r--r--remoting/client/jni/chromoting_jni_runtime.h11
-rw-r--r--remoting/remoting.gyp3
-rw-r--r--remoting/remoting_android.gypi79
-rw-r--r--remoting/resources/remoting_strings.grd9
19 files changed, 1110 insertions, 24 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();
+ }
+ }
+}
diff --git a/remoting/android/java/AndroidManifest.xml.jinja2 b/remoting/android/java/AndroidManifest.xml.jinja2
index 2279574..836533c 100644
--- a/remoting/android/java/AndroidManifest.xml.jinja2
+++ b/remoting/android/java/AndroidManifest.xml.jinja2
@@ -34,7 +34,8 @@
</activity>
<activity android:name="org.chromium.chromoting.Desktop"
android:configChanges="orientation|screenSize"
- android:windowSoftInputMode="adjustResize"/>
+ android:windowSoftInputMode="adjustResize"
+ android:theme="@style/Theme.AppCompat"/>
<activity android:name="org.chromium.chromoting.HelpActivity"
android:configChanges="orientation|screenSize"
android:uiOptions="splitActionBarWhenNarrow"/>
diff --git a/remoting/android/java/res/menu/desktop_actionbar.xml b/remoting/android/java/res/menu/desktop_actionbar.xml
index 3b4cc64..6596f98 100644
--- a/remoting/android/java/res/menu/desktop_actionbar.xml
+++ b/remoting/android/java/res/menu/desktop_actionbar.xml
@@ -6,26 +6,31 @@
-->
<!--Action bar buttons for the Android app's remote desktop viewer-->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:id="@+id/media_route_menu_item"
+ android:title="@string/cast_button_title"
+ app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"
+ app:showAsAction="always"/>
<item android:id="@+id/actionbar_keyboard"
android:title="@string/show_hide_keyboard"
android:icon="@drawable/ic_action_keyboard"
- android:showAsAction="ifRoom"/>
+ app:showAsAction="ifRoom"/>
<item android:id="@+id/actionbar_hide"
android:title="@string/full_screen"
android:icon="@drawable/ic_action_full_screen"
- android:showAsAction="ifRoom"/>
+ app:showAsAction="ifRoom"/>
<item android:id="@+id/actionbar_send_ctrl_alt_del"
android:title="@string/send_ctrl_alt_del"
- android:showAsAction="withText"/>
+ app:showAsAction="withText"/>
<item android:id="@+id/actionbar_disconnect"
android:title="@string/disconnect_myself_button"
- android:showAsAction="withText"/>
+ app:showAsAction="withText"/>
<!--
The Help option must be the final menu item and it must only appear in
the action-bar overflow menu.
-->
<item android:id="@+id/actionbar_help"
android:title="@string/actionbar_help"
- android:showAsAction="never"/>
+ app:showAsAction="never"/>
</menu>
diff --git a/remoting/android/java/src/org/chromium/chromoting/ActivityLifecycleListener.java b/remoting/android/java/src/org/chromium/chromoting/ActivityLifecycleListener.java
new file mode 100644
index 0000000..bec5b7b0
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/ActivityLifecycleListener.java
@@ -0,0 +1,38 @@
+// 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.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+
+/**
+ * Interface to listen to receive events of an activity's lifecycle and options menu. This interface
+ * is similar to Application.ActivityLifecycleCallbacks, but is inherently different. This interface
+ * is intended to act as a listener for a specific Activity. The other is intended as a generic
+ * listener to be registered at the Application level, for all Activities' lifecycles.
+ */
+public interface ActivityLifecycleListener {
+
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState);
+
+ public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu);
+
+ public void onActivityDestroyed(Activity activity);
+
+ public boolean onActivityOptionsItemSelected(Activity activity, MenuItem item);
+
+ public void onActivityPaused(Activity activity);
+
+ public void onActivityResumed(Activity activity);
+
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState);
+
+ public void onActivityStarted(Activity activity);
+
+ public void onActivityStopped(Activity activity);
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/Capabilities.java b/remoting/android/java/src/org/chromium/chromoting/Capabilities.java
new file mode 100644
index 0000000..cff3d9e
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/Capabilities.java
@@ -0,0 +1,15 @@
+// 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;
+
+/**
+ * The list of all capabilities that might be supported by the Chromoting host
+ * and client. As more capabilities are supported on the android client,
+ * they can be enumerated here. This list must be kept synced with the
+ * Chromoting host.
+ */
+public class Capabilities {
+ public static final String CAST_CAPABILITY = "casting";
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/CapabilityManager.java b/remoting/android/java/src/org/chromium/chromoting/CapabilityManager.java
new file mode 100644
index 0000000..932b385
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/CapabilityManager.java
@@ -0,0 +1,157 @@
+// 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.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A manager for the capabilities of the Android client. Based on the negotiated set of
+ * capabilities, it creates the associated ClientExtensions, and enables their communication with
+ * the Chromoting host by dispatching extension messages appropriately.
+ *
+ * The CapabilityManager mirrors how the Chromoting host handles extension messages. For each
+ * incoming extension message, runs through a list of HostExtensionSession objects, giving each one
+ * a chance to handle the message.
+ *
+ * The CapabilityManager is a singleton class so we can manage client extensions on an application
+ * level. The singleton object may be used from multiple Activities, thus allowing it to support
+ * different capabilities at different stages of the application.
+ */
+public class CapabilityManager {
+
+ /** Lazily-initialized singleton object that can be used from different Activities. */
+ private static CapabilityManager sInstance;
+
+ /** Protects access to |sInstance|. */
+ private static final Object sInstanceLock = new Object();
+
+ /** List of all capabilities that are supported by the application. */
+ private List<String> mLocalCapabilities;
+
+ /** List of negotiated capabilities received from the host. */
+ private List<String> mNegotiatedCapabilities;
+
+ /** List of extensions to the client based on capabilities negotiated with the host. */
+ private List<ClientExtension> mClientExtensions;
+
+ private CapabilityManager() {
+ mLocalCapabilities = new ArrayList<String>();
+ mClientExtensions = new ArrayList<ClientExtension>();
+
+ mLocalCapabilities.add(Capabilities.CAST_CAPABILITY);
+ }
+
+ /**
+ * Returns the singleton object. Thread-safe.
+ */
+ public static CapabilityManager getInstance() {
+ synchronized (sInstanceLock) {
+ if (sInstance == null) {
+ sInstance = new CapabilityManager();
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Returns a space-separated list (required by host) of the capabilities supported by
+ * this client.
+ */
+ public String getLocalCapabilities() {
+ return TextUtils.join(" ", mLocalCapabilities);
+ }
+
+ /**
+ * Returns the ActivityLifecycleListener associated with the specified capability, if
+ * |capability| is enabled and such a listener exists.
+ *
+ * Activities that call this method agree to appropriately notify the listener of lifecycle
+ * events., thus supporting |capability|. This allows extensions like the CastExtensionHandler
+ * to hook into an existing activity's lifecycle.
+ */
+ public ActivityLifecycleListener onActivityAcceptingListener(
+ Activity activity, String capability) {
+
+ ActivityLifecycleListener listener;
+
+ if (isCapabilityEnabled(capability)) {
+ for (ClientExtension ext : mClientExtensions) {
+ if (ext.getCapability().equals(capability)) {
+ listener = ext.onActivityAcceptingListener(activity);
+ if (listener != null)
+ return listener;
+ }
+ }
+ }
+
+ return new DummyActivityLifecycleListener();
+ }
+
+ /**
+ * Receives the capabilities negotiated between client and host and creates the appropriate
+ * extension handlers.
+ *
+ * Currently only the CAST_CAPABILITY exists, so that is the only extension constructed.
+ */
+ public void setNegotiatedCapabilities(String capabilities) {
+ mNegotiatedCapabilities = Arrays.asList(capabilities.split(" "));
+ mClientExtensions.clear();
+ if (isCapabilityEnabled(Capabilities.CAST_CAPABILITY)) {
+ mClientExtensions.add(maybeCreateCastExtensionHandler());
+ }
+ }
+
+ /**
+ * Passes the deconstructed extension message to each ClientExtension in turn until the message
+ * is handled or none remain. Returns true if the message was handled.
+ */
+ public boolean onExtensionMessage(String type, String data) {
+ if (type == null || type.isEmpty()) {
+ return false;
+ }
+
+ for (ClientExtension ext : mClientExtensions) {
+ if (ext.onExtensionMessage(type, data)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the capability is enabled for this connection with the host.
+ */
+ private boolean isCapabilityEnabled(String capability) {
+ return (mNegotiatedCapabilities != null && mNegotiatedCapabilities.contains(capability));
+ }
+
+ /**
+ * Tries to reflectively instantiate a CastExtensionHandler object.
+ *
+ * Note: The ONLY reason this is done is that by default, the regular android application
+ * will be built, without this experimental extension.
+ */
+ private ClientExtension maybeCreateCastExtensionHandler() {
+ try {
+ Class<?> cls = Class.forName("org.chromium.chromoting.CastExtensionHandler");
+ return (ClientExtension) cls.newInstance();
+ } catch (ClassNotFoundException e) {
+ Log.w("CapabilityManager", "Failed to create CastExtensionHandler.");
+ return new DummyClientExtension();
+ } catch (InstantiationException e) {
+ Log.w("CapabilityManager", "Failed to create CastExtensionHandler.");
+ return new DummyClientExtension();
+ } catch (IllegalAccessException e) {
+ Log.w("CapabilityManager", "Failed to create CastExtensionHandler.");
+ return new DummyClientExtension();
+ }
+ }
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/ClientExtension.java b/remoting/android/java/src/org/chromium/chromoting/ClientExtension.java
new file mode 100644
index 0000000..070dcc7
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/ClientExtension.java
@@ -0,0 +1,32 @@
+// 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;
+
+/**
+ * Interface to extend the Android client's functionality by providing a way to communicate with
+ * the Chromoting host.
+ */
+public interface ClientExtension {
+
+ /** Returns the capability supported by this extension, or an empty string. */
+ public String getCapability();
+
+ /**
+ * Called when the client receives an extension message from the host through JniInterface. It
+ * returns true if the message was handled appropriately, and false otherwise.
+ */
+ public boolean onExtensionMessage(String type, String data);
+
+ /**
+ * Called when an activity offers to accept an ActivityListener for its lifecycle events.
+ * This gives Extensions the option to hook into an existing Activity, get notified about
+ * changes in its state and modify its behavior. Returns the extension's activity listener,
+ * or null.
+ */
+ public ActivityLifecycleListener onActivityAcceptingListener(Activity activity);
+
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/Desktop.java b/remoting/android/java/src/org/chromium/chromoting/Desktop.java
index f40469f..f6b4d260 100644
--- a/remoting/android/java/src/org/chromium/chromoting/Desktop.java
+++ b/remoting/android/java/src/org/chromium/chromoting/Desktop.java
@@ -5,10 +5,10 @@
package org.chromium.chromoting;
import android.annotation.SuppressLint;
-import android.app.Activity;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.Menu;
@@ -25,7 +25,7 @@ import java.util.TreeSet;
/**
* A simple screen that does nothing except display a DesktopView and notify it of rotations.
*/
-public class Desktop extends Activity implements View.OnSystemUiVisibilityChangeListener {
+public class Desktop extends ActionBarActivity implements View.OnSystemUiVisibilityChangeListener {
/** Web page to be displayed in the Help screen when launched from this activity. */
private static final String HELP_URL =
"http://support.google.com/chrome/?p=mobile_crd_connecthost";
@@ -39,6 +39,9 @@ public class Desktop extends Activity implements View.OnSystemUiVisibilityChange
/** Set of pressed keys for which we've sent TextEvent. */
private Set<Integer> mPressedTextKeys = new TreeSet<Integer>();
+ private ActivityLifecycleListener mActivityLifecycleListener;
+
+
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -53,6 +56,36 @@ public class Desktop extends Activity implements View.OnSystemUiVisibilityChange
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(this);
+
+ mActivityLifecycleListener = CapabilityManager.getInstance()
+ .onActivityAcceptingListener(this, Capabilities.CAST_CAPABILITY);
+ mActivityLifecycleListener.onActivityCreated(this, savedInstanceState);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mActivityLifecycleListener.onActivityStarted(this);
+ }
+
+ @Override
+ protected void onPause() {
+ if (isFinishing()) {
+ mActivityLifecycleListener.onActivityPaused(this);
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mActivityLifecycleListener.onActivityResumed(this);
+ }
+
+ @Override
+ protected void onStop() {
+ mActivityLifecycleListener.onActivityStopped(this);
+ super.onStop();
}
/** Called when the activity is finally finished. */
@@ -73,6 +106,9 @@ public class Desktop extends Activity implements View.OnSystemUiVisibilityChange
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.desktop_actionbar, menu);
+
+ mActivityLifecycleListener.onActivityCreatedOptionsMenu(this, menu);
+
return super.onCreateOptionsMenu(menu);
}
@@ -144,6 +180,9 @@ public class Desktop extends Activity implements View.OnSystemUiVisibilityChange
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
+
+ mActivityLifecycleListener.onActivityOptionsItemSelected(this, item);
+
if (id == R.id.actionbar_keyboard) {
((InputMethodManager)getSystemService(INPUT_METHOD_SERVICE)).toggleSoftInput(0, 0);
return true;
diff --git a/remoting/android/java/src/org/chromium/chromoting/DummyActivityLifecycleListener.java b/remoting/android/java/src/org/chromium/chromoting/DummyActivityLifecycleListener.java
new file mode 100644
index 0000000..76b8c6f
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/DummyActivityLifecycleListener.java
@@ -0,0 +1,48 @@
+// 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.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * A dummy implementation of ActivityListener that will be passed to any activity requesting
+ * a capability that is not currently enabled.
+ */
+public class DummyActivityLifecycleListener implements ActivityLifecycleListener {
+
+ @Override
+ public void onActivityCreated (Activity activity, Bundle savedInstanceState) {}
+
+ @Override
+ public void onActivityDestroyed (Activity activity) {}
+
+ @Override
+ public void onActivityPaused (Activity activity) {}
+
+ @Override
+ public void onActivityResumed (Activity activity) {}
+
+ @Override
+ public void onActivitySaveInstanceState (Activity activity, Bundle outState) {}
+
+ @Override
+ public void onActivityStarted (Activity activity) {}
+
+ @Override
+ public void onActivityStopped (Activity activity) {}
+
+ @Override
+ public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActivityOptionsItemSelected(Activity activity, MenuItem item) {
+ return false;
+ }
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/DummyClientExtension.java b/remoting/android/java/src/org/chromium/chromoting/DummyClientExtension.java
new file mode 100644
index 0000000..e1dc677
--- /dev/null
+++ b/remoting/android/java/src/org/chromium/chromoting/DummyClientExtension.java
@@ -0,0 +1,28 @@
+// 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;
+
+/**
+ * A dummy implementation of ClientExtension.
+ */
+public class DummyClientExtension implements ClientExtension {
+
+ @Override
+ public String getCapability() {
+ return "";
+ }
+
+ @Override
+ public boolean onExtensionMessage(String type, String data) {
+ return false;
+ }
+
+ @Override
+ public ActivityLifecycleListener onActivityAcceptingListener(Activity activity) {
+ return null;
+ }
+}
diff --git a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java
index 34bf0df..9304ee3 100644
--- a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java
+++ b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java
@@ -22,6 +22,7 @@ import android.widget.Toast;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
+import org.chromium.chromoting.CapabilityManager;
import org.chromium.chromoting.Chromoting;
import org.chromium.chromoting.R;
@@ -143,6 +144,9 @@ public class JniInterface {
/** Bitmap holding the cursor shape. Accessed on the graphics thread. */
private static Bitmap sCursorBitmap = null;
+ /** Capability Manager through which capabilities and extensions are handled. */
+ private static CapabilityManager sCapabilityManager = CapabilityManager.getInstance();
+
/**
* To be called once from the main Activity. Any subsequent calls will update the application
* context, but not reload the library. This is useful e.g. when the activity is closed and the
@@ -177,13 +181,15 @@ public class JniInterface {
sConnectionListener = listener;
SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE);
nativeConnect(username, authToken, hostJid, hostId, hostPubkey,
- prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""));
+ prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""),
+ sCapabilityManager.getLocalCapabilities());
sConnected = true;
}
/** Performs the native portion of the connection. */
private static native void nativeConnect(String username, String authToken, String hostJid,
- String hostId, String hostPubkey, String pairId, String pairSecret);
+ String hostId, String hostPubkey, String pairId, String pairSecret,
+ String capabilities);
/** Severs the connection and cleans up. Called on the UI thread. */
public static void disconnectFromHost() {
@@ -490,4 +496,35 @@ public class JniInterface {
/** Passes authentication data to the native handling code. */
private static native void nativeOnThirdPartyTokenFetched(String token, String sharedSecret);
+
+ //
+ // Host and Client Capabilities
+ //
+
+ /** Set the list of negotiated capabilities between host and client. Called on the UI thread. */
+ @CalledByNative
+ public static void setCapabilities(String capabilities) {
+ sCapabilityManager.setNegotiatedCapabilities(capabilities);
+ }
+
+ //
+ // Extension Message Handling
+ //
+
+ /** Passes on the deconstructed ExtensionMessage to the app. Called on the UI thread. */
+ @CalledByNative
+ public static void handleExtensionMessage(String type, String data) {
+ sCapabilityManager.onExtensionMessage(type, data);
+ }
+
+ /** Sends an extension message to the Chromoting host. Called on the UI thread. */
+ public static void sendExtensionMessage(String type, String data) {
+ if (!sConnected) {
+ return;
+ }
+
+ nativeSendExtensionMessage(type, data);
+ }
+
+ private static native void nativeSendExtensionMessage(String type, String data);
}
diff --git a/remoting/client/jni/chromoting_jni_instance.cc b/remoting/client/jni/chromoting_jni_instance.cc
index 253c54c..a8d1d27 100644
--- a/remoting/client/jni/chromoting_jni_instance.cc
+++ b/remoting/client/jni/chromoting_jni_instance.cc
@@ -46,12 +46,14 @@ ChromotingJniInstance::ChromotingJniInstance(ChromotingJniRuntime* jni_runtime,
const char* host_id,
const char* host_pubkey,
const char* pairing_id,
- const char* pairing_secret)
+ const char* pairing_secret,
+ const char* capabilities)
: jni_runtime_(jni_runtime),
host_id_(host_id),
host_jid_(host_jid),
create_pairing_(false),
stats_logging_enabled_(false),
+ capabilities_(capabilities),
weak_factory_(this) {
DCHECK(jni_runtime_->ui_task_runner()->BelongsToCurrentThread());
@@ -250,6 +252,22 @@ void ChromotingJniInstance::SendTextEvent(const std::string& text) {
client_->input_stub()->InjectTextEvent(event);
}
+void ChromotingJniInstance::SendClientMessage(const std::string& type,
+ const std::string& data) {
+ if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) {
+ jni_runtime_->network_task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(
+ &ChromotingJniInstance::SendClientMessage, this, type, data));
+ return;
+ }
+
+ protocol::ExtensionMessage extension_message;
+ extension_message.set_type(type);
+ extension_message.set_data(data);
+ client_->host_stub()->DeliverClientMessage(extension_message);
+}
+
void ChromotingJniInstance::RecordPaintTime(int64 paint_time_ms) {
if (!jni_runtime_->network_task_runner()->BelongsToCurrentThread()) {
jni_runtime_->network_task_runner()->PostTask(
@@ -299,7 +317,11 @@ void ChromotingJniInstance::OnRouteChanged(
}
void ChromotingJniInstance::SetCapabilities(const std::string& capabilities) {
- NOTIMPLEMENTED();
+ jni_runtime_->ui_task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(&ChromotingJniRuntime::SetCapabilities,
+ base::Unretained(jni_runtime_),
+ capabilities));
}
void ChromotingJniInstance::SetPairingResponse(
@@ -314,7 +336,12 @@ void ChromotingJniInstance::SetPairingResponse(
void ChromotingJniInstance::DeliverHostMessage(
const protocol::ExtensionMessage& message) {
- NOTIMPLEMENTED();
+ jni_runtime_->ui_task_runner()->PostTask(
+ FROM_HERE,
+ base::Bind(&ChromotingJniRuntime::HandleExtensionMessage,
+ base::Unretained(jni_runtime_),
+ message.type(),
+ message.data()));
}
protocol::ClipboardStub* ChromotingJniInstance::GetClipboardStub() {
@@ -402,7 +429,7 @@ void ChromotingJniInstance::ConnectToHostOnNetworkThread() {
network_settings));
client_->Start(signaling_.get(), authenticator_.Pass(),
- transport_factory.Pass(), host_jid_, std::string());
+ transport_factory.Pass(), host_jid_, capabilities_);
}
void ChromotingJniInstance::DisconnectFromHostOnNetworkThread() {
diff --git a/remoting/client/jni/chromoting_jni_instance.h b/remoting/client/jni/chromoting_jni_instance.h
index 8f47ea9..ce572fc 100644
--- a/remoting/client/jni/chromoting_jni_instance.h
+++ b/remoting/client/jni/chromoting_jni_instance.h
@@ -48,7 +48,8 @@ class ChromotingJniInstance
const char* host_id,
const char* host_pubkey,
const char* pairing_id,
- const char* pairing_secret);
+ const char* pairing_secret,
+ const char* capabilities);
// Terminates the current connection (if it hasn't already failed) and cleans
// up. Must be called before destruction.
@@ -86,6 +87,8 @@ class ChromotingJniInstance
void SendTextEvent(const std::string& text);
+ void SendClientMessage(const std::string& type, const std::string& data);
+
// Records paint time for statistics logging, if enabled. May be called from
// any thread.
void RecordPaintTime(int64 paint_time_ms);
@@ -180,6 +183,11 @@ class ChromotingJniInstance
// the Android log. Used on the network thread.
bool stats_logging_enabled_;
+ // The set of capabilities supported by the client. Accessed on the network
+ // thread. Once SetCapabilities() is called, this will contain the negotiated
+ // set of capabilities for this remoting session.
+ std::string capabilities_;
+
friend class base::RefCountedThreadSafe<ChromotingJniInstance>;
base::WeakPtrFactory<ChromotingJniInstance> weak_factory_;
diff --git a/remoting/client/jni/chromoting_jni_runtime.cc b/remoting/client/jni/chromoting_jni_runtime.cc
index 3cc2e080..e44c340 100644
--- a/remoting/client/jni/chromoting_jni_runtime.cc
+++ b/remoting/client/jni/chromoting_jni_runtime.cc
@@ -77,7 +77,8 @@ static void Connect(JNIEnv* env,
jstring hostId,
jstring hostPubkey,
jstring pairId,
- jstring pairSecret) {
+ jstring pairSecret,
+ jstring capabilities) {
remoting::ChromotingJniRuntime::GetInstance()->ConnectToHost(
ConvertJavaStringToUTF8(env, username).c_str(),
ConvertJavaStringToUTF8(env, authToken).c_str(),
@@ -85,7 +86,8 @@ static void Connect(JNIEnv* env,
ConvertJavaStringToUTF8(env, hostId).c_str(),
ConvertJavaStringToUTF8(env, hostPubkey).c_str(),
ConvertJavaStringToUTF8(env, pairId).c_str(),
- ConvertJavaStringToUTF8(env, pairSecret).c_str());
+ ConvertJavaStringToUTF8(env, pairSecret).c_str(),
+ ConvertJavaStringToUTF8(env, capabilities).c_str());
}
static void Disconnect(JNIEnv* env, jclass clazz) {
@@ -156,6 +158,15 @@ static void OnThirdPartyTokenFetched(JNIEnv* env,
ConvertJavaStringToUTF8(env, shared_secret)));
}
+static void SendExtensionMessage(JNIEnv* env,
+ jclass clazz,
+ jstring type,
+ jstring data) {
+ remoting::ChromotingJniRuntime::GetInstance()->session()->SendClientMessage(
+ ConvertJavaStringToUTF8(env, type),
+ ConvertJavaStringToUTF8(env, data));
+}
+
// ChromotingJniRuntime implementation.
// static
@@ -214,7 +225,8 @@ void ChromotingJniRuntime::ConnectToHost(const char* username,
const char* host_id,
const char* host_pubkey,
const char* pairing_id,
- const char* pairing_secret) {
+ const char* pairing_secret,
+ const char* capabilities) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
DCHECK(!session_);
session_ = new ChromotingJniInstance(this,
@@ -224,7 +236,8 @@ void ChromotingJniRuntime::ConnectToHost(const char* username,
host_id,
host_pubkey,
pairing_id,
- pairing_secret);
+ pairing_secret,
+ capabilities);
}
void ChromotingJniRuntime::DisconnectFromHost() {
@@ -281,6 +294,27 @@ void ChromotingJniRuntime::FetchThirdPartyToken(const GURL& token_url,
env, j_url.obj(), j_client_id.obj(), j_scope.obj());
}
+void ChromotingJniRuntime::SetCapabilities(const std::string& capabilities) {
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+
+ ScopedJavaLocalRef<jstring> j_cap =
+ ConvertUTF8ToJavaString(env, capabilities);
+
+ Java_JniInterface_setCapabilities(env, j_cap.obj());
+}
+
+void ChromotingJniRuntime::HandleExtensionMessage(const std::string& type,
+ const std::string& message) {
+ DCHECK(ui_task_runner_->BelongsToCurrentThread());
+ JNIEnv* env = base::android::AttachCurrentThread();
+
+ ScopedJavaLocalRef<jstring> j_type = ConvertUTF8ToJavaString(env, type);
+ ScopedJavaLocalRef<jstring> j_message = ConvertUTF8ToJavaString(env, message);
+
+ Java_JniInterface_handleExtensionMessage(env, j_type.obj(), j_message.obj());
+}
+
base::android::ScopedJavaLocalRef<jobject> ChromotingJniRuntime::NewBitmap(
webrtc::DesktopSize size) {
JNIEnv* env = base::android::AttachCurrentThread();
diff --git a/remoting/client/jni/chromoting_jni_runtime.h b/remoting/client/jni/chromoting_jni_runtime.h
index f905702..4768966 100644
--- a/remoting/client/jni/chromoting_jni_runtime.h
+++ b/remoting/client/jni/chromoting_jni_runtime.h
@@ -57,7 +57,8 @@ class ChromotingJniRuntime {
const char* host_id,
const char* host_pubkey,
const char* pairing_id,
- const char* pairing_secret);
+ const char* pairing_secret,
+ const char* capabilities);
// Terminates any ongoing connection attempt and cleans up by nullifying
// |session_|. This is a no-op unless |session| is currently non-null.
@@ -88,6 +89,14 @@ class ChromotingJniRuntime {
const std::string& client_id,
const std::string& scope);
+ // Pass on the set of negotiated capabilities to the client.
+ void SetCapabilities(const std::string& capabilities);
+
+ // Passes on the deconstructed ExtensionMessage to the client to handle
+ // appropriately.
+ void HandleExtensionMessage(const std::string& type,
+ const std::string& message);
+
// Creates a new Bitmap object to store a video frame.
base::android::ScopedJavaLocalRef<jobject> NewBitmap(
webrtc::DesktopSize size);
diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp
index 2a902d6..80f89b8 100644
--- a/remoting/remoting.gyp
+++ b/remoting/remoting.gyp
@@ -9,6 +9,9 @@
# Set this to run the jscompile checks after building the webapp.
'run_jscompile%': 0,
+ # Set this to enable cast mode on the android client.
+ 'enable_cast%': 0,
+
'variables': {
'conditions': [
# Enable the multi-process host on Windows by default.
diff --git a/remoting/remoting_android.gypi b/remoting/remoting_android.gypi
index 6b2766e..b9f0778 100644
--- a/remoting/remoting_android.gypi
+++ b/remoting/remoting_android.gypi
@@ -56,8 +56,16 @@
{
'target_name': 'remoting_apk_manifest',
'type': 'none',
- 'sources': [
- 'android/java/AndroidManifest.xml.jinja2',
+ 'conditions': [
+ ['enable_cast==1', {
+ 'sources': [
+ 'android/cast/AndroidManifest.xml.jinja2'
+ ],
+ }, { # 'enable_cast != 1'
+ 'sources': [
+ 'android/java/AndroidManifest.xml.jinja2',
+ ],
+ }],
],
'rules': [{
'rule_name': 'generate_manifest',
@@ -94,11 +102,26 @@
],
},
'dependencies': [
+ 'android_support_v4_javalib_no_res',
'../base/base.gyp:base_java',
'../ui/android/ui_android.gyp:ui_java',
'remoting_android_resources',
+ '../third_party/android_tools/android_tools.gyp:android_support_v7_appcompat_javalib',
+ '../third_party/android_tools/android_tools.gyp:android_support_v7_mediarouter_javalib',
],
'includes': [ '../build/java.gypi' ],
+ 'conditions' : [
+ ['enable_cast==1', {
+ 'variables': {
+ 'additional_src_dirs': [
+ 'android/cast',
+ ],
+ },
+ 'dependencies': [
+ 'google_play_services_javalib',
+ ],
+ }],
+ ],
},
{
'target_name': 'remoting_apk',
@@ -132,7 +155,57 @@
},
'includes': [ '../build/java_apk.gypi' ],
}, # end of target 'remoting_test_apk'
- ], # end of 'targets'
+ {
+ # This jar contains the Android support v4 libary. It does not have
+ # any associated resources.
+ 'target_name': 'android_support_v4_javalib_no_res',
+ 'type': 'none',
+ 'variables': {
+ 'jar_path': '../third_party/android_tools/sdk/extras/android/support/v4/android-support-v4.jar',
+ },
+ 'includes': ['../build/java_prebuilt.gypi'],
+ }, # end of target 'android_support_v4_javalib_no_res'
+ ], # end of 'targets'
+ 'conditions': [
+ ['enable_cast==1', {
+ 'targets': [
+ {
+ # This jar contains the Google Play services library without the
+ # resources needed for the library to work. See crbug.com/274697 or
+ # ../third_party/android_tools/android_tools.gyp for more info.
+ # This target will fail to build unless you have a local version
+ # of the Google Play services jar.
+ 'target_name': 'google_play_services_javalib_no_res',
+ 'type': 'none',
+ 'variables': {
+ 'jar_path': 'android/google-play-services_lib/libs/google-play-services.jar',
+ },
+ 'includes': ['../build/java_prebuilt.gypi'],
+
+ }, # end of target 'google_play_services_javalib_no_res'
+ {
+ # This target contains the Google Play services library with the
+ # resources needed. It will fail to build unless you have a local
+ # version of the Google Play services libary project.
+ # TODO(aiguha): Solve issue of needing to use local version. Also,
+ # watch crbug.com/274697.
+ 'target_name': 'google_play_services_javalib',
+ 'type': 'none',
+ 'variables': {
+ 'java_in_dir': 'android/google-play-services_lib/',
+ 'R_package': ['com.google.android.gms'],
+ 'R_package_relpath': ['com/google/android/gms'],
+ 'has_java_resources': 1,
+ 'res_v14_verify_only': 1,
+ },
+ 'dependencies': [
+ 'google_play_services_javalib_no_res',
+ ],
+ 'includes': ['../build/java.gypi'],
+ }, # end of target 'google_play_services_javalib'
+ ], # end of targets
+ }],
+ ],
}], # 'OS=="android"'
['OS=="android"', {
diff --git a/remoting/resources/remoting_strings.grd b/remoting/resources/remoting_strings.grd
index 7c9f75f..0af6d94 100644
--- a/remoting/resources/remoting_strings.grd
+++ b/remoting/resources/remoting_strings.grd
@@ -424,6 +424,15 @@
<message desc="Text shown in tooltip when user touches an offline host on the device." name="IDS_HOST_OFFLINE_TOOLTIP" formatter_data="android_java">
Host is offline.
</message>
+ <message desc="Error that is shown in tooltip if the app loses its connection to the Cast device." name="IDS_CONNECTION_TO_CAST_FAILED" formatter_data="android_java">
+ Failed to connect to Cast device.
+ </message>
+ <message desc="Text shown in tooltip when the application has closed its connection to the Cast device." name="IDS_CONNECTION_TO_CAST_CLOSED" formatter_data="android_java">
+ Closed connection to Cast device.
+ </message>
+ <message desc="Title for Cast button in Android application menu." name="IDS_CAST_BUTTON_TITLE" formatter_data="android_java">
+ Cast
+ </message>
<!-- Play Store listings text. These Android-specific strings are not marked
with formatter_data="android_java" since they are used only for the Play