OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 package org.chromium.chromoting; |
| 6 |
| 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; |
| 19 |
| 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.OnConnectionFailedListe
ner; |
| 29 import com.google.android.gms.common.api.ResultCallback; |
| 30 import com.google.android.gms.common.api.Status; |
| 31 |
| 32 import org.chromium.chromoting.jni.JniInterface; |
| 33 |
| 34 import java.io.IOException; |
| 35 import java.util.ArrayList; |
| 36 import java.util.List; |
| 37 |
| 38 /** |
| 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. |
| 42 */ |
| 43 public class CastExtensionHandler implements ClientExtension, ActivityLifecycleL
istener { |
| 44 |
| 45 /** Extension messages of this type will be handled by the CastExtensionHand
ler. */ |
| 46 public static final String EXTENSION_MSG_TYPE = "cast_message"; |
| 47 |
| 48 /** Tag used for logging. */ |
| 49 private static final String TAG = "CastExtensionHandler"; |
| 50 |
| 51 /** Application Id of the Cast Receiver App that will be run on the Cast dev
ice. */ |
| 52 private static final String RECEIVER_APP_ID = "8A1211E3"; |
| 53 |
| 54 /** |
| 55 * Custom namespace that will be used to communicate with the Cast device. |
| 56 * TODO(aiguha): Use com.google.chromeremotedesktop for official builds. |
| 57 */ |
| 58 private static final String CHROMOTOCAST_NAMESPACE = "urn:x-cast:com.chromot
ing.cast.all"; |
| 59 |
| 60 /** Context that wil be used to initialize the MediaRouter and the GoogleApi
Client. */ |
| 61 private Context mContext = null; |
| 62 |
| 63 /** True if the application has been launched on the Cast device. */ |
| 64 private boolean mApplicationStarted; |
| 65 |
| 66 /** True if the client is temporarily in a disconnected state. */ |
| 67 private boolean mWaitingForReconnect; |
| 68 |
| 69 /** Object that allows routing of media to external devices including Google
Cast devices. */ |
| 70 private MediaRouter mMediaRouter; |
| 71 |
| 72 /** Describes the capabilities of routes that the application might want to
use. */ |
| 73 private MediaRouteSelector mMediaRouteSelector; |
| 74 |
| 75 /** Cast device selected by the user. */ |
| 76 private CastDevice mSelectedDevice; |
| 77 |
| 78 /** Object to receive callbacks about media routing changes. */ |
| 79 private MediaRouter.Callback mMediaRouterCallback; |
| 80 |
| 81 /** Listener for events related to the connected Cast device.*/ |
| 82 private Listener mCastClientListener; |
| 83 |
| 84 /** Object that handles Google Play Services integration. */ |
| 85 private GoogleApiClient mApiClient; |
| 86 |
| 87 /** Callback objects for connection changes with Google Play Services. */ |
| 88 private ConnectionCallbacks mConnectionCallbacks; |
| 89 private OnConnectionFailedListener mConnectionFailedListener; |
| 90 |
| 91 /** Channel for receiving messages from the Cast device. */ |
| 92 private ChromotocastChannel mChromotocastChannel; |
| 93 |
| 94 /** Current session ID, if there is one. */ |
| 95 private String mSessionId; |
| 96 |
| 97 /** Queue of messages that are yet to be delivered to the Receiver App. */ |
| 98 private List<String> mChromotocastMessageQueue; |
| 99 |
| 100 /** Current status of the application, if any. */ |
| 101 private String mApplicationStatus; |
| 102 |
| 103 /** |
| 104 * A callback class for receiving events about media routing. |
| 105 */ |
| 106 private class CustomMediaRouterCallback extends MediaRouter.Callback { |
| 107 @Override |
| 108 public void onRouteSelected(MediaRouter router, RouteInfo info) { |
| 109 mSelectedDevice = CastDevice.getFromBundle(info.getExtras()); |
| 110 connectApiClient(); |
| 111 } |
| 112 |
| 113 @Override |
| 114 public void onRouteUnselected(MediaRouter router, RouteInfo info) { |
| 115 tearDown(); |
| 116 mSelectedDevice = null; |
| 117 } |
| 118 } |
| 119 |
| 120 /** |
| 121 * A callback class for receiving the result of launching an application on
the user-selected |
| 122 * Google Cast device. |
| 123 */ |
| 124 private class ApplicationConnectionResultCallback implements |
| 125 ResultCallback<Cast.ApplicationConnectionResult> { |
| 126 @Override |
| 127 public void onResult(Cast.ApplicationConnectionResult result) { |
| 128 Status status = result.getStatus(); |
| 129 if (!status.isSuccess()) { |
| 130 tearDown(); |
| 131 return; |
| 132 } |
| 133 |
| 134 mSessionId = result.getSessionId(); |
| 135 mApplicationStatus = result.getApplicationStatus(); |
| 136 mApplicationStarted = result.getWasLaunched(); |
| 137 mChromotocastChannel = new ChromotocastChannel(); |
| 138 |
| 139 try { |
| 140 Cast.CastApi.setMessageReceivedCallbacks(mApiClient, |
| 141 mChromotocastChannel.getNamespace(), mChromotocastChanne
l); |
| 142 sendPendingMessagesToCastDevice(); |
| 143 } catch (IOException e) { |
| 144 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT
); |
| 145 tearDown(); |
| 146 } catch (IllegalStateException e) { |
| 147 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT
); |
| 148 tearDown(); |
| 149 } |
| 150 } |
| 151 } |
| 152 |
| 153 /** |
| 154 * A callback class for receiving events about client connections and discon
nections from |
| 155 * Google Play Services. |
| 156 */ |
| 157 private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallb
acks { |
| 158 @Override |
| 159 public void onConnected(Bundle connectionHint) { |
| 160 if (mWaitingForReconnect) { |
| 161 mWaitingForReconnect = false; |
| 162 reconnectChannels(); |
| 163 return; |
| 164 } |
| 165 Cast.CastApi.launchApplication(mApiClient, RECEIVER_APP_ID, false).s
etResultCallback( |
| 166 new ApplicationConnectionResultCallback()); |
| 167 } |
| 168 |
| 169 @Override |
| 170 public void onConnectionSuspended(int cause) { |
| 171 mWaitingForReconnect = true; |
| 172 } |
| 173 } |
| 174 |
| 175 /** |
| 176 * A listener for failures to connect with Google Play Services. |
| 177 */ |
| 178 private class ConnectionFailedListener implements GoogleApiClient.OnConnecti
onFailedListener { |
| 179 @Override |
| 180 public void onConnectionFailed(ConnectionResult result) { |
| 181 Log.e(TAG, String.format("Google Play Service connection failed: %s"
, result)); |
| 182 |
| 183 tearDown(); |
| 184 } |
| 185 |
| 186 } |
| 187 |
| 188 /** |
| 189 * A channel for communication with the Cast device on the CHROMOTOCAST_NAME
SPACE. |
| 190 */ |
| 191 private class ChromotocastChannel implements Cast.MessageReceivedCallback { |
| 192 |
| 193 /** |
| 194 * Returns the namespace associated with this channel. |
| 195 */ |
| 196 public String getNamespace() { |
| 197 return CHROMOTOCAST_NAMESPACE; |
| 198 } |
| 199 |
| 200 @Override |
| 201 public void onMessageReceived(CastDevice castDevice, String namespace, S
tring message) { |
| 202 if (namespace.equals(CHROMOTOCAST_NAMESPACE)) { |
| 203 sendMessageToHost(message); |
| 204 } |
| 205 } |
| 206 } |
| 207 |
| 208 /** |
| 209 * A listener for changes when connected to a Google Cast device. |
| 210 */ |
| 211 private class CastClientListener extends Cast.Listener { |
| 212 @Override |
| 213 public void onApplicationStatusChanged() { |
| 214 try { |
| 215 if (mApiClient != null) { |
| 216 mApplicationStatus = Cast.CastApi.getApplicationStatus(mApiC
lient); |
| 217 } |
| 218 } catch (IllegalStateException e) { |
| 219 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT
); |
| 220 tearDown(); |
| 221 } |
| 222 } |
| 223 |
| 224 @Override |
| 225 public void onVolumeChanged() {} // Changes in volume do not affect us. |
| 226 |
| 227 @Override |
| 228 public void onApplicationDisconnected(int errorCode) { |
| 229 if (errorCode != CastStatusCodes.SUCCESS) { |
| 230 Log.e(TAG, String.format("Application disconnected with: %d", er
rorCode)); |
| 231 } |
| 232 tearDown(); |
| 233 } |
| 234 } |
| 235 |
| 236 /** |
| 237 * Constructs a CastExtensionHandler with an empty message queue. |
| 238 */ |
| 239 public CastExtensionHandler() { |
| 240 mChromotocastMessageQueue = new ArrayList<String>(); |
| 241 } |
| 242 |
| 243 // |
| 244 // ClientExtension implementation. |
| 245 // |
| 246 |
| 247 @Override |
| 248 public String getCapability() { |
| 249 return Capabilities.CAST_CAPABILITY; |
| 250 } |
| 251 |
| 252 @Override |
| 253 public boolean onExtensionMessage(String type, String data) { |
| 254 if (type.equals(EXTENSION_MSG_TYPE)) { |
| 255 mChromotocastMessageQueue.add(data); |
| 256 if (mApplicationStarted) { |
| 257 sendPendingMessagesToCastDevice(); |
| 258 } |
| 259 return true; |
| 260 } |
| 261 return false; |
| 262 } |
| 263 |
| 264 @Override |
| 265 public ActivityLifecycleListener onActivityAcceptingListener(Activity activi
ty) { |
| 266 return this; |
| 267 } |
| 268 |
| 269 // |
| 270 // ActivityLifecycleListener implementation. |
| 271 // |
| 272 |
| 273 /** Initializes the MediaRouter and related objects using the provided activ
ity Context. */ |
| 274 @Override |
| 275 public void onActivityCreated(Activity activity, Bundle savedInstanceState)
{ |
| 276 if (activity == null) { |
| 277 return; |
| 278 } |
| 279 mContext = activity; |
| 280 mMediaRouter = MediaRouter.getInstance(activity); |
| 281 mMediaRouteSelector = new MediaRouteSelector.Builder() |
| 282 .addControlCategory(CastMediaControlIntent.categoryForCast(RECEI
VER_APP_ID)) |
| 283 .build(); |
| 284 mMediaRouterCallback = new CustomMediaRouterCallback(); |
| 285 } |
| 286 |
| 287 @Override |
| 288 public void onActivityDestroyed(Activity activity) { |
| 289 tearDown(); |
| 290 } |
| 291 |
| 292 @Override |
| 293 public void onActivityPaused(Activity activity) { |
| 294 removeMediaRouterCallback(); |
| 295 } |
| 296 |
| 297 @Override |
| 298 public void onActivityResumed(Activity activity) { |
| 299 addMediaRouterCallback(); |
| 300 } |
| 301 |
| 302 @Override |
| 303 public void onActivitySaveInstanceState (Activity activity, Bundle outState)
{} |
| 304 |
| 305 @Override |
| 306 public void onActivityStarted(Activity activity) { |
| 307 addMediaRouterCallback(); |
| 308 } |
| 309 |
| 310 @Override |
| 311 public void onActivityStopped(Activity activity) { |
| 312 removeMediaRouterCallback(); |
| 313 } |
| 314 |
| 315 @Override |
| 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) { |
| 320 return false; |
| 321 } |
| 322 |
| 323 // Setup a MediaRouteActionProvider using the button. |
| 324 MediaRouteActionProvider mediaRouteActionProvider = |
| 325 (MediaRouteActionProvider) MenuItemCompat.getActionProvider(medi
aRouteMenuItem); |
| 326 mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector); |
| 327 |
| 328 return true; |
| 329 } |
| 330 |
| 331 @Override |
| 332 public boolean onActivityOptionsItemSelected(Activity activity, MenuItem ite
m) { |
| 333 if (item.getItemId() == R.id.actionbar_disconnect) { |
| 334 removeMediaRouterCallback(); |
| 335 showToast(R.string.connection_to_cast_closed, Toast.LENGTH_SHORT); |
| 336 tearDown(); |
| 337 return true; |
| 338 } |
| 339 return false; |
| 340 } |
| 341 |
| 342 // |
| 343 // Extension Message Handling logic |
| 344 // |
| 345 |
| 346 /** Sends a message to the Chromoting host. */ |
| 347 private void sendMessageToHost(String data) { |
| 348 JniInterface.sendExtensionMessage(EXTENSION_MSG_TYPE, data); |
| 349 } |
| 350 |
| 351 /** Sends any messages in the message queue to the Cast device. */ |
| 352 private void sendPendingMessagesToCastDevice() { |
| 353 for (String msg : mChromotocastMessageQueue) { |
| 354 sendMessageToCastDevice(msg); |
| 355 } |
| 356 mChromotocastMessageQueue.clear(); |
| 357 } |
| 358 |
| 359 // |
| 360 // Cast Sender API logic |
| 361 // |
| 362 |
| 363 /** |
| 364 * Initializes and connects to Google Play Services. |
| 365 */ |
| 366 private void connectApiClient() { |
| 367 if (mContext == null) { |
| 368 return; |
| 369 } |
| 370 mCastClientListener = new CastClientListener(); |
| 371 mConnectionCallbacks = new ConnectionCallbacks(); |
| 372 mConnectionFailedListener = new ConnectionFailedListener(); |
| 373 |
| 374 Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions |
| 375 .builder(mSelectedDevice, mCastClientListener) |
| 376 .setVerboseLoggingEnabled(true); |
| 377 |
| 378 mApiClient = new GoogleApiClient.Builder(mContext) |
| 379 .addApi(Cast.API, apiOptionsBuilder.build()) |
| 380 .addConnectionCallbacks(mConnectionCallbacks) |
| 381 .addOnConnectionFailedListener(mConnectionFailedListener) |
| 382 .build(); |
| 383 mApiClient.connect(); |
| 384 } |
| 385 |
| 386 /** |
| 387 * Adds the callback object to the MediaRouter. Called when the owning activ
ity starts/resumes. |
| 388 */ |
| 389 private void addMediaRouterCallback() { |
| 390 if (mMediaRouter != null && mMediaRouteSelector != null && mMediaRouterC
allback != null) { |
| 391 mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback, |
| 392 MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); |
| 393 } |
| 394 } |
| 395 |
| 396 /** |
| 397 * Removes the callback object from the MediaRouter. Called when the owning
activity |
| 398 * stops/pauses. |
| 399 */ |
| 400 private void removeMediaRouterCallback() { |
| 401 if (mMediaRouter != null && mMediaRouterCallback != null) { |
| 402 mMediaRouter.removeCallback(mMediaRouterCallback); |
| 403 } |
| 404 } |
| 405 |
| 406 /** |
| 407 * Sends a message to the Cast device on the CHROMOTOCAST_NAMESPACE. |
| 408 */ |
| 409 private void sendMessageToCastDevice(String message) { |
| 410 if (mApiClient == null || mChromotocastChannel == null) { |
| 411 return; |
| 412 } |
| 413 Cast.CastApi.sendMessage(mApiClient, mChromotocastChannel.getNamespace()
, message) |
| 414 .setResultCallback(new ResultCallback<Status>() { |
| 415 @Override |
| 416 public void onResult(Status result) { |
| 417 if (!result.isSuccess()) { |
| 418 Log.e(TAG, "Failed to send message to cast device.")
; |
| 419 } |
| 420 } |
| 421 }); |
| 422 |
| 423 } |
| 424 |
| 425 /** |
| 426 * Restablishes the chromotocast message channel, so we can continue communi
cating with the |
| 427 * Google Cast device. This must be called when resuming a connection. |
| 428 */ |
| 429 private void reconnectChannels() { |
| 430 if (mApiClient == null && mChromotocastChannel == null) { |
| 431 return; |
| 432 } |
| 433 try { |
| 434 Cast.CastApi.setMessageReceivedCallbacks( |
| 435 mApiClient,mChromotocastChannel.getNamespace(),mChromotocast
Channel); |
| 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); |
| 441 } |
| 442 } |
| 443 |
| 444 /** |
| 445 * Stops the running application on the Google Cast device and performs the
required tearDown |
| 446 * sequence. |
| 447 */ |
| 448 private void tearDown() { |
| 449 if (mApiClient != null && mApplicationStarted && mApiClient.isConnected(
)) { |
| 450 Cast.CastApi.stopApplication(mApiClient, mSessionId); |
| 451 if (mChromotocastChannel != null) { |
| 452 try { |
| 453 Cast.CastApi.removeMessageReceivedCallbacks( |
| 454 mApiClient, mChromotocastChannel.getNamespace()); |
| 455 } catch (IOException e) { |
| 456 Log.e(TAG, "Failed to remove chromotocast channel."); |
| 457 } |
| 458 } |
| 459 mApiClient.disconnect(); |
| 460 } |
| 461 mChromotocastChannel = null; |
| 462 mApplicationStarted = false; |
| 463 mApiClient = null; |
| 464 mSelectedDevice = null; |
| 465 mWaitingForReconnect = false; |
| 466 mSessionId = null; |
| 467 } |
| 468 |
| 469 /** |
| 470 * Makes a toast using the given message and duration. |
| 471 */ |
| 472 private void showToast(int messageId, int duration) { |
| 473 if (mContext != null) { |
| 474 Toast.makeText(mContext, mContext.getString(messageId), duration).sh
ow(); |
| 475 } |
| 476 } |
| 477 } |
OLD | NEW |