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