Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(230)

Side by Side Diff: remoting/android/cast/src/org/chromium/chromoting/CastExtensionHandler.java

Issue 451973002: Capabilities + Extensions + Cast Host Extension Support for Android client (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: FindBugs Pass + Changes after Review Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698