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