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

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: Minor Fix 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 /**
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 }
OLDNEW
« no previous file with comments | « remoting/android/cast/AndroidManifest.xml.jinja2 ('k') | remoting/android/java/AndroidManifest.xml.jinja2 » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698