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

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

Powered by Google App Engine
This is Rietveld 408576698