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 | |
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
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 |