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

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: 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.ApplicationMetadata;
21 import com.google.android.gms.cast.Cast;
22 import com.google.android.gms.cast.Cast.Listener;
23 import com.google.android.gms.cast.CastDevice;
24 import com.google.android.gms.cast.CastMediaControlIntent;
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 setup the related cast Cast Receiver A pp on a nearby,
41 * user-selected cast device.
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";
56
57 /** Context that wil be used to initialize the MediaRouter and the GoogleApi Client. */
58 private static Context sContext = 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 /** Object to receive callbacks about media routing changes. */
75 private MediaRouter.Callback mMediaRouterCallback;
76
77 /** Listener for events related to the connected Cast device.*/
78 private Listener mCastClientListener;
79
80 /** Object that handles Google Play services integration. */
81 private GoogleApiClient mApiClient;
82
83 /** Callback object for connection changes with Google Play services. */
84 private ConnectionCallbacks mConnectionCallbacks;
85 private OnConnectionFailedListener mConnectionFailedListener;
86
87 /** Channel for receiving messages from the Cast device. */
88 private ChromotocastChannel mChromotocastChannel;
89
90 /** Current session ID, if there is one. */
91 private String mSessionId;
92
93 /** Queue of messages that are yet to be delivered to the Receiver App. */
94 private List<String> mChromotocastMessageQueue;
95
96 /**
97 * A callback class for receiving events about media routing changes.
98 */
99 private class CustomMediaRouterCallback extends MediaRouter.Callback {
100
101 @Override
102 public void onRouteSelected(MediaRouter router, RouteInfo info) {
103 mSelectedDevice = CastDevice.getFromBundle(info.getExtras());
104 String routeId = info.getId();
105 connectApiClient();
106 }
107
108 @Override
109 public void onRouteUnselected(MediaRouter router, RouteInfo info) {
110 teardown();
111 mSelectedDevice = null;
112 }
113 }
114
115 /**
116 * A callback class for receiving events about client connections and discon nections from
117 * Google Play services.
118 */
119 private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallb acks {
120
121 @Override
122 public void onConnected(Bundle connectionHint) {
123 if (mWaitingForReconnect) {
124 mWaitingForReconnect = false;
125 reconnectChannels();
126 } else {
127 Cast.CastApi.launchApplication(mApiClient, "8A1211E3", false)
128 .setResultCallback(
129 new ResultCallback<Cast.ApplicationConnectionResult>() {
130 @Override
131 public void onResult(Cast.ApplicationConnectionResult result ) {
132 Status status = result.getStatus();
133 if (status.isSuccess()) {
134 ApplicationMetadata applicationMetadata =
135 result.getApplicationMetadata();
136 String sessionId = result.getSessionId();
137 String applicationStatus = result.getApplicationStat us();
138 boolean wasLaunched = result.getWasLaunched();
139
140 mApplicationStarted = true;
141 mSessionId = sessionId;
142 mChromotocastChannel = new ChromotocastChannel();
143 try {
144 Cast.CastApi.setMessageReceivedCallbacks(mApiCli ent,
145 mChromotocastChannel.getNamespace(),
146 mChromotocastChannel);
147 sendPendingMessagesToCastDevice();
148 } catch (IOException e) {
149 Log.e(TAG, "Exception while creating channel.", e);
150 Toast.makeText(
151 sContext, "Failed to connect to Cast device. ",
152 Toast.LENGTH_SHORT).show();
153 } catch (IllegalStateException e) {
154 Toast.makeText(
155 sContext, "Failed to connect to Cast device. ",
aiguha 2014/08/08 05:13:50 This (and some others like it) looks awkward, but
156 Toast.LENGTH_SHORT).show();
157 }
158 } else {
159 teardown();
160 }
161 }
162 });
163 }
164 }
165
166 @Override
167 public void onConnectionSuspended(int cause) {
168 mWaitingForReconnect = true;
169 }
170 }
171
172 /**
173 * A listener for connection failures.
174 */
175 private class ConnectionFailedListener implements GoogleApiClient.OnConnecti onFailedListener {
176
177 @Override
178 public void onConnectionFailed(ConnectionResult arg0) {
179 teardown();
180 }
181
182 }
183
184 /**
185 * A channel for communication with the Cast device on the CHROMOTOCAST_NAME SPACE.
186 */
187 class ChromotocastChannel implements Cast.MessageReceivedCallback {
188 public String getNamespace() {
189 return CHROMOTOCAST_NAMESPACE;
190 }
191
192 @Override
193 public void onMessageReceived(CastDevice castDevice, String namespace,
194 String message) {
195 Log.d(TAG, "onMessageReceived: " + namespace + ":" + message);
196 if (namespace.equals(CHROMOTOCAST_NAMESPACE)) {
197 sendMessageToHost(message);
198 }
199 else {
200 Log.i(TAG, "Unknown namespace message: " + message);
201 }
202 }
203 }
204
205 /** Constructs the CastExtensionHandler with an empty message queue. */
206 public CastExtensionHandler() {
207 mChromotocastMessageQueue = new ArrayList<String>();
208 }
209
210 /** ClientExtension implementation. */
211
212 @Override
213 public String getCapability() {
214 return Capabilities.CAST_CAPABILITY;
215 }
216
217 @Override
218 public boolean onExtensionMessage(String type, String data) {
219 if (type.equals(EXTENSION_MSG_TYPE)) {
220 mChromotocastMessageQueue.add(data);
221 if (mApplicationStarted) {
222 sendPendingMessagesToCastDevice();
223 }
224 return true;
225 }
226 return false;
227 }
228
229 @Override
230 public ActivityLifecycleListener onActivityAcceptingListener(Activity activi ty) {
231 return this;
232 }
233
234 /** ActivityLifecycleListener implementation. */
235
236 /** Initializes the MediaRouter and related objects using the provided activ ity Context. */
237 @Override
238 public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
239 if (activity == null) {
240 Log.w(TAG, "Initialization attemped without activity.");
241 return;
242 }
243 sContext = activity;
244 mMediaRouter = MediaRouter.getInstance(activity);
245 mMediaRouteSelector = new MediaRouteSelector.Builder()
246 .addControlCategory(CastMediaControlIntent.categoryForCast(RECEIVER_APP_ ID))
247 .build();
248 mMediaRouterCallback = new CustomMediaRouterCallback();
249 }
250
251 @Override
252 public void onActivityDestroyed(Activity activity) {
253 teardown();
254 }
255
256 @Override
257 public void onActivityPaused(Activity activity) {
258 removeMediaRouterCallback();
259 }
260
261 @Override
262 public void onActivityResumed(Activity activity) {
263 addMediaRouterCallback();
264 }
265
266 @Override
267 public void onActivitySaveInstanceState (Activity activity, Bundle outState) {}
268
269 @Override
270 public void onActivityStarted(Activity activity) {
271 addMediaRouterCallback();
272 }
273
274 @Override
275 public void onActivityStopped(Activity activity) {
276 removeMediaRouterCallback();
277 }
278
279 /** Sets up the cast menu item on the application. */
280 @Override
281 public boolean onActivityCreatedOptionsMenu(Activity activity, Menu menu) {
282 MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
283 if (mediaRouteMenuItem == null) {
284 Log.w(TAG, "Cannot setup route selector without menu item.");
285 return false;
286 }
287 MediaRouteActionProvider mediaRouteActionProvider =
288 (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRou teMenuItem);
289 mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
290 return true;
291 }
292
293 @Override
294 public boolean onActivityOptionsItemSelected(Activity activity, MenuItem ite m) {
295 if (item.getItemId() == R.id.actionbar_disconnect) {
296 removeMediaRouterCallback();
297 Toast.makeText(
298 sContext, "Closing connection to Cast device.", Toast.LENGTH_SHO RT).show();
299 teardown();
300 return true;
301 }
302 return false;
303 }
304
305
306 /** Extension Message Handling logic */
307
308 /** Sends a message to the Chromoting host. */
309 private void sendMessageToHost(String data) {
310 JniInterface.sendExtensionMessage(EXTENSION_MSG_TYPE, data);
311 }
312
313 /** Sends any messages in the message queue to the Cast device. */
314 private void sendPendingMessagesToCastDevice() {
315 for (String msg : mChromotocastMessageQueue) {
316 sendMessageToCastDevice(msg);
317 }
318 mChromotocastMessageQueue.clear();
319 }
320
321 /** Cast Sender API logic */
322
323 /**
324 * Initializes and connects to the Google Play Services service.
325 */
326 private void connectApiClient() {
327 if (sContext == null) {
328 Log.e(TAG, "Cannot connect Api Client without context.");
329 return;
330 }
331 mCastClientListener = new Cast.Listener() {
332 @Override
333 public void onApplicationStatusChanged() {
334 try {
335 if (mApiClient != null) {
336 Log.d(TAG, "onApplicationStatusChanged: "
337 + Cast.CastApi.getApplicationStatus(mApiClient));
338 }
339 } catch (IllegalStateException e) {
340 Toast.makeText(
341 sContext, "Lost connection to Cast device.", Toast.LENGT H_SHORT).show();
342 teardown();
343 }
344 }
345
346 @Override
347 public void onVolumeChanged() {
348 try {
349 if (mApiClient != null) {
350 Log.d(TAG, "onVolumeChanged: " + Cast.CastApi.getVolume( mApiClient));
351 }
352 } catch (IllegalStateException e) {
353 Toast.makeText(
354 sContext, "Lost connection to Cast device.", Toast.LENGT H_SHORT).show();
355 teardown();
356 }
357 }
358
359 @Override
360 public void onApplicationDisconnected(int errorCode) {
361 teardown();
362 }
363 };
364 Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions
365 .builder(mSelectedDevice, mCastClientListener)
366 .setVerboseLoggingEnabled(true);
367 mConnectionCallbacks = new ConnectionCallbacks();
368 mConnectionFailedListener = new ConnectionFailedListener();
369 mApiClient = new GoogleApiClient.Builder(sContext)
370 .addApi(Cast.API, apiOptionsBuilder.build())
371 .addConnectionCallbacks(mConnectionCallbacks)
372 .addOnConnectionFailedListener(mConnectionFailedListener)
373 .build();
374 mApiClient.connect();
375 }
376
377 /**
378 * Adds the callback object to the MediaRouter. Called when the owning activ ity starts/resumes.
379 */
380 private void addMediaRouterCallback() {
381 if (mMediaRouter != null && mMediaRouteSelector != null && mMediaRouterC allback != null) {
382 mMediaRouter.addCallback(mMediaRouteSelector, mMediaRouterCallback,
383 MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
384 }
385 }
386
387 /**
388 * Removes the callback object from the MediaRouter. Called when the owning activity
389 * stops/pauses.
390 */
391 private void removeMediaRouterCallback() {
392 if (mMediaRouter != null && mMediaRouterCallback != null) {
393 mMediaRouter.removeCallback(mMediaRouterCallback);
394 }
395 }
396
397
398 /** Sends a message to the Cast device on the CHROMOTOCAST_NAMESPACE. */
399 private void sendMessageToCastDevice(String message) {
400 if (mApiClient != null && mChromotocastChannel != null) {
401 Cast.CastApi.sendMessage(mApiClient, mChromotocastChannel.getNamespa ce(), message)
402 .setResultCallback(
403 new ResultCallback<Status>() {
404 @Override
405 public void onResult(Status result) {
406 if (!result.isSuccess()) {
407 Log.e(TAG, "Failed to send message to cast device.");
408 }
409 }
410 });
411 }
412 }
413
414 /** Restablishes connected channels, which must be done when resuming a conn ection. */
415 private void reconnectChannels() {
416 if (mApiClient != null && mChromotocastChannel != null) {
417 try {
418 Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
419 mChromotocastChannel.getNamespace(),
420 mChromotocastChannel);
421 sendPendingMessagesToCastDevice();
422 } catch (IOException e) {
423 Log.e(TAG, "Exception while creating channel.", e);
424 Toast.makeText(
425 sContext, "Failed to reconnect to Cast device.", Toast.LENGT H_SHORT).show();
426 } catch (IllegalStateException e) {
427 Toast.makeText(
428 sContext, "Lost connection to Cast device.", Toast.LENGTH_SH ORT).show();
429 }
430 }
431 }
432
433 /** Stops the running application on the Cast device and tears down relevant objects. */
434 private void teardown() {
435 if (mApiClient != null) {
436 if (mApplicationStarted) {
437 if (mApiClient.isConnected()) {
438 try {
439 Cast.CastApi.stopApplication(mApiClient, mSessionId);
440 if (mChromotocastChannel != null) {
441 Cast.CastApi.removeMessageReceivedCallbacks(
442 mApiClient,
443 mChromotocastChannel.getNamespace());
444 mChromotocastChannel = null;
445 }
446 } catch (IOException e) {
447 Log.e(TAG, "Exception while removing channel.", e);
448 }
449 mApiClient.disconnect();
450 }
451 mApplicationStarted = false;
452 }
453 mApiClient = null;
454 }
455 mSelectedDevice = null;
456 mWaitingForReconnect = false;
457 mSessionId = null;
458 }
459 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698