OLD | NEW |
---|---|
(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 } | |
OLD | NEW |