1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org
.chromium
.chromoting
;
7 import android
.app
.Activity
;
8 import android
.content
.Context
;
9 import android
.os
.Bundle
;
10 import android
.support
.v4
.view
.MenuItemCompat
;
11 import android
.support
.v7
.app
.MediaRouteActionProvider
;
12 import android
.support
.v7
.media
.MediaRouteSelector
;
13 import android
.support
.v7
.media
.MediaRouter
;
14 import android
.support
.v7
.media
.MediaRouter
.RouteInfo
;
15 import android
.util
.Log
;
16 import android
.view
.Menu
;
17 import android
.view
.MenuItem
;
18 import android
.widget
.Toast
;
20 import com
.google
.android
.gms
.cast
.Cast
;
21 import com
.google
.android
.gms
.cast
.Cast
.Listener
;
22 import com
.google
.android
.gms
.cast
.CastDevice
;
23 import com
.google
.android
.gms
.cast
.CastMediaControlIntent
;
24 import com
.google
.android
.gms
.cast
.CastStatusCodes
;
25 import com
.google
.android
.gms
.common
.ConnectionResult
;
26 import com
.google
.android
.gms
.common
.api
.GoogleApiClient
;
27 import com
.google
.android
.gms
.common
.api
.GoogleApiClient
.ConnectionCallbacks
;
28 import com
.google
.android
.gms
.common
.api
.GoogleApiClient
.OnConnectionFailedListener
;
29 import com
.google
.android
.gms
.common
.api
.ResultCallback
;
30 import com
.google
.android
.gms
.common
.api
.Status
;
32 import org
.chromium
.chromoting
.jni
.JniInterface
;
34 import java
.io
.IOException
;
35 import java
.util
.ArrayList
;
36 import java
.util
.List
;
39 * A handler that interacts with the Cast Extension of the Chromoting host using extension messages.
40 * It uses the Cast Android Sender API to start our registered Cast Receiver App on a nearby Cast
41 * device, if the user chooses to do so.
43 public class CastExtensionHandler
implements ClientExtension
, ActivityLifecycleListener
{
45 /** Extension messages of this type will be handled by the CastExtensionHandler. */
46 public static final String EXTENSION_MSG_TYPE
= "cast_message";
48 /** Tag used for logging. */
49 private static final String TAG
= "CastExtensionHandler";
51 /** Application Id of the Cast Receiver App that will be run on the Cast device. */
52 private static final String RECEIVER_APP_ID
= "8A1211E3";
55 * Custom namespace that will be used to communicate with the Cast device.
56 * TODO(aiguha): Use com.google.chromeremotedesktop for official builds.
58 private static final String CHROMOTOCAST_NAMESPACE
= "urn:x-cast:com.chromoting.cast.all";
60 /** Context that wil be used to initialize the MediaRouter and the GoogleApiClient. */
61 private Context mContext
= null;
63 /** True if the application has been launched on the Cast device. */
64 private boolean mApplicationStarted
;
66 /** True if the client is temporarily in a disconnected state. */
67 private boolean mWaitingForReconnect
;
69 /** Object that allows routing of media to external devices including Google Cast devices. */
70 private MediaRouter mMediaRouter
;
72 /** Describes the capabilities of routes that the application might want to use. */
73 private MediaRouteSelector mMediaRouteSelector
;
75 /** Cast device selected by the user. */
76 private CastDevice mSelectedDevice
;
78 /** Object to receive callbacks about media routing changes. */
79 private MediaRouter
.Callback mMediaRouterCallback
;
81 /** Listener for events related to the connected Cast device.*/
82 private Listener mCastClientListener
;
84 /** Object that handles Google Play Services integration. */
85 private GoogleApiClient mApiClient
;
87 /** Callback objects for connection changes with Google Play Services. */
88 private ConnectionCallbacks mConnectionCallbacks
;
89 private OnConnectionFailedListener mConnectionFailedListener
;
91 /** Channel for receiving messages from the Cast device. */
92 private ChromotocastChannel mChromotocastChannel
;
94 /** Current session ID, if there is one. */
95 private String mSessionId
;
97 /** Queue of messages that are yet to be delivered to the Receiver App. */
98 private List
<String
> mChromotocastMessageQueue
;
100 /** Current status of the application, if any. */
101 private String mApplicationStatus
;
104 * A callback class for receiving events about media routing.
106 private class CustomMediaRouterCallback
extends MediaRouter
.Callback
{
108 public void onRouteSelected(MediaRouter router
, RouteInfo info
) {
109 mSelectedDevice
= CastDevice
.getFromBundle(info
.getExtras());
114 public void onRouteUnselected(MediaRouter router
, RouteInfo info
) {
116 mSelectedDevice
= null;
121 * A callback class for receiving the result of launching an application on the user-selected
122 * Google Cast device.
124 private class ApplicationConnectionResultCallback
implements
125 ResultCallback
<Cast
.ApplicationConnectionResult
> {
127 public void onResult(Cast
.ApplicationConnectionResult result
) {
128 Status status
= result
.getStatus();
129 if (!status
.isSuccess()) {
134 mSessionId
= result
.getSessionId();
135 mApplicationStatus
= result
.getApplicationStatus();
136 mApplicationStarted
= result
.getWasLaunched();
137 mChromotocastChannel
= new ChromotocastChannel();
140 Cast
.CastApi
.setMessageReceivedCallbacks(mApiClient
,
141 mChromotocastChannel
.getNamespace(), mChromotocastChannel
);
142 sendPendingMessagesToCastDevice();
143 } catch (IOException e
) {
144 showToast(R
.string
.connection_to_cast_failed
, Toast
.LENGTH_SHORT
);
146 } catch (IllegalStateException e
) {
147 showToast(R
.string
.connection_to_cast_failed
, Toast
.LENGTH_SHORT
);
154 * A callback class for receiving events about client connections and disconnections from
155 * Google Play Services.
157 private class ConnectionCallbacks
implements GoogleApiClient
.ConnectionCallbacks
{
159 public void onConnected(Bundle connectionHint
) {
160 if (mWaitingForReconnect
) {
161 mWaitingForReconnect
= false;
165 Cast
.CastApi
.launchApplication(mApiClient
, RECEIVER_APP_ID
, false).setResultCallback(
166 new ApplicationConnectionResultCallback());
170 public void onConnectionSuspended(int cause
) {
171 mWaitingForReconnect
= true;
176 * A listener for failures to connect with Google Play Services.
178 private class ConnectionFailedListener
implements GoogleApiClient
.OnConnectionFailedListener
{
180 public void onConnectionFailed(ConnectionResult result
) {
181 Log
.e(TAG
, String
.format("Google Play Service connection failed: %s", result
));
189 * A channel for communication with the Cast device on the CHROMOTOCAST_NAMESPACE.
191 private class ChromotocastChannel
implements Cast
.MessageReceivedCallback
{
194 * Returns the namespace associated with this channel.
196 public String
getNamespace() {
197 return CHROMOTOCAST_NAMESPACE
;
201 public void onMessageReceived(CastDevice castDevice
, String namespace
, String message
) {
202 if (namespace
.equals(CHROMOTOCAST_NAMESPACE
)) {
203 sendMessageToHost(message
);
209 * A listener for changes when connected to a Google Cast device.
211 private class CastClientListener
extends Cast
.Listener
{
213 public void onApplicationStatusChanged() {
215 if (mApiClient
!= null) {
216 mApplicationStatus
= Cast
.CastApi
.getApplicationStatus(mApiClient
);
218 } catch (IllegalStateException e
) {
219 showToast(R
.string
.connection_to_cast_failed
, Toast
.LENGTH_SHORT
);
225 public void onVolumeChanged() {} // Changes in volume do not affect us.
228 public void onApplicationDisconnected(int errorCode
) {
229 if (errorCode
!= CastStatusCodes
.SUCCESS
) {
230 Log
.e(TAG
, String
.format("Application disconnected with: %d", errorCode
));
237 * Constructs a CastExtensionHandler with an empty message queue.
239 public CastExtensionHandler() {
240 mChromotocastMessageQueue
= new ArrayList
<String
>();
244 // ClientExtension implementation.
248 public String
getCapability() {
249 return Capabilities
.CAST_CAPABILITY
;
253 public boolean onExtensionMessage(String type
, String data
) {
254 if (type
.equals(EXTENSION_MSG_TYPE
)) {
255 mChromotocastMessageQueue
.add(data
);
256 if (mApplicationStarted
) {
257 sendPendingMessagesToCastDevice();
265 public ActivityLifecycleListener
onActivityAcceptingListener(Activity activity
) {
270 // ActivityLifecycleListener implementation.
273 /** Initializes the MediaRouter and related objects using the provided activity Context. */
275 public void onActivityCreated(Activity activity
, Bundle savedInstanceState
) {
276 if (activity
== null) {
280 mMediaRouter
= MediaRouter
.getInstance(activity
);
281 mMediaRouteSelector
= new MediaRouteSelector
.Builder()
282 .addControlCategory(CastMediaControlIntent
.categoryForCast(RECEIVER_APP_ID
))
284 mMediaRouterCallback
= new CustomMediaRouterCallback();
288 public void onActivityDestroyed(Activity activity
) {
293 public void onActivityPaused(Activity activity
) {
294 removeMediaRouterCallback();
298 public void onActivityResumed(Activity activity
) {
299 addMediaRouterCallback();
303 public void onActivitySaveInstanceState(Activity activity
, Bundle outState
) {}
306 public void onActivityStarted(Activity activity
) {
307 addMediaRouterCallback();
311 public void onActivityStopped(Activity activity
) {
312 removeMediaRouterCallback();
316 public boolean onActivityCreatedOptionsMenu(Activity activity
, Menu menu
) {
317 // Find the cast button in the menu.
318 MenuItem mediaRouteMenuItem
= menu
.findItem(R
.id
.media_route_menu_item
);
319 if (mediaRouteMenuItem
== null) {
323 // Setup a MediaRouteActionProvider using the button.
324 MediaRouteActionProvider mediaRouteActionProvider
=
325 (MediaRouteActionProvider
) MenuItemCompat
.getActionProvider(mediaRouteMenuItem
);
326 mediaRouteActionProvider
.setRouteSelector(mMediaRouteSelector
);
332 public boolean onActivityOptionsItemSelected(Activity activity
, MenuItem item
) {
333 if (item
.getItemId() == R
.id
.actionbar_disconnect
) {
334 removeMediaRouterCallback();
335 showToast(R
.string
.connection_to_cast_closed
, Toast
.LENGTH_SHORT
);
343 // Extension Message Handling logic
346 /** Sends a message to the Chromoting host. */
347 private void sendMessageToHost(String data
) {
348 JniInterface
.sendExtensionMessage(EXTENSION_MSG_TYPE
, data
);
351 /** Sends any messages in the message queue to the Cast device. */
352 private void sendPendingMessagesToCastDevice() {
353 for (String msg
: mChromotocastMessageQueue
) {
354 sendMessageToCastDevice(msg
);
356 mChromotocastMessageQueue
.clear();
360 // Cast Sender API logic
364 * Initializes and connects to Google Play Services.
366 private void connectApiClient() {
367 if (mContext
== null) {
370 mCastClientListener
= new CastClientListener();
371 mConnectionCallbacks
= new ConnectionCallbacks();
372 mConnectionFailedListener
= new ConnectionFailedListener();
374 Cast
.CastOptions
.Builder apiOptionsBuilder
= Cast
.CastOptions
375 .builder(mSelectedDevice
, mCastClientListener
)
376 .setVerboseLoggingEnabled(true);
378 mApiClient
= new GoogleApiClient
.Builder(mContext
)
379 .addApi(Cast
.API
, apiOptionsBuilder
.build())
380 .addConnectionCallbacks(mConnectionCallbacks
)
381 .addOnConnectionFailedListener(mConnectionFailedListener
)
383 mApiClient
.connect();
387 * Adds the callback object to the MediaRouter. Called when the owning activity starts/resumes.
389 private void addMediaRouterCallback() {
390 if (mMediaRouter
!= null && mMediaRouteSelector
!= null && mMediaRouterCallback
!= null) {
391 mMediaRouter
.addCallback(mMediaRouteSelector
, mMediaRouterCallback
,
392 MediaRouter
.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
);
397 * Removes the callback object from the MediaRouter. Called when the owning activity
400 private void removeMediaRouterCallback() {
401 if (mMediaRouter
!= null && mMediaRouterCallback
!= null) {
402 mMediaRouter
.removeCallback(mMediaRouterCallback
);
407 * Sends a message to the Cast device on the CHROMOTOCAST_NAMESPACE.
409 private void sendMessageToCastDevice(String message
) {
410 if (mApiClient
== null || mChromotocastChannel
== null) {
413 Cast
.CastApi
.sendMessage(mApiClient
, mChromotocastChannel
.getNamespace(), message
)
414 .setResultCallback(new ResultCallback
<Status
>() {
416 public void onResult(Status result
) {
417 if (!result
.isSuccess()) {
418 Log
.e(TAG
, "Failed to send message to cast device.");
426 * Restablishes the chromotocast message channel, so we can continue communicating with the
427 * Google Cast device. This must be called when resuming a connection.
429 private void reconnectChannels() {
430 if (mApiClient
== null && mChromotocastChannel
== null) {
434 Cast
.CastApi
.setMessageReceivedCallbacks(
435 mApiClient
, mChromotocastChannel
.getNamespace(), mChromotocastChannel
);
436 sendPendingMessagesToCastDevice();
437 } catch (IOException e
) {
438 showToast(R
.string
.connection_to_cast_failed
, Toast
.LENGTH_SHORT
);
439 } catch (IllegalStateException e
) {
440 showToast(R
.string
.connection_to_cast_failed
, Toast
.LENGTH_SHORT
);
445 * Stops the running application on the Google Cast device and performs the required tearDown
448 private void tearDown() {
449 if (mApiClient
!= null && mApplicationStarted
&& mApiClient
.isConnected()) {
450 Cast
.CastApi
.stopApplication(mApiClient
, mSessionId
);
451 if (mChromotocastChannel
!= null) {
453 Cast
.CastApi
.removeMessageReceivedCallbacks(
454 mApiClient
, mChromotocastChannel
.getNamespace());
455 } catch (IOException e
) {
456 Log
.e(TAG
, "Failed to remove chromotocast channel.");
459 mApiClient
.disconnect();
461 mChromotocastChannel
= null;
462 mApplicationStarted
= false;
464 mSelectedDevice
= null;
465 mWaitingForReconnect
= false;
470 * Makes a toast using the given message and duration.
472 private void showToast(int messageId
, int duration
) {
473 if (mContext
!= null) {
474 Toast
.makeText(mContext
, mContext
.getString(messageId
), duration
).show();