blob: f91ea8c03a39d28298b6a0e38ce1d469a0bcbc2e [file] [log] [blame]
Jeff Brown69b07162013-11-07 00:30:16 -08001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.media;
18
Jeff Brown69b07162013-11-07 00:30:16 -080019import com.android.server.Watchdog;
20
21import android.Manifest;
22import android.app.ActivityManager;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.pm.PackageManager;
28import android.media.AudioSystem;
29import android.media.IMediaRouterClient;
30import android.media.IMediaRouterService;
31import android.media.MediaRouter;
32import android.media.MediaRouterClientState;
33import android.media.RemoteDisplayState;
34import android.media.RemoteDisplayState.RemoteDisplayInfo;
35import android.os.Binder;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.Looper;
39import android.os.Message;
40import android.os.RemoteException;
41import android.os.SystemClock;
42import android.text.TextUtils;
43import android.util.ArrayMap;
44import android.util.Log;
45import android.util.Slog;
46import android.util.SparseArray;
47import android.util.TimeUtils;
48
49import java.io.FileDescriptor;
50import java.io.PrintWriter;
51import java.util.ArrayList;
52import java.util.Collections;
53import java.util.List;
Kenny Roote6585b32013-12-13 12:00:26 -080054import java.util.Objects;
Jeff Brown69b07162013-11-07 00:30:16 -080055
56/**
57 * Provides a mechanism for discovering media routes and manages media playback
58 * behalf of applications.
59 * <p>
60 * Currently supports discovering remote displays via remote display provider
61 * services that have been registered by applications.
62 * </p>
63 */
64public final class MediaRouterService extends IMediaRouterService.Stub
65 implements Watchdog.Monitor {
66 private static final String TAG = "MediaRouterService";
67 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
68
69 /**
70 * Timeout in milliseconds for a selected route to transition from a
71 * disconnected state to a connecting state. If we don't observe any
72 * progress within this interval, then we will give up and unselect the route.
73 */
74 static final long CONNECTING_TIMEOUT = 5000;
75
76 /**
77 * Timeout in milliseconds for a selected route to transition from a
78 * connecting state to a connected state. If we don't observe any
79 * progress within this interval, then we will give up and unselect the route.
80 */
81 static final long CONNECTED_TIMEOUT = 60000;
82
83 private final Context mContext;
84
85 // State guarded by mLock.
86 private final Object mLock = new Object();
87 private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
88 private final ArrayMap<IBinder, ClientRecord> mAllClientRecords =
89 new ArrayMap<IBinder, ClientRecord>();
90 private int mCurrentUserId = -1;
91
92 public MediaRouterService(Context context) {
93 mContext = context;
94 Watchdog.getInstance().addMonitor(this);
95 }
96
97 public void systemRunning() {
98 IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
99 mContext.registerReceiver(new BroadcastReceiver() {
100 @Override
101 public void onReceive(Context context, Intent intent) {
102 if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) {
103 switchUser();
104 }
105 }
106 }, filter);
107
108 switchUser();
109 }
110
111 @Override
112 public void monitor() {
113 synchronized (mLock) { /* check for deadlock */ }
114 }
115
116 // Binder call
117 @Override
118 public void registerClientAsUser(IMediaRouterClient client, String packageName, int userId) {
119 if (client == null) {
120 throw new IllegalArgumentException("client must not be null");
121 }
122
123 final int uid = Binder.getCallingUid();
124 if (!validatePackageName(uid, packageName)) {
125 throw new SecurityException("packageName must match the calling uid");
126 }
127
128 final int pid = Binder.getCallingPid();
129 final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
130 false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName);
Jeff Brownaf574182013-11-14 18:16:08 -0800131 final boolean trusted = mContext.checkCallingOrSelfPermission(
132 android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) ==
133 PackageManager.PERMISSION_GRANTED;
Jeff Brown69b07162013-11-07 00:30:16 -0800134 final long token = Binder.clearCallingIdentity();
135 try {
136 synchronized (mLock) {
Jeff Brownaf574182013-11-14 18:16:08 -0800137 registerClientLocked(client, pid, packageName, resolvedUserId, trusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800138 }
139 } finally {
140 Binder.restoreCallingIdentity(token);
141 }
142 }
143
144 // Binder call
145 @Override
146 public void unregisterClient(IMediaRouterClient client) {
147 if (client == null) {
148 throw new IllegalArgumentException("client must not be null");
149 }
150
151 final long token = Binder.clearCallingIdentity();
152 try {
153 synchronized (mLock) {
154 unregisterClientLocked(client, false);
155 }
156 } finally {
157 Binder.restoreCallingIdentity(token);
158 }
159 }
160
161 // Binder call
162 @Override
163 public MediaRouterClientState getState(IMediaRouterClient client) {
164 if (client == null) {
165 throw new IllegalArgumentException("client must not be null");
166 }
167
168 final long token = Binder.clearCallingIdentity();
169 try {
170 synchronized (mLock) {
171 return getStateLocked(client);
172 }
173 } finally {
174 Binder.restoreCallingIdentity(token);
175 }
176 }
177
178 // Binder call
179 @Override
180 public void setDiscoveryRequest(IMediaRouterClient client,
181 int routeTypes, boolean activeScan) {
182 if (client == null) {
183 throw new IllegalArgumentException("client must not be null");
184 }
185
186 final long token = Binder.clearCallingIdentity();
187 try {
188 synchronized (mLock) {
189 setDiscoveryRequestLocked(client, routeTypes, activeScan);
190 }
191 } finally {
192 Binder.restoreCallingIdentity(token);
193 }
194 }
195
196 // Binder call
197 // A null routeId means that the client wants to unselect its current route.
198 // The explicit flag indicates whether the change was explicitly requested by the
199 // user or the application which may cause changes to propagate out to the rest
200 // of the system. Should be false when the change is in response to a new globally
201 // selected route or a default selection.
202 @Override
203 public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) {
204 if (client == null) {
205 throw new IllegalArgumentException("client must not be null");
206 }
207
208 final long token = Binder.clearCallingIdentity();
209 try {
210 synchronized (mLock) {
211 setSelectedRouteLocked(client, routeId, explicit);
212 }
213 } finally {
214 Binder.restoreCallingIdentity(token);
215 }
216 }
217
218 // Binder call
219 @Override
220 public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) {
221 if (client == null) {
222 throw new IllegalArgumentException("client must not be null");
223 }
224 if (routeId == null) {
225 throw new IllegalArgumentException("routeId must not be null");
226 }
227
228 final long token = Binder.clearCallingIdentity();
229 try {
230 synchronized (mLock) {
231 requestSetVolumeLocked(client, routeId, volume);
232 }
233 } finally {
234 Binder.restoreCallingIdentity(token);
235 }
236 }
237
238 // Binder call
239 @Override
240 public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) {
241 if (client == null) {
242 throw new IllegalArgumentException("client must not be null");
243 }
244 if (routeId == null) {
245 throw new IllegalArgumentException("routeId must not be null");
246 }
247
248 final long token = Binder.clearCallingIdentity();
249 try {
250 synchronized (mLock) {
251 requestUpdateVolumeLocked(client, routeId, direction);
252 }
253 } finally {
254 Binder.restoreCallingIdentity(token);
255 }
256 }
257
258 // Binder call
259 @Override
260 public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
261 if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
262 != PackageManager.PERMISSION_GRANTED) {
263 pw.println("Permission Denial: can't dump MediaRouterService from from pid="
264 + Binder.getCallingPid()
265 + ", uid=" + Binder.getCallingUid());
266 return;
267 }
268
269 pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)");
270 pw.println();
271 pw.println("Global state");
272 pw.println(" mCurrentUserId=" + mCurrentUserId);
273
274 synchronized (mLock) {
275 final int count = mUserRecords.size();
276 for (int i = 0; i < count; i++) {
277 UserRecord userRecord = mUserRecords.valueAt(i);
278 pw.println();
279 userRecord.dump(pw, "");
280 }
281 }
282 }
283
284 void switchUser() {
285 synchronized (mLock) {
286 int userId = ActivityManager.getCurrentUser();
287 if (mCurrentUserId != userId) {
288 final int oldUserId = mCurrentUserId;
289 mCurrentUserId = userId; // do this first
290
291 UserRecord oldUser = mUserRecords.get(oldUserId);
292 if (oldUser != null) {
293 oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP);
294 disposeUserIfNeededLocked(oldUser); // since no longer current user
295 }
296
297 UserRecord newUser = mUserRecords.get(userId);
298 if (newUser != null) {
299 newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START);
300 }
301 }
302 }
303 }
304
305 void clientDied(ClientRecord clientRecord) {
306 synchronized (mLock) {
307 unregisterClientLocked(clientRecord.mClient, true);
308 }
309 }
310
311 private void registerClientLocked(IMediaRouterClient client,
Jeff Brownaf574182013-11-14 18:16:08 -0800312 int pid, String packageName, int userId, boolean trusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800313 final IBinder binder = client.asBinder();
314 ClientRecord clientRecord = mAllClientRecords.get(binder);
315 if (clientRecord == null) {
316 boolean newUser = false;
317 UserRecord userRecord = mUserRecords.get(userId);
318 if (userRecord == null) {
319 userRecord = new UserRecord(userId);
320 newUser = true;
321 }
Jeff Brownaf574182013-11-14 18:16:08 -0800322 clientRecord = new ClientRecord(userRecord, client, pid, packageName, trusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800323 try {
324 binder.linkToDeath(clientRecord, 0);
325 } catch (RemoteException ex) {
326 throw new RuntimeException("Media router client died prematurely.", ex);
327 }
328
329 if (newUser) {
330 mUserRecords.put(userId, userRecord);
331 initializeUserLocked(userRecord);
332 }
333
334 userRecord.mClientRecords.add(clientRecord);
335 mAllClientRecords.put(binder, clientRecord);
336 initializeClientLocked(clientRecord);
337 }
338 }
339
340 private void unregisterClientLocked(IMediaRouterClient client, boolean died) {
341 ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder());
342 if (clientRecord != null) {
343 UserRecord userRecord = clientRecord.mUserRecord;
344 userRecord.mClientRecords.remove(clientRecord);
345 disposeClientLocked(clientRecord, died);
346 disposeUserIfNeededLocked(userRecord); // since client removed from user
347 }
348 }
349
350 private MediaRouterClientState getStateLocked(IMediaRouterClient client) {
351 ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
352 if (clientRecord != null) {
Jeff Brownaf574182013-11-14 18:16:08 -0800353 return clientRecord.getState();
Jeff Brown69b07162013-11-07 00:30:16 -0800354 }
355 return null;
356 }
357
358 private void setDiscoveryRequestLocked(IMediaRouterClient client,
359 int routeTypes, boolean activeScan) {
360 final IBinder binder = client.asBinder();
361 ClientRecord clientRecord = mAllClientRecords.get(binder);
362 if (clientRecord != null) {
Jeff Brownaf574182013-11-14 18:16:08 -0800363 // Only let the system discover remote display routes for now.
364 if (!clientRecord.mTrusted) {
365 routeTypes &= ~MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
366 }
367
Jeff Brown69b07162013-11-07 00:30:16 -0800368 if (clientRecord.mRouteTypes != routeTypes
369 || clientRecord.mActiveScan != activeScan) {
370 if (DEBUG) {
371 Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x"
372 + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan);
373 }
374 clientRecord.mRouteTypes = routeTypes;
375 clientRecord.mActiveScan = activeScan;
376 clientRecord.mUserRecord.mHandler.sendEmptyMessage(
377 UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
378 }
379 }
380 }
381
382 private void setSelectedRouteLocked(IMediaRouterClient client,
383 String routeId, boolean explicit) {
384 ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
385 if (clientRecord != null) {
386 final String oldRouteId = clientRecord.mSelectedRouteId;
Kenny Roote6585b32013-12-13 12:00:26 -0800387 if (!Objects.equals(routeId, oldRouteId)) {
Jeff Brown69b07162013-11-07 00:30:16 -0800388 if (DEBUG) {
389 Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId
390 + ", oldRouteId=" + oldRouteId
391 + ", explicit=" + explicit);
392 }
393
394 clientRecord.mSelectedRouteId = routeId;
395 if (explicit) {
Jeff Brownaf574182013-11-14 18:16:08 -0800396 // Any app can disconnect from the globally selected route.
Jeff Brown69b07162013-11-07 00:30:16 -0800397 if (oldRouteId != null) {
398 clientRecord.mUserRecord.mHandler.obtainMessage(
399 UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget();
400 }
Jeff Brownaf574182013-11-14 18:16:08 -0800401 // Only let the system connect to new global routes for now.
402 // A similar check exists in the display manager for wifi display.
403 if (routeId != null && clientRecord.mTrusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800404 clientRecord.mUserRecord.mHandler.obtainMessage(
405 UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
406 }
407 }
408 }
409 }
410 }
411
412 private void requestSetVolumeLocked(IMediaRouterClient client,
413 String routeId, int volume) {
414 final IBinder binder = client.asBinder();
415 ClientRecord clientRecord = mAllClientRecords.get(binder);
416 if (clientRecord != null) {
417 clientRecord.mUserRecord.mHandler.obtainMessage(
418 UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget();
419 }
420 }
421
422 private void requestUpdateVolumeLocked(IMediaRouterClient client,
423 String routeId, int direction) {
424 final IBinder binder = client.asBinder();
425 ClientRecord clientRecord = mAllClientRecords.get(binder);
426 if (clientRecord != null) {
427 clientRecord.mUserRecord.mHandler.obtainMessage(
428 UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget();
429 }
430 }
431
432 private void initializeUserLocked(UserRecord userRecord) {
433 if (DEBUG) {
434 Slog.d(TAG, userRecord + ": Initialized");
435 }
436 if (userRecord.mUserId == mCurrentUserId) {
437 userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START);
438 }
439 }
440
441 private void disposeUserIfNeededLocked(UserRecord userRecord) {
442 // If there are no records left and the user is no longer current then go ahead
443 // and purge the user record and all of its associated state. If the user is current
444 // then leave it alone since we might be connected to a route or want to query
445 // the same route information again soon.
446 if (userRecord.mUserId != mCurrentUserId
447 && userRecord.mClientRecords.isEmpty()) {
448 if (DEBUG) {
449 Slog.d(TAG, userRecord + ": Disposed");
450 }
451 mUserRecords.remove(userRecord.mUserId);
452 // Note: User already stopped (by switchUser) so no need to send stop message here.
453 }
454 }
455
456 private void initializeClientLocked(ClientRecord clientRecord) {
457 if (DEBUG) {
458 Slog.d(TAG, clientRecord + ": Registered");
459 }
460 }
461
462 private void disposeClientLocked(ClientRecord clientRecord, boolean died) {
463 if (DEBUG) {
464 if (died) {
465 Slog.d(TAG, clientRecord + ": Died!");
466 } else {
467 Slog.d(TAG, clientRecord + ": Unregistered");
468 }
469 }
470 if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) {
471 clientRecord.mUserRecord.mHandler.sendEmptyMessage(
472 UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
473 }
474 clientRecord.dispose();
475 }
476
477 private boolean validatePackageName(int uid, String packageName) {
478 if (packageName != null) {
479 String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
480 if (packageNames != null) {
481 for (String n : packageNames) {
482 if (n.equals(packageName)) {
483 return true;
484 }
485 }
486 }
487 }
488 return false;
489 }
490
491 /**
492 * Information about a particular client of the media router.
493 * The contents of this object is guarded by mLock.
494 */
495 final class ClientRecord implements DeathRecipient {
496 public final UserRecord mUserRecord;
497 public final IMediaRouterClient mClient;
498 public final int mPid;
499 public final String mPackageName;
Jeff Brownaf574182013-11-14 18:16:08 -0800500 public final boolean mTrusted;
Jeff Brown69b07162013-11-07 00:30:16 -0800501
502 public int mRouteTypes;
503 public boolean mActiveScan;
504 public String mSelectedRouteId;
505
506 public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
Jeff Brownaf574182013-11-14 18:16:08 -0800507 int pid, String packageName, boolean trusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800508 mUserRecord = userRecord;
509 mClient = client;
510 mPid = pid;
511 mPackageName = packageName;
Jeff Brownaf574182013-11-14 18:16:08 -0800512 mTrusted = trusted;
Jeff Brown69b07162013-11-07 00:30:16 -0800513 }
514
515 public void dispose() {
516 mClient.asBinder().unlinkToDeath(this, 0);
517 }
518
519 @Override
520 public void binderDied() {
521 clientDied(this);
522 }
523
Jeff Brownaf574182013-11-14 18:16:08 -0800524 MediaRouterClientState getState() {
525 return mTrusted ? mUserRecord.mTrustedState : mUserRecord.mUntrustedState;
526 }
527
Jeff Brown69b07162013-11-07 00:30:16 -0800528 public void dump(PrintWriter pw, String prefix) {
529 pw.println(prefix + this);
530
531 final String indent = prefix + " ";
Jeff Brownaf574182013-11-14 18:16:08 -0800532 pw.println(indent + "mTrusted=" + mTrusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800533 pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes));
534 pw.println(indent + "mActiveScan=" + mActiveScan);
535 pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId);
536 }
537
538 @Override
539 public String toString() {
540 return "Client " + mPackageName + " (pid " + mPid + ")";
541 }
542 }
543
544 /**
545 * Information about a particular user.
546 * The contents of this object is guarded by mLock.
547 */
548 final class UserRecord {
549 public final int mUserId;
550 public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
551 public final UserHandler mHandler;
Jeff Brownaf574182013-11-14 18:16:08 -0800552 public MediaRouterClientState mTrustedState;
553 public MediaRouterClientState mUntrustedState;
Jeff Brown69b07162013-11-07 00:30:16 -0800554
555 public UserRecord(int userId) {
556 mUserId = userId;
557 mHandler = new UserHandler(MediaRouterService.this, this);
558 }
559
560 public void dump(final PrintWriter pw, String prefix) {
561 pw.println(prefix + this);
562
563 final String indent = prefix + " ";
564 final int clientCount = mClientRecords.size();
565 if (clientCount != 0) {
566 for (int i = 0; i < clientCount; i++) {
567 mClientRecords.get(i).dump(pw, indent);
568 }
569 } else {
570 pw.println(indent + "<no clients>");
571 }
572
Jeff Brownaf574182013-11-14 18:16:08 -0800573 pw.println(indent + "State");
574 pw.println(indent + "mTrustedState=" + mTrustedState);
575 pw.println(indent + "mUntrustedState=" + mUntrustedState);
576
Jeff Brown69b07162013-11-07 00:30:16 -0800577 if (!mHandler.runWithScissors(new Runnable() {
578 @Override
579 public void run() {
580 mHandler.dump(pw, indent);
581 }
582 }, 1000)) {
583 pw.println(indent + "<could not dump handler state>");
584 }
585 }
586
587 @Override
588 public String toString() {
589 return "User " + mUserId;
590 }
591 }
592
593 /**
594 * Media router handler
595 * <p>
596 * Since remote display providers are designed to be single-threaded by nature,
597 * this class encapsulates all of the associated functionality and exports state
598 * to the service as it evolves.
599 * </p><p>
600 * One important task of this class is to keep track of the current globally selected
601 * route id for certain routes that have global effects, such as remote displays.
602 * Global route selections override local selections made within apps. The change
603 * is propagated to all apps so that they are all in sync. Synchronization works
604 * both ways. Whenever the globally selected route is explicitly unselected by any
605 * app, then it becomes unselected globally and all apps are informed.
606 * </p><p>
607 * This class is currently hardcoded to work with remote display providers but
608 * it is intended to be eventually extended to support more general route providers
609 * similar to the support library media router.
610 * </p>
611 */
612 static final class UserHandler extends Handler
613 implements RemoteDisplayProviderWatcher.Callback,
614 RemoteDisplayProviderProxy.Callback {
615 public static final int MSG_START = 1;
616 public static final int MSG_STOP = 2;
617 public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3;
618 public static final int MSG_SELECT_ROUTE = 4;
619 public static final int MSG_UNSELECT_ROUTE = 5;
620 public static final int MSG_REQUEST_SET_VOLUME = 6;
621 public static final int MSG_REQUEST_UPDATE_VOLUME = 7;
622 private static final int MSG_UPDATE_CLIENT_STATE = 8;
623 private static final int MSG_CONNECTION_TIMED_OUT = 9;
624
625 private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800626 private static final int TIMEOUT_REASON_CONNECTION_LOST = 2;
627 private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 3;
628 private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 4;
629
630 // The relative order of these constants is important and expresses progress
631 // through the process of connecting to a route.
632 private static final int PHASE_NOT_AVAILABLE = -1;
633 private static final int PHASE_NOT_CONNECTED = 0;
634 private static final int PHASE_CONNECTING = 1;
635 private static final int PHASE_CONNECTED = 2;
Jeff Brown69b07162013-11-07 00:30:16 -0800636
637 private final MediaRouterService mService;
638 private final UserRecord mUserRecord;
639 private final RemoteDisplayProviderWatcher mWatcher;
640 private final ArrayList<ProviderRecord> mProviderRecords =
641 new ArrayList<ProviderRecord>();
642 private final ArrayList<IMediaRouterClient> mTempClients =
643 new ArrayList<IMediaRouterClient>();
644
645 private boolean mRunning;
646 private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
647 private RouteRecord mGloballySelectedRouteRecord;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800648 private int mConnectionPhase = PHASE_NOT_AVAILABLE;
Jeff Brown69b07162013-11-07 00:30:16 -0800649 private int mConnectionTimeoutReason;
650 private long mConnectionTimeoutStartTime;
651 private boolean mClientStateUpdateScheduled;
652
653 public UserHandler(MediaRouterService service, UserRecord userRecord) {
654 super(Looper.getMainLooper(), null, true);
655 mService = service;
656 mUserRecord = userRecord;
657 mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this,
658 this, mUserRecord.mUserId);
659 }
660
661 @Override
662 public void handleMessage(Message msg) {
663 switch (msg.what) {
664 case MSG_START: {
665 start();
666 break;
667 }
668 case MSG_STOP: {
669 stop();
670 break;
671 }
672 case MSG_UPDATE_DISCOVERY_REQUEST: {
673 updateDiscoveryRequest();
674 break;
675 }
676 case MSG_SELECT_ROUTE: {
677 selectRoute((String)msg.obj);
678 break;
679 }
680 case MSG_UNSELECT_ROUTE: {
681 unselectRoute((String)msg.obj);
682 break;
683 }
684 case MSG_REQUEST_SET_VOLUME: {
685 requestSetVolume((String)msg.obj, msg.arg1);
686 break;
687 }
688 case MSG_REQUEST_UPDATE_VOLUME: {
689 requestUpdateVolume((String)msg.obj, msg.arg1);
690 break;
691 }
692 case MSG_UPDATE_CLIENT_STATE: {
693 updateClientState();
694 break;
695 }
696 case MSG_CONNECTION_TIMED_OUT: {
697 connectionTimedOut();
698 break;
699 }
700 }
701 }
702
703 public void dump(PrintWriter pw, String prefix) {
704 pw.println(prefix + "Handler");
705
706 final String indent = prefix + " ";
707 pw.println(indent + "mRunning=" + mRunning);
708 pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode);
709 pw.println(indent + "mGloballySelectedRouteRecord=" + mGloballySelectedRouteRecord);
Jeff Brown39ad0e52013-11-11 17:55:08 -0800710 pw.println(indent + "mConnectionPhase=" + mConnectionPhase);
Jeff Brown69b07162013-11-07 00:30:16 -0800711 pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason);
712 pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ?
713 TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "<n/a>"));
714
715 mWatcher.dump(pw, prefix);
716
717 final int providerCount = mProviderRecords.size();
718 if (providerCount != 0) {
719 for (int i = 0; i < providerCount; i++) {
720 mProviderRecords.get(i).dump(pw, prefix);
721 }
722 } else {
723 pw.println(indent + "<no providers>");
724 }
725 }
726
727 private void start() {
728 if (!mRunning) {
729 mRunning = true;
730 mWatcher.start(); // also starts all providers
731 }
732 }
733
734 private void stop() {
735 if (mRunning) {
736 mRunning = false;
737 unselectGloballySelectedRoute();
738 mWatcher.stop(); // also stops all providers
739 }
740 }
741
742 private void updateDiscoveryRequest() {
743 int routeTypes = 0;
744 boolean activeScan = false;
745 synchronized (mService.mLock) {
746 final int count = mUserRecord.mClientRecords.size();
747 for (int i = 0; i < count; i++) {
748 ClientRecord clientRecord = mUserRecord.mClientRecords.get(i);
749 routeTypes |= clientRecord.mRouteTypes;
750 activeScan |= clientRecord.mActiveScan;
751 }
752 }
753
754 final int newDiscoveryMode;
Jeff Brownaf574182013-11-14 18:16:08 -0800755 if ((routeTypes & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
Jeff Brown69b07162013-11-07 00:30:16 -0800756 if (activeScan) {
757 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
758 } else {
759 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;
760 }
761 } else {
762 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
763 }
764
765 if (mDiscoveryMode != newDiscoveryMode) {
766 mDiscoveryMode = newDiscoveryMode;
767 final int count = mProviderRecords.size();
768 for (int i = 0; i < count; i++) {
769 mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode);
770 }
771 }
772 }
773
774 private void selectRoute(String routeId) {
775 if (routeId != null
776 && (mGloballySelectedRouteRecord == null
777 || !routeId.equals(mGloballySelectedRouteRecord.getUniqueId()))) {
778 RouteRecord routeRecord = findRouteRecord(routeId);
779 if (routeRecord != null) {
780 unselectGloballySelectedRoute();
781
782 Slog.i(TAG, "Selected global route:" + routeRecord);
783 mGloballySelectedRouteRecord = routeRecord;
784 checkGloballySelectedRouteState();
785 routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId());
786
787 scheduleUpdateClientState();
788 }
789 }
790 }
791
792 private void unselectRoute(String routeId) {
793 if (routeId != null
794 && mGloballySelectedRouteRecord != null
795 && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
796 unselectGloballySelectedRoute();
797 }
798 }
799
800 private void unselectGloballySelectedRoute() {
801 if (mGloballySelectedRouteRecord != null) {
802 Slog.i(TAG, "Unselected global route:" + mGloballySelectedRouteRecord);
803 mGloballySelectedRouteRecord.getProvider().setSelectedDisplay(null);
804 mGloballySelectedRouteRecord = null;
805 checkGloballySelectedRouteState();
806
807 scheduleUpdateClientState();
808 }
809 }
810
811 private void requestSetVolume(String routeId, int volume) {
812 if (mGloballySelectedRouteRecord != null
813 && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
814 mGloballySelectedRouteRecord.getProvider().setDisplayVolume(volume);
815 }
816 }
817
818 private void requestUpdateVolume(String routeId, int direction) {
819 if (mGloballySelectedRouteRecord != null
820 && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
821 mGloballySelectedRouteRecord.getProvider().adjustDisplayVolume(direction);
822 }
823 }
824
825 @Override
826 public void addProvider(RemoteDisplayProviderProxy provider) {
827 provider.setCallback(this);
828 provider.setDiscoveryMode(mDiscoveryMode);
829 provider.setSelectedDisplay(null); // just to be safe
830
831 ProviderRecord providerRecord = new ProviderRecord(provider);
832 mProviderRecords.add(providerRecord);
833 providerRecord.updateDescriptor(provider.getDisplayState());
834
835 scheduleUpdateClientState();
836 }
837
838 @Override
839 public void removeProvider(RemoteDisplayProviderProxy provider) {
840 int index = findProviderRecord(provider);
841 if (index >= 0) {
842 ProviderRecord providerRecord = mProviderRecords.remove(index);
843 providerRecord.updateDescriptor(null); // mark routes invalid
844 provider.setCallback(null);
845 provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE);
846
847 checkGloballySelectedRouteState();
848 scheduleUpdateClientState();
849 }
850 }
851
852 @Override
853 public void onDisplayStateChanged(RemoteDisplayProviderProxy provider,
854 RemoteDisplayState state) {
855 updateProvider(provider, state);
856 }
857
858 private void updateProvider(RemoteDisplayProviderProxy provider,
859 RemoteDisplayState state) {
860 int index = findProviderRecord(provider);
861 if (index >= 0) {
862 ProviderRecord providerRecord = mProviderRecords.get(index);
863 if (providerRecord.updateDescriptor(state)) {
864 checkGloballySelectedRouteState();
865 scheduleUpdateClientState();
866 }
867 }
868 }
869
870 /**
871 * This function is called whenever the state of the globally selected route
872 * may have changed. It checks the state and updates timeouts or unselects
873 * the route as appropriate.
874 */
875 private void checkGloballySelectedRouteState() {
876 // Unschedule timeouts when the route is unselected.
877 if (mGloballySelectedRouteRecord == null) {
Jeff Brown39ad0e52013-11-11 17:55:08 -0800878 mConnectionPhase = PHASE_NOT_AVAILABLE;
Jeff Brown69b07162013-11-07 00:30:16 -0800879 updateConnectionTimeout(0);
880 return;
881 }
882
883 // Ensure that the route is still present and enabled.
884 if (!mGloballySelectedRouteRecord.isValid()
885 || !mGloballySelectedRouteRecord.isEnabled()) {
886 updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
887 return;
888 }
889
Jeff Brown39ad0e52013-11-11 17:55:08 -0800890 // Make sure we haven't lost our connection.
891 final int oldPhase = mConnectionPhase;
892 mConnectionPhase = getConnectionPhase(mGloballySelectedRouteRecord.getStatus());
893 if (oldPhase >= PHASE_CONNECTING && mConnectionPhase < PHASE_CONNECTING) {
894 updateConnectionTimeout(TIMEOUT_REASON_CONNECTION_LOST);
895 return;
896 }
897
Jeff Brown69b07162013-11-07 00:30:16 -0800898 // Check the route status.
Jeff Brown39ad0e52013-11-11 17:55:08 -0800899 switch (mConnectionPhase) {
900 case PHASE_CONNECTED:
901 if (oldPhase != PHASE_CONNECTED) {
Jeff Brown69b07162013-11-07 00:30:16 -0800902 Slog.i(TAG, "Connected to global route: "
903 + mGloballySelectedRouteRecord);
904 }
905 updateConnectionTimeout(0);
906 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800907 case PHASE_CONNECTING:
908 if (oldPhase != PHASE_CONNECTING) {
Jeff Brown69b07162013-11-07 00:30:16 -0800909 Slog.i(TAG, "Connecting to global route: "
910 + mGloballySelectedRouteRecord);
911 }
912 updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED);
913 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800914 case PHASE_NOT_CONNECTED:
Jeff Brown69b07162013-11-07 00:30:16 -0800915 updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING);
916 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800917 case PHASE_NOT_AVAILABLE:
Jeff Brown69b07162013-11-07 00:30:16 -0800918 default:
919 updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
920 break;
921 }
922 }
923
924 private void updateConnectionTimeout(int reason) {
925 if (reason != mConnectionTimeoutReason) {
926 if (mConnectionTimeoutReason != 0) {
927 removeMessages(MSG_CONNECTION_TIMED_OUT);
928 }
929 mConnectionTimeoutReason = reason;
930 mConnectionTimeoutStartTime = SystemClock.uptimeMillis();
931 switch (reason) {
932 case TIMEOUT_REASON_NOT_AVAILABLE:
Jeff Brown39ad0e52013-11-11 17:55:08 -0800933 case TIMEOUT_REASON_CONNECTION_LOST:
934 // Route became unavailable or connection lost.
935 // Unselect it immediately.
Jeff Brown69b07162013-11-07 00:30:16 -0800936 sendEmptyMessage(MSG_CONNECTION_TIMED_OUT);
937 break;
938 case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
939 // Waiting for route to start connecting.
940 sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT);
941 break;
942 case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
943 // Waiting for route to complete connection.
944 sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT);
945 break;
946 }
947 }
948 }
949
950 private void connectionTimedOut() {
951 if (mConnectionTimeoutReason == 0 || mGloballySelectedRouteRecord == null) {
952 // Shouldn't get here. There must be a bug somewhere.
953 Log.wtf(TAG, "Handled connection timeout for no reason.");
954 return;
955 }
956
957 switch (mConnectionTimeoutReason) {
958 case TIMEOUT_REASON_NOT_AVAILABLE:
959 Slog.i(TAG, "Global route no longer available: "
960 + mGloballySelectedRouteRecord);
961 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800962 case TIMEOUT_REASON_CONNECTION_LOST:
963 Slog.i(TAG, "Global route connection lost: "
964 + mGloballySelectedRouteRecord);
965 break;
Jeff Brown69b07162013-11-07 00:30:16 -0800966 case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
967 Slog.i(TAG, "Global route timed out while waiting for "
968 + "connection attempt to begin after "
969 + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
970 + " ms: " + mGloballySelectedRouteRecord);
971 break;
972 case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
973 Slog.i(TAG, "Global route timed out while connecting after "
974 + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
975 + " ms: " + mGloballySelectedRouteRecord);
976 break;
977 }
978 mConnectionTimeoutReason = 0;
979
980 unselectGloballySelectedRoute();
981 }
982
983 private void scheduleUpdateClientState() {
984 if (!mClientStateUpdateScheduled) {
985 mClientStateUpdateScheduled = true;
986 sendEmptyMessage(MSG_UPDATE_CLIENT_STATE);
987 }
988 }
989
990 private void updateClientState() {
991 mClientStateUpdateScheduled = false;
992
Jeff Brownaf574182013-11-14 18:16:08 -0800993 final String globallySelectedRouteId = mGloballySelectedRouteRecord != null ?
Jeff Brown69b07162013-11-07 00:30:16 -0800994 mGloballySelectedRouteRecord.getUniqueId() : null;
Jeff Brownaf574182013-11-14 18:16:08 -0800995
996 // Build a new client state for trusted clients.
997 MediaRouterClientState trustedState = new MediaRouterClientState();
998 trustedState.globallySelectedRouteId = globallySelectedRouteId;
Jeff Brown69b07162013-11-07 00:30:16 -0800999 final int providerCount = mProviderRecords.size();
1000 for (int i = 0; i < providerCount; i++) {
Jeff Brownaf574182013-11-14 18:16:08 -08001001 mProviderRecords.get(i).appendClientState(trustedState);
1002 }
1003
1004 // Build a new client state for untrusted clients that can only see
1005 // the currently selected route.
1006 MediaRouterClientState untrustedState = new MediaRouterClientState();
1007 untrustedState.globallySelectedRouteId = globallySelectedRouteId;
1008 if (globallySelectedRouteId != null) {
1009 untrustedState.routes.add(trustedState.getRoute(globallySelectedRouteId));
Jeff Brown69b07162013-11-07 00:30:16 -08001010 }
1011
1012 try {
1013 synchronized (mService.mLock) {
1014 // Update the UserRecord.
Jeff Brownaf574182013-11-14 18:16:08 -08001015 mUserRecord.mTrustedState = trustedState;
1016 mUserRecord.mUntrustedState = untrustedState;
Jeff Brown69b07162013-11-07 00:30:16 -08001017
1018 // Collect all clients.
1019 final int count = mUserRecord.mClientRecords.size();
1020 for (int i = 0; i < count; i++) {
1021 mTempClients.add(mUserRecord.mClientRecords.get(i).mClient);
1022 }
1023 }
1024
1025 // Notify all clients (outside of the lock).
1026 final int count = mTempClients.size();
1027 for (int i = 0; i < count; i++) {
1028 try {
1029 mTempClients.get(i).onStateChanged();
1030 } catch (RemoteException ex) {
1031 // ignore errors, client probably died
1032 }
1033 }
1034 } finally {
1035 // Clear the list in preparation for the next time.
1036 mTempClients.clear();
1037 }
1038 }
1039
1040 private int findProviderRecord(RemoteDisplayProviderProxy provider) {
1041 final int count = mProviderRecords.size();
1042 for (int i = 0; i < count; i++) {
1043 ProviderRecord record = mProviderRecords.get(i);
1044 if (record.getProvider() == provider) {
1045 return i;
1046 }
1047 }
1048 return -1;
1049 }
1050
1051 private RouteRecord findRouteRecord(String uniqueId) {
1052 final int count = mProviderRecords.size();
1053 for (int i = 0; i < count; i++) {
1054 RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId);
1055 if (record != null) {
1056 return record;
1057 }
1058 }
1059 return null;
1060 }
1061
Jeff Brown39ad0e52013-11-11 17:55:08 -08001062 private static int getConnectionPhase(int status) {
1063 switch (status) {
1064 case MediaRouter.RouteInfo.STATUS_NONE:
1065 case MediaRouter.RouteInfo.STATUS_CONNECTED:
1066 return PHASE_CONNECTED;
1067 case MediaRouter.RouteInfo.STATUS_CONNECTING:
1068 return PHASE_CONNECTING;
1069 case MediaRouter.RouteInfo.STATUS_SCANNING:
1070 case MediaRouter.RouteInfo.STATUS_AVAILABLE:
1071 return PHASE_NOT_CONNECTED;
1072 case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE:
1073 case MediaRouter.RouteInfo.STATUS_IN_USE:
1074 default:
1075 return PHASE_NOT_AVAILABLE;
1076 }
1077 }
1078
Jeff Brown69b07162013-11-07 00:30:16 -08001079 static final class ProviderRecord {
1080 private final RemoteDisplayProviderProxy mProvider;
1081 private final String mUniquePrefix;
1082 private final ArrayList<RouteRecord> mRoutes = new ArrayList<RouteRecord>();
1083 private RemoteDisplayState mDescriptor;
1084
1085 public ProviderRecord(RemoteDisplayProviderProxy provider) {
1086 mProvider = provider;
1087 mUniquePrefix = provider.getFlattenedComponentName() + ":";
1088 }
1089
1090 public RemoteDisplayProviderProxy getProvider() {
1091 return mProvider;
1092 }
1093
1094 public String getUniquePrefix() {
1095 return mUniquePrefix;
1096 }
1097
1098 public boolean updateDescriptor(RemoteDisplayState descriptor) {
1099 boolean changed = false;
1100 if (mDescriptor != descriptor) {
1101 mDescriptor = descriptor;
1102
1103 // Update all existing routes and reorder them to match
1104 // the order of their descriptors.
1105 int targetIndex = 0;
1106 if (descriptor != null) {
1107 if (descriptor.isValid()) {
1108 final List<RemoteDisplayInfo> routeDescriptors = descriptor.displays;
1109 final int routeCount = routeDescriptors.size();
1110 for (int i = 0; i < routeCount; i++) {
1111 final RemoteDisplayInfo routeDescriptor =
1112 routeDescriptors.get(i);
1113 final String descriptorId = routeDescriptor.id;
1114 final int sourceIndex = findRouteByDescriptorId(descriptorId);
1115 if (sourceIndex < 0) {
1116 // Add the route to the provider.
1117 String uniqueId = assignRouteUniqueId(descriptorId);
1118 RouteRecord route =
1119 new RouteRecord(this, descriptorId, uniqueId);
1120 mRoutes.add(targetIndex++, route);
1121 route.updateDescriptor(routeDescriptor);
1122 changed = true;
1123 } else if (sourceIndex < targetIndex) {
1124 // Ignore route with duplicate id.
1125 Slog.w(TAG, "Ignoring route descriptor with duplicate id: "
1126 + routeDescriptor);
1127 } else {
1128 // Reorder existing route within the list.
1129 RouteRecord route = mRoutes.get(sourceIndex);
1130 Collections.swap(mRoutes, sourceIndex, targetIndex++);
1131 changed |= route.updateDescriptor(routeDescriptor);
1132 }
1133 }
1134 } else {
1135 Slog.w(TAG, "Ignoring invalid descriptor from media route provider: "
1136 + mProvider.getFlattenedComponentName());
1137 }
1138 }
1139
1140 // Dispose all remaining routes that do not have matching descriptors.
1141 for (int i = mRoutes.size() - 1; i >= targetIndex; i--) {
1142 RouteRecord route = mRoutes.remove(i);
1143 route.updateDescriptor(null); // mark route invalid
1144 changed = true;
1145 }
1146 }
1147 return changed;
1148 }
1149
1150 public void appendClientState(MediaRouterClientState state) {
1151 final int routeCount = mRoutes.size();
1152 for (int i = 0; i < routeCount; i++) {
1153 state.routes.add(mRoutes.get(i).getInfo());
1154 }
1155 }
1156
1157 public RouteRecord findRouteByUniqueId(String uniqueId) {
1158 final int routeCount = mRoutes.size();
1159 for (int i = 0; i < routeCount; i++) {
1160 RouteRecord route = mRoutes.get(i);
1161 if (route.getUniqueId().equals(uniqueId)) {
1162 return route;
1163 }
1164 }
1165 return null;
1166 }
1167
1168 private int findRouteByDescriptorId(String descriptorId) {
1169 final int routeCount = mRoutes.size();
1170 for (int i = 0; i < routeCount; i++) {
1171 RouteRecord route = mRoutes.get(i);
1172 if (route.getDescriptorId().equals(descriptorId)) {
1173 return i;
1174 }
1175 }
1176 return -1;
1177 }
1178
1179 public void dump(PrintWriter pw, String prefix) {
1180 pw.println(prefix + this);
1181
1182 final String indent = prefix + " ";
1183 mProvider.dump(pw, indent);
1184
1185 final int routeCount = mRoutes.size();
1186 if (routeCount != 0) {
1187 for (int i = 0; i < routeCount; i++) {
1188 mRoutes.get(i).dump(pw, indent);
1189 }
1190 } else {
1191 pw.println(indent + "<no routes>");
1192 }
1193 }
1194
1195 @Override
1196 public String toString() {
1197 return "Provider " + mProvider.getFlattenedComponentName();
1198 }
1199
1200 private String assignRouteUniqueId(String descriptorId) {
1201 return mUniquePrefix + descriptorId;
1202 }
1203 }
1204
1205 static final class RouteRecord {
1206 private final ProviderRecord mProviderRecord;
1207 private final String mDescriptorId;
1208 private final MediaRouterClientState.RouteInfo mMutableInfo;
1209 private MediaRouterClientState.RouteInfo mImmutableInfo;
1210 private RemoteDisplayInfo mDescriptor;
1211
1212 public RouteRecord(ProviderRecord providerRecord,
1213 String descriptorId, String uniqueId) {
1214 mProviderRecord = providerRecord;
1215 mDescriptorId = descriptorId;
1216 mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId);
1217 }
1218
1219 public RemoteDisplayProviderProxy getProvider() {
1220 return mProviderRecord.getProvider();
1221 }
1222
1223 public ProviderRecord getProviderRecord() {
1224 return mProviderRecord;
1225 }
1226
1227 public String getDescriptorId() {
1228 return mDescriptorId;
1229 }
1230
1231 public String getUniqueId() {
1232 return mMutableInfo.id;
1233 }
1234
1235 public MediaRouterClientState.RouteInfo getInfo() {
1236 if (mImmutableInfo == null) {
1237 mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo);
1238 }
1239 return mImmutableInfo;
1240 }
1241
1242 public boolean isValid() {
1243 return mDescriptor != null;
1244 }
1245
1246 public boolean isEnabled() {
1247 return mMutableInfo.enabled;
1248 }
1249
1250 public int getStatus() {
1251 return mMutableInfo.statusCode;
1252 }
1253
1254 public boolean updateDescriptor(RemoteDisplayInfo descriptor) {
1255 boolean changed = false;
1256 if (mDescriptor != descriptor) {
1257 mDescriptor = descriptor;
1258 if (descriptor != null) {
1259 final String name = computeName(descriptor);
Kenny Roote6585b32013-12-13 12:00:26 -08001260 if (!Objects.equals(mMutableInfo.name, name)) {
Jeff Brown69b07162013-11-07 00:30:16 -08001261 mMutableInfo.name = name;
1262 changed = true;
1263 }
1264 final String description = computeDescription(descriptor);
Kenny Roote6585b32013-12-13 12:00:26 -08001265 if (!Objects.equals(mMutableInfo.description, description)) {
Jeff Brown69b07162013-11-07 00:30:16 -08001266 mMutableInfo.description = description;
1267 changed = true;
1268 }
1269 final int supportedTypes = computeSupportedTypes(descriptor);
1270 if (mMutableInfo.supportedTypes != supportedTypes) {
1271 mMutableInfo.supportedTypes = supportedTypes;
1272 changed = true;
1273 }
1274 final boolean enabled = computeEnabled(descriptor);
1275 if (mMutableInfo.enabled != enabled) {
1276 mMutableInfo.enabled = enabled;
1277 changed = true;
1278 }
1279 final int statusCode = computeStatusCode(descriptor);
1280 if (mMutableInfo.statusCode != statusCode) {
1281 mMutableInfo.statusCode = statusCode;
1282 changed = true;
1283 }
1284 final int playbackType = computePlaybackType(descriptor);
1285 if (mMutableInfo.playbackType != playbackType) {
1286 mMutableInfo.playbackType = playbackType;
1287 changed = true;
1288 }
1289 final int playbackStream = computePlaybackStream(descriptor);
1290 if (mMutableInfo.playbackStream != playbackStream) {
1291 mMutableInfo.playbackStream = playbackStream;
1292 changed = true;
1293 }
1294 final int volume = computeVolume(descriptor);
1295 if (mMutableInfo.volume != volume) {
1296 mMutableInfo.volume = volume;
1297 changed = true;
1298 }
1299 final int volumeMax = computeVolumeMax(descriptor);
1300 if (mMutableInfo.volumeMax != volumeMax) {
1301 mMutableInfo.volumeMax = volumeMax;
1302 changed = true;
1303 }
1304 final int volumeHandling = computeVolumeHandling(descriptor);
1305 if (mMutableInfo.volumeHandling != volumeHandling) {
1306 mMutableInfo.volumeHandling = volumeHandling;
1307 changed = true;
1308 }
1309 final int presentationDisplayId = computePresentationDisplayId(descriptor);
1310 if (mMutableInfo.presentationDisplayId != presentationDisplayId) {
1311 mMutableInfo.presentationDisplayId = presentationDisplayId;
1312 changed = true;
1313 }
1314 }
1315 }
1316 if (changed) {
1317 mImmutableInfo = null;
1318 }
1319 return changed;
1320 }
1321
1322 public void dump(PrintWriter pw, String prefix) {
1323 pw.println(prefix + this);
1324
1325 final String indent = prefix + " ";
1326 pw.println(indent + "mMutableInfo=" + mMutableInfo);
1327 pw.println(indent + "mDescriptorId=" + mDescriptorId);
1328 pw.println(indent + "mDescriptor=" + mDescriptor);
1329 }
1330
1331 @Override
1332 public String toString() {
1333 return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")";
1334 }
1335
1336 private static String computeName(RemoteDisplayInfo descriptor) {
1337 // Note that isValid() already ensures the name is non-empty.
1338 return descriptor.name;
1339 }
1340
1341 private static String computeDescription(RemoteDisplayInfo descriptor) {
1342 final String description = descriptor.description;
1343 return TextUtils.isEmpty(description) ? null : description;
1344 }
1345
1346 private static int computeSupportedTypes(RemoteDisplayInfo descriptor) {
1347 return MediaRouter.ROUTE_TYPE_LIVE_AUDIO
1348 | MediaRouter.ROUTE_TYPE_LIVE_VIDEO
1349 | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
1350 }
1351
1352 private static boolean computeEnabled(RemoteDisplayInfo descriptor) {
1353 switch (descriptor.status) {
1354 case RemoteDisplayInfo.STATUS_CONNECTED:
1355 case RemoteDisplayInfo.STATUS_CONNECTING:
1356 case RemoteDisplayInfo.STATUS_AVAILABLE:
1357 return true;
1358 default:
1359 return false;
1360 }
1361 }
1362
1363 private static int computeStatusCode(RemoteDisplayInfo descriptor) {
1364 switch (descriptor.status) {
1365 case RemoteDisplayInfo.STATUS_NOT_AVAILABLE:
1366 return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE;
1367 case RemoteDisplayInfo.STATUS_AVAILABLE:
1368 return MediaRouter.RouteInfo.STATUS_AVAILABLE;
1369 case RemoteDisplayInfo.STATUS_IN_USE:
1370 return MediaRouter.RouteInfo.STATUS_IN_USE;
1371 case RemoteDisplayInfo.STATUS_CONNECTING:
1372 return MediaRouter.RouteInfo.STATUS_CONNECTING;
1373 case RemoteDisplayInfo.STATUS_CONNECTED:
1374 return MediaRouter.RouteInfo.STATUS_CONNECTED;
1375 default:
1376 return MediaRouter.RouteInfo.STATUS_NONE;
1377 }
1378 }
1379
1380 private static int computePlaybackType(RemoteDisplayInfo descriptor) {
1381 return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
1382 }
1383
1384 private static int computePlaybackStream(RemoteDisplayInfo descriptor) {
1385 return AudioSystem.STREAM_MUSIC;
1386 }
1387
1388 private static int computeVolume(RemoteDisplayInfo descriptor) {
1389 final int volume = descriptor.volume;
1390 final int volumeMax = descriptor.volumeMax;
1391 if (volume < 0) {
1392 return 0;
1393 } else if (volume > volumeMax) {
1394 return volumeMax;
1395 }
1396 return volume;
1397 }
1398
1399 private static int computeVolumeMax(RemoteDisplayInfo descriptor) {
1400 final int volumeMax = descriptor.volumeMax;
1401 return volumeMax > 0 ? volumeMax : 0;
1402 }
1403
1404 private static int computeVolumeHandling(RemoteDisplayInfo descriptor) {
1405 final int volumeHandling = descriptor.volumeHandling;
1406 switch (volumeHandling) {
1407 case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE:
1408 return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
1409 case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED:
1410 default:
1411 return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
1412 }
1413 }
1414
1415 private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) {
1416 // The MediaRouter class validates that the id corresponds to an extant
1417 // presentation display. So all we do here is canonicalize the null case.
1418 final int displayId = descriptor.presentationDisplayId;
1419 return displayId < 0 ? -1 : displayId;
1420 }
1421 }
1422 }
1423}