Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / android / cast / src / org / chromium / chromoting / CastExtensionHandler.java
blob6b49e9a9c186c6574908a467e5e447ea5457b6fb
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;
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.
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";
54 /**
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 {
107 @Override
108 public void onRouteSelected(MediaRouter router, RouteInfo info) {
109 mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
110 connectApiClient();
113 @Override
114 public void onRouteUnselected(MediaRouter router, RouteInfo info) {
115 tearDown();
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> {
126 @Override
127 public void onResult(Cast.ApplicationConnectionResult result) {
128 Status status = result.getStatus();
129 if (!status.isSuccess()) {
130 tearDown();
131 return;
134 mSessionId = result.getSessionId();
135 mApplicationStatus = result.getApplicationStatus();
136 mApplicationStarted = result.getWasLaunched();
137 mChromotocastChannel = new ChromotocastChannel();
139 try {
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);
145 tearDown();
146 } catch (IllegalStateException e) {
147 showToast(R.string.connection_to_cast_failed, Toast.LENGTH_SHORT);
148 tearDown();
154 * A callback class for receiving events about client connections and disconnections from
155 * Google Play Services.
157 private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks {
158 @Override
159 public void onConnected(Bundle connectionHint) {
160 if (mWaitingForReconnect) {
161 mWaitingForReconnect = false;
162 reconnectChannels();
163 return;
165 Cast.CastApi.launchApplication(mApiClient, RECEIVER_APP_ID, false).setResultCallback(
166 new ApplicationConnectionResultCallback());
169 @Override
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 {
179 @Override
180 public void onConnectionFailed(ConnectionResult result) {
181 Log.e(TAG, String.format("Google Play Service connection failed: %s", result));
183 tearDown();
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;
200 @Override
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 {
212 @Override
213 public void onApplicationStatusChanged() {
214 try {
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);
220 tearDown();
224 @Override
225 public void onVolumeChanged() {} // Changes in volume do not affect us.
227 @Override
228 public void onApplicationDisconnected(int errorCode) {
229 if (errorCode != CastStatusCodes.SUCCESS) {
230 Log.e(TAG, String.format("Application disconnected with: %d", errorCode));
232 tearDown();
237 * Constructs a CastExtensionHandler with an empty message queue.
239 public CastExtensionHandler() {
240 mChromotocastMessageQueue = new ArrayList<String>();
244 // ClientExtension implementation.
247 @Override
248 public String getCapability() {
249 return Capabilities.CAST_CAPABILITY;
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();
259 return true;
261 return false;
264 @Override
265 public ActivityLifecycleListener onActivityAcceptingListener(Activity activity) {
266 return this;
270 // ActivityLifecycleListener implementation.
273 /** Initializes the MediaRouter and related objects using the provided activity Context. */
274 @Override
275 public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
276 if (activity == null) {
277 return;
279 mContext = activity;
280 mMediaRouter = MediaRouter.getInstance(activity);
281 mMediaRouteSelector = new MediaRouteSelector.Builder()
282 .addControlCategory(CastMediaControlIntent.categoryForCast(RECEIVER_APP_ID))
283 .build();
284 mMediaRouterCallback = new CustomMediaRouterCallback();
287 @Override
288 public void onActivityDestroyed(Activity activity) {
289 tearDown();
292 @Override
293 public void onActivityPaused(Activity activity) {
294 removeMediaRouterCallback();
297 @Override
298 public void onActivityResumed(Activity activity) {
299 addMediaRouterCallback();
302 @Override
303 public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
305 @Override
306 public void onActivityStarted(Activity activity) {
307 addMediaRouterCallback();
310 @Override
311 public void onActivityStopped(Activity activity) {
312 removeMediaRouterCallback();
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;
323 // Setup a MediaRouteActionProvider using the button.
324 MediaRouteActionProvider mediaRouteActionProvider =
325 (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem);
326 mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
328 return true;
331 @Override
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);
336 tearDown();
337 return true;
339 return false;
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) {
368 return;
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)
382 .build();
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
398 * stops/pauses.
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) {
411 return;
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.");
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) {
431 return;
433 try {
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
446 * sequence.
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.");
459 mApiClient.disconnect();
461 mChromotocastChannel = null;
462 mApplicationStarted = false;
463 mApiClient = null;
464 mSelectedDevice = null;
465 mWaitingForReconnect = false;
466 mSessionId = null;
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();