MediaRouterService.java revision 69b07161bebdb2c726e3a826c2268866f1a94517
1/*
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
19import com.android.internal.util.Objects;
20import com.android.server.Watchdog;
21
22import android.Manifest;
23import android.app.ActivityManager;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.pm.PackageManager;
29import android.media.AudioSystem;
30import android.media.IMediaRouterClient;
31import android.media.IMediaRouterService;
32import android.media.MediaRouter;
33import android.media.MediaRouterClientState;
34import android.media.RemoteDisplayState;
35import android.media.RemoteDisplayState.RemoteDisplayInfo;
36import android.os.Binder;
37import android.os.Handler;
38import android.os.IBinder;
39import android.os.Looper;
40import android.os.Message;
41import android.os.RemoteException;
42import android.os.SystemClock;
43import android.text.TextUtils;
44import android.util.ArrayMap;
45import android.util.Log;
46import android.util.Slog;
47import android.util.SparseArray;
48import android.util.TimeUtils;
49
50import java.io.FileDescriptor;
51import java.io.PrintWriter;
52import java.util.ArrayList;
53import java.util.Collections;
54import java.util.List;
55
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);
131        final long token = Binder.clearCallingIdentity();
132        try {
133            synchronized (mLock) {
134                registerClientLocked(client, pid, packageName, resolvedUserId);
135            }
136        } finally {
137            Binder.restoreCallingIdentity(token);
138        }
139    }
140
141    // Binder call
142    @Override
143    public void unregisterClient(IMediaRouterClient client) {
144        if (client == null) {
145            throw new IllegalArgumentException("client must not be null");
146        }
147
148        final long token = Binder.clearCallingIdentity();
149        try {
150            synchronized (mLock) {
151                unregisterClientLocked(client, false);
152            }
153        } finally {
154            Binder.restoreCallingIdentity(token);
155        }
156    }
157
158    // Binder call
159    @Override
160    public MediaRouterClientState getState(IMediaRouterClient client) {
161        if (client == null) {
162            throw new IllegalArgumentException("client must not be null");
163        }
164
165        final long token = Binder.clearCallingIdentity();
166        try {
167            synchronized (mLock) {
168                return getStateLocked(client);
169            }
170        } finally {
171            Binder.restoreCallingIdentity(token);
172        }
173    }
174
175    // Binder call
176    @Override
177    public void setDiscoveryRequest(IMediaRouterClient client,
178            int routeTypes, boolean activeScan) {
179        if (client == null) {
180            throw new IllegalArgumentException("client must not be null");
181        }
182
183        final long token = Binder.clearCallingIdentity();
184        try {
185            synchronized (mLock) {
186                setDiscoveryRequestLocked(client, routeTypes, activeScan);
187            }
188        } finally {
189            Binder.restoreCallingIdentity(token);
190        }
191    }
192
193    // Binder call
194    // A null routeId means that the client wants to unselect its current route.
195    // The explicit flag indicates whether the change was explicitly requested by the
196    // user or the application which may cause changes to propagate out to the rest
197    // of the system.  Should be false when the change is in response to a new globally
198    // selected route or a default selection.
199    @Override
200    public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) {
201        if (client == null) {
202            throw new IllegalArgumentException("client must not be null");
203        }
204
205        final long token = Binder.clearCallingIdentity();
206        try {
207            synchronized (mLock) {
208                setSelectedRouteLocked(client, routeId, explicit);
209            }
210        } finally {
211            Binder.restoreCallingIdentity(token);
212        }
213    }
214
215    // Binder call
216    @Override
217    public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) {
218        if (client == null) {
219            throw new IllegalArgumentException("client must not be null");
220        }
221        if (routeId == null) {
222            throw new IllegalArgumentException("routeId must not be null");
223        }
224
225        final long token = Binder.clearCallingIdentity();
226        try {
227            synchronized (mLock) {
228                requestSetVolumeLocked(client, routeId, volume);
229            }
230        } finally {
231            Binder.restoreCallingIdentity(token);
232        }
233    }
234
235    // Binder call
236    @Override
237    public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) {
238        if (client == null) {
239            throw new IllegalArgumentException("client must not be null");
240        }
241        if (routeId == null) {
242            throw new IllegalArgumentException("routeId must not be null");
243        }
244
245        final long token = Binder.clearCallingIdentity();
246        try {
247            synchronized (mLock) {
248                requestUpdateVolumeLocked(client, routeId, direction);
249            }
250        } finally {
251            Binder.restoreCallingIdentity(token);
252        }
253    }
254
255    // Binder call
256    @Override
257    public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
258        if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
259                != PackageManager.PERMISSION_GRANTED) {
260            pw.println("Permission Denial: can't dump MediaRouterService from from pid="
261                    + Binder.getCallingPid()
262                    + ", uid=" + Binder.getCallingUid());
263            return;
264        }
265
266        pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)");
267        pw.println();
268        pw.println("Global state");
269        pw.println("  mCurrentUserId=" + mCurrentUserId);
270
271        synchronized (mLock) {
272            final int count = mUserRecords.size();
273            for (int i = 0; i < count; i++) {
274                UserRecord userRecord = mUserRecords.valueAt(i);
275                pw.println();
276                userRecord.dump(pw, "");
277            }
278        }
279    }
280
281    void switchUser() {
282        synchronized (mLock) {
283            int userId = ActivityManager.getCurrentUser();
284            if (mCurrentUserId != userId) {
285                final int oldUserId = mCurrentUserId;
286                mCurrentUserId = userId; // do this first
287
288                UserRecord oldUser = mUserRecords.get(oldUserId);
289                if (oldUser != null) {
290                    oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP);
291                    disposeUserIfNeededLocked(oldUser); // since no longer current user
292                }
293
294                UserRecord newUser = mUserRecords.get(userId);
295                if (newUser != null) {
296                    newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START);
297                }
298            }
299        }
300    }
301
302    void clientDied(ClientRecord clientRecord) {
303        synchronized (mLock) {
304            unregisterClientLocked(clientRecord.mClient, true);
305        }
306    }
307
308    private void registerClientLocked(IMediaRouterClient client,
309            int pid, String packageName, int userId) {
310        final IBinder binder = client.asBinder();
311        ClientRecord clientRecord = mAllClientRecords.get(binder);
312        if (clientRecord == null) {
313            boolean newUser = false;
314            UserRecord userRecord = mUserRecords.get(userId);
315            if (userRecord == null) {
316                userRecord = new UserRecord(userId);
317                newUser = true;
318            }
319            clientRecord = new ClientRecord(userRecord, client, pid, packageName);
320            try {
321                binder.linkToDeath(clientRecord, 0);
322            } catch (RemoteException ex) {
323                throw new RuntimeException("Media router client died prematurely.", ex);
324            }
325
326            if (newUser) {
327                mUserRecords.put(userId, userRecord);
328                initializeUserLocked(userRecord);
329            }
330
331            userRecord.mClientRecords.add(clientRecord);
332            mAllClientRecords.put(binder, clientRecord);
333            initializeClientLocked(clientRecord);
334        }
335    }
336
337    private void unregisterClientLocked(IMediaRouterClient client, boolean died) {
338        ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder());
339        if (clientRecord != null) {
340            UserRecord userRecord = clientRecord.mUserRecord;
341            userRecord.mClientRecords.remove(clientRecord);
342            disposeClientLocked(clientRecord, died);
343            disposeUserIfNeededLocked(userRecord); // since client removed from user
344        }
345    }
346
347    private MediaRouterClientState getStateLocked(IMediaRouterClient client) {
348        ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
349        if (clientRecord != null) {
350            return clientRecord.mUserRecord.mState;
351        }
352        return null;
353    }
354
355    private void setDiscoveryRequestLocked(IMediaRouterClient client,
356            int routeTypes, boolean activeScan) {
357        final IBinder binder = client.asBinder();
358        ClientRecord clientRecord = mAllClientRecords.get(binder);
359        if (clientRecord != null) {
360            if (clientRecord.mRouteTypes != routeTypes
361                    || clientRecord.mActiveScan != activeScan) {
362                if (DEBUG) {
363                    Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x"
364                            + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan);
365                }
366                clientRecord.mRouteTypes = routeTypes;
367                clientRecord.mActiveScan = activeScan;
368                clientRecord.mUserRecord.mHandler.sendEmptyMessage(
369                        UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
370            }
371        }
372    }
373
374    private void setSelectedRouteLocked(IMediaRouterClient client,
375            String routeId, boolean explicit) {
376        ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
377        if (clientRecord != null) {
378            final String oldRouteId = clientRecord.mSelectedRouteId;
379            if (!Objects.equal(routeId, oldRouteId)) {
380                if (DEBUG) {
381                    Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId
382                            + ", oldRouteId=" + oldRouteId
383                            + ", explicit=" + explicit);
384                }
385
386                clientRecord.mSelectedRouteId = routeId;
387                if (explicit) {
388                    if (oldRouteId != null) {
389                        clientRecord.mUserRecord.mHandler.obtainMessage(
390                                UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget();
391                    }
392                    if (routeId != null) {
393                        clientRecord.mUserRecord.mHandler.obtainMessage(
394                                UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
395                    }
396                }
397            }
398        }
399    }
400
401    private void requestSetVolumeLocked(IMediaRouterClient client,
402            String routeId, int volume) {
403        final IBinder binder = client.asBinder();
404        ClientRecord clientRecord = mAllClientRecords.get(binder);
405        if (clientRecord != null) {
406            clientRecord.mUserRecord.mHandler.obtainMessage(
407                    UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget();
408        }
409    }
410
411    private void requestUpdateVolumeLocked(IMediaRouterClient client,
412            String routeId, int direction) {
413        final IBinder binder = client.asBinder();
414        ClientRecord clientRecord = mAllClientRecords.get(binder);
415        if (clientRecord != null) {
416            clientRecord.mUserRecord.mHandler.obtainMessage(
417                    UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget();
418        }
419    }
420
421    private void initializeUserLocked(UserRecord userRecord) {
422        if (DEBUG) {
423            Slog.d(TAG, userRecord + ": Initialized");
424        }
425        if (userRecord.mUserId == mCurrentUserId) {
426            userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START);
427        }
428    }
429
430    private void disposeUserIfNeededLocked(UserRecord userRecord) {
431        // If there are no records left and the user is no longer current then go ahead
432        // and purge the user record and all of its associated state.  If the user is current
433        // then leave it alone since we might be connected to a route or want to query
434        // the same route information again soon.
435        if (userRecord.mUserId != mCurrentUserId
436                && userRecord.mClientRecords.isEmpty()) {
437            if (DEBUG) {
438                Slog.d(TAG, userRecord + ": Disposed");
439            }
440            mUserRecords.remove(userRecord.mUserId);
441            // Note: User already stopped (by switchUser) so no need to send stop message here.
442        }
443    }
444
445    private void initializeClientLocked(ClientRecord clientRecord) {
446        if (DEBUG) {
447            Slog.d(TAG, clientRecord + ": Registered");
448        }
449    }
450
451    private void disposeClientLocked(ClientRecord clientRecord, boolean died) {
452        if (DEBUG) {
453            if (died) {
454                Slog.d(TAG, clientRecord + ": Died!");
455            } else {
456                Slog.d(TAG, clientRecord + ": Unregistered");
457            }
458        }
459        if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) {
460            clientRecord.mUserRecord.mHandler.sendEmptyMessage(
461                    UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
462        }
463        clientRecord.dispose();
464    }
465
466    private boolean validatePackageName(int uid, String packageName) {
467        if (packageName != null) {
468            String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
469            if (packageNames != null) {
470                for (String n : packageNames) {
471                    if (n.equals(packageName)) {
472                        return true;
473                    }
474                }
475            }
476        }
477        return false;
478    }
479
480    /**
481     * Information about a particular client of the media router.
482     * The contents of this object is guarded by mLock.
483     */
484    final class ClientRecord implements DeathRecipient {
485        public final UserRecord mUserRecord;
486        public final IMediaRouterClient mClient;
487        public final int mPid;
488        public final String mPackageName;
489
490        public int mRouteTypes;
491        public boolean mActiveScan;
492        public String mSelectedRouteId;
493
494        public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
495                int pid, String packageName) {
496            mUserRecord = userRecord;
497            mClient = client;
498            mPid = pid;
499            mPackageName = packageName;
500        }
501
502        public void dispose() {
503            mClient.asBinder().unlinkToDeath(this, 0);
504        }
505
506        @Override
507        public void binderDied() {
508            clientDied(this);
509        }
510
511        public void dump(PrintWriter pw, String prefix) {
512            pw.println(prefix + this);
513
514            final String indent = prefix + "  ";
515            pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes));
516            pw.println(indent + "mActiveScan=" + mActiveScan);
517            pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId);
518        }
519
520        @Override
521        public String toString() {
522            return "Client " + mPackageName + " (pid " + mPid + ")";
523        }
524    }
525
526    /**
527     * Information about a particular user.
528     * The contents of this object is guarded by mLock.
529     */
530    final class UserRecord {
531        public final int mUserId;
532        public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
533        public final UserHandler mHandler;
534        public MediaRouterClientState mState;
535
536        public UserRecord(int userId) {
537            mUserId = userId;
538            mHandler = new UserHandler(MediaRouterService.this, this);
539        }
540
541        public void dump(final PrintWriter pw, String prefix) {
542            pw.println(prefix + this);
543
544            final String indent = prefix + "  ";
545            final int clientCount = mClientRecords.size();
546            if (clientCount != 0) {
547                for (int i = 0; i < clientCount; i++) {
548                    mClientRecords.get(i).dump(pw, indent);
549                }
550            } else {
551                pw.println(indent + "<no clients>");
552            }
553
554            if (!mHandler.runWithScissors(new Runnable() {
555                @Override
556                public void run() {
557                    mHandler.dump(pw, indent);
558                }
559            }, 1000)) {
560                pw.println(indent + "<could not dump handler state>");
561            }
562         }
563
564        @Override
565        public String toString() {
566            return "User " + mUserId;
567        }
568    }
569
570    /**
571     * Media router handler
572     * <p>
573     * Since remote display providers are designed to be single-threaded by nature,
574     * this class encapsulates all of the associated functionality and exports state
575     * to the service as it evolves.
576     * </p><p>
577     * One important task of this class is to keep track of the current globally selected
578     * route id for certain routes that have global effects, such as remote displays.
579     * Global route selections override local selections made within apps.  The change
580     * is propagated to all apps so that they are all in sync.  Synchronization works
581     * both ways.  Whenever the globally selected route is explicitly unselected by any
582     * app, then it becomes unselected globally and all apps are informed.
583     * </p><p>
584     * This class is currently hardcoded to work with remote display providers but
585     * it is intended to be eventually extended to support more general route providers
586     * similar to the support library media router.
587     * </p>
588     */
589    static final class UserHandler extends Handler
590            implements RemoteDisplayProviderWatcher.Callback,
591            RemoteDisplayProviderProxy.Callback {
592        public static final int MSG_START = 1;
593        public static final int MSG_STOP = 2;
594        public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3;
595        public static final int MSG_SELECT_ROUTE = 4;
596        public static final int MSG_UNSELECT_ROUTE = 5;
597        public static final int MSG_REQUEST_SET_VOLUME = 6;
598        public static final int MSG_REQUEST_UPDATE_VOLUME = 7;
599        private static final int MSG_UPDATE_CLIENT_STATE = 8;
600        private static final int MSG_CONNECTION_TIMED_OUT = 9;
601
602        private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
603        private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 2;
604        private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 3;
605
606        private final MediaRouterService mService;
607        private final UserRecord mUserRecord;
608        private final RemoteDisplayProviderWatcher mWatcher;
609        private final ArrayList<ProviderRecord> mProviderRecords =
610                new ArrayList<ProviderRecord>();
611        private final ArrayList<IMediaRouterClient> mTempClients =
612                new ArrayList<IMediaRouterClient>();
613
614        private boolean mRunning;
615        private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
616        private RouteRecord mGloballySelectedRouteRecord;
617        private int mConnectionTimeoutReason;
618        private long mConnectionTimeoutStartTime;
619        private boolean mClientStateUpdateScheduled;
620
621        public UserHandler(MediaRouterService service, UserRecord userRecord) {
622            super(Looper.getMainLooper(), null, true);
623            mService = service;
624            mUserRecord = userRecord;
625            mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this,
626                    this, mUserRecord.mUserId);
627        }
628
629        @Override
630        public void handleMessage(Message msg) {
631            switch (msg.what) {
632                case MSG_START: {
633                    start();
634                    break;
635                }
636                case MSG_STOP: {
637                    stop();
638                    break;
639                }
640                case MSG_UPDATE_DISCOVERY_REQUEST: {
641                    updateDiscoveryRequest();
642                    break;
643                }
644                case MSG_SELECT_ROUTE: {
645                    selectRoute((String)msg.obj);
646                    break;
647                }
648                case MSG_UNSELECT_ROUTE: {
649                    unselectRoute((String)msg.obj);
650                    break;
651                }
652                case MSG_REQUEST_SET_VOLUME: {
653                    requestSetVolume((String)msg.obj, msg.arg1);
654                    break;
655                }
656                case MSG_REQUEST_UPDATE_VOLUME: {
657                    requestUpdateVolume((String)msg.obj, msg.arg1);
658                    break;
659                }
660                case MSG_UPDATE_CLIENT_STATE: {
661                    updateClientState();
662                    break;
663                }
664                case MSG_CONNECTION_TIMED_OUT: {
665                    connectionTimedOut();
666                    break;
667                }
668            }
669        }
670
671        public void dump(PrintWriter pw, String prefix) {
672            pw.println(prefix + "Handler");
673
674            final String indent = prefix + "  ";
675            pw.println(indent + "mRunning=" + mRunning);
676            pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode);
677            pw.println(indent + "mGloballySelectedRouteRecord=" + mGloballySelectedRouteRecord);
678            pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason);
679            pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ?
680                    TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "<n/a>"));
681
682            mWatcher.dump(pw, prefix);
683
684            final int providerCount = mProviderRecords.size();
685            if (providerCount != 0) {
686                for (int i = 0; i < providerCount; i++) {
687                    mProviderRecords.get(i).dump(pw, prefix);
688                }
689            } else {
690                pw.println(indent + "<no providers>");
691            }
692        }
693
694        private void start() {
695            if (!mRunning) {
696                mRunning = true;
697                mWatcher.start(); // also starts all providers
698            }
699        }
700
701        private void stop() {
702            if (mRunning) {
703                mRunning = false;
704                unselectGloballySelectedRoute();
705                mWatcher.stop(); // also stops all providers
706            }
707        }
708
709        private void updateDiscoveryRequest() {
710            int routeTypes = 0;
711            boolean activeScan = false;
712            synchronized (mService.mLock) {
713                final int count = mUserRecord.mClientRecords.size();
714                for (int i = 0; i < count; i++) {
715                    ClientRecord clientRecord = mUserRecord.mClientRecords.get(i);
716                    routeTypes |= clientRecord.mRouteTypes;
717                    activeScan |= clientRecord.mActiveScan;
718                }
719            }
720
721            final int newDiscoveryMode;
722            if ((routeTypes & (MediaRouter.ROUTE_TYPE_LIVE_VIDEO
723                    | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) != 0) {
724                if (activeScan) {
725                    newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
726                } else {
727                    newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;
728                }
729            } else {
730                newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
731            }
732
733            if (mDiscoveryMode != newDiscoveryMode) {
734                mDiscoveryMode = newDiscoveryMode;
735                final int count = mProviderRecords.size();
736                for (int i = 0; i < count; i++) {
737                    mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode);
738                }
739            }
740        }
741
742        private void selectRoute(String routeId) {
743            if (routeId != null
744                    && (mGloballySelectedRouteRecord == null
745                            || !routeId.equals(mGloballySelectedRouteRecord.getUniqueId()))) {
746                RouteRecord routeRecord = findRouteRecord(routeId);
747                if (routeRecord != null) {
748                    unselectGloballySelectedRoute();
749
750                    Slog.i(TAG, "Selected global route:" + routeRecord);
751                    mGloballySelectedRouteRecord = routeRecord;
752                    checkGloballySelectedRouteState();
753                    routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId());
754
755                    scheduleUpdateClientState();
756                }
757            }
758        }
759
760        private void unselectRoute(String routeId) {
761            if (routeId != null
762                    && mGloballySelectedRouteRecord != null
763                    && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
764                unselectGloballySelectedRoute();
765            }
766        }
767
768        private void unselectGloballySelectedRoute() {
769            if (mGloballySelectedRouteRecord != null) {
770                Slog.i(TAG, "Unselected global route:" + mGloballySelectedRouteRecord);
771                mGloballySelectedRouteRecord.getProvider().setSelectedDisplay(null);
772                mGloballySelectedRouteRecord = null;
773                checkGloballySelectedRouteState();
774
775                scheduleUpdateClientState();
776            }
777        }
778
779        private void requestSetVolume(String routeId, int volume) {
780            if (mGloballySelectedRouteRecord != null
781                    && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
782                mGloballySelectedRouteRecord.getProvider().setDisplayVolume(volume);
783            }
784        }
785
786        private void requestUpdateVolume(String routeId, int direction) {
787            if (mGloballySelectedRouteRecord != null
788                    && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
789                mGloballySelectedRouteRecord.getProvider().adjustDisplayVolume(direction);
790            }
791        }
792
793        @Override
794        public void addProvider(RemoteDisplayProviderProxy provider) {
795            provider.setCallback(this);
796            provider.setDiscoveryMode(mDiscoveryMode);
797            provider.setSelectedDisplay(null); // just to be safe
798
799            ProviderRecord providerRecord = new ProviderRecord(provider);
800            mProviderRecords.add(providerRecord);
801            providerRecord.updateDescriptor(provider.getDisplayState());
802
803            scheduleUpdateClientState();
804        }
805
806        @Override
807        public void removeProvider(RemoteDisplayProviderProxy provider) {
808            int index = findProviderRecord(provider);
809            if (index >= 0) {
810                ProviderRecord providerRecord = mProviderRecords.remove(index);
811                providerRecord.updateDescriptor(null); // mark routes invalid
812                provider.setCallback(null);
813                provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE);
814
815                checkGloballySelectedRouteState();
816                scheduleUpdateClientState();
817            }
818        }
819
820        @Override
821        public void onDisplayStateChanged(RemoteDisplayProviderProxy provider,
822                RemoteDisplayState state) {
823            updateProvider(provider, state);
824        }
825
826        private void updateProvider(RemoteDisplayProviderProxy provider,
827                RemoteDisplayState state) {
828            int index = findProviderRecord(provider);
829            if (index >= 0) {
830                ProviderRecord providerRecord = mProviderRecords.get(index);
831                if (providerRecord.updateDescriptor(state)) {
832                    checkGloballySelectedRouteState();
833                    scheduleUpdateClientState();
834                }
835            }
836        }
837
838        /**
839         * This function is called whenever the state of the globally selected route
840         * may have changed.  It checks the state and updates timeouts or unselects
841         * the route as appropriate.
842         */
843        private void checkGloballySelectedRouteState() {
844            // Unschedule timeouts when the route is unselected.
845            if (mGloballySelectedRouteRecord == null) {
846                updateConnectionTimeout(0);
847                return;
848            }
849
850            // Ensure that the route is still present and enabled.
851            if (!mGloballySelectedRouteRecord.isValid()
852                    || !mGloballySelectedRouteRecord.isEnabled()) {
853                updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
854                return;
855            }
856
857            // Check the route status.
858            switch (mGloballySelectedRouteRecord.getStatus()) {
859                case MediaRouter.RouteInfo.STATUS_NONE:
860                case MediaRouter.RouteInfo.STATUS_CONNECTED:
861                    if (mConnectionTimeoutReason != 0) {
862                        Slog.i(TAG, "Connected to global route: "
863                                + mGloballySelectedRouteRecord);
864                    }
865                    updateConnectionTimeout(0);
866                    break;
867                case MediaRouter.RouteInfo.STATUS_CONNECTING:
868                    if (mConnectionTimeoutReason != 0) {
869                        Slog.i(TAG, "Connecting to global route: "
870                                + mGloballySelectedRouteRecord);
871                    }
872                    updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED);
873                    break;
874                case MediaRouter.RouteInfo.STATUS_SCANNING:
875                case MediaRouter.RouteInfo.STATUS_AVAILABLE:
876                    updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING);
877                    break;
878                case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE:
879                case MediaRouter.RouteInfo.STATUS_IN_USE:
880                default:
881                    updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
882                    break;
883            }
884        }
885
886        private void updateConnectionTimeout(int reason) {
887            if (reason != mConnectionTimeoutReason) {
888                if (mConnectionTimeoutReason != 0) {
889                    removeMessages(MSG_CONNECTION_TIMED_OUT);
890                }
891                mConnectionTimeoutReason = reason;
892                mConnectionTimeoutStartTime = SystemClock.uptimeMillis();
893                switch (reason) {
894                    case TIMEOUT_REASON_NOT_AVAILABLE:
895                        // Route became unavailable.  Unselect it immediately.
896                        sendEmptyMessage(MSG_CONNECTION_TIMED_OUT);
897                        break;
898                    case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
899                        // Waiting for route to start connecting.
900                        sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT);
901                        break;
902                    case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
903                        // Waiting for route to complete connection.
904                        sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT);
905                        break;
906                }
907            }
908        }
909
910        private void connectionTimedOut() {
911            if (mConnectionTimeoutReason == 0 || mGloballySelectedRouteRecord == null) {
912                // Shouldn't get here.  There must be a bug somewhere.
913                Log.wtf(TAG, "Handled connection timeout for no reason.");
914                return;
915            }
916
917            switch (mConnectionTimeoutReason) {
918                case TIMEOUT_REASON_NOT_AVAILABLE:
919                    Slog.i(TAG, "Global route no longer available: "
920                            + mGloballySelectedRouteRecord);
921                    break;
922                case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
923                    Slog.i(TAG, "Global route timed out while waiting for "
924                            + "connection attempt to begin after "
925                            + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
926                            + " ms: " + mGloballySelectedRouteRecord);
927                    break;
928                case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
929                    Slog.i(TAG, "Global route timed out while connecting after "
930                            + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
931                            + " ms: " + mGloballySelectedRouteRecord);
932                    break;
933            }
934            mConnectionTimeoutReason = 0;
935
936            unselectGloballySelectedRoute();
937        }
938
939        private void scheduleUpdateClientState() {
940            if (!mClientStateUpdateScheduled) {
941                mClientStateUpdateScheduled = true;
942                sendEmptyMessage(MSG_UPDATE_CLIENT_STATE);
943            }
944        }
945
946        private void updateClientState() {
947            mClientStateUpdateScheduled = false;
948
949            // Build a new client state.
950            MediaRouterClientState state = new MediaRouterClientState();
951            state.globallySelectedRouteId = mGloballySelectedRouteRecord != null ?
952                    mGloballySelectedRouteRecord.getUniqueId() : null;
953            final int providerCount = mProviderRecords.size();
954            for (int i = 0; i < providerCount; i++) {
955                mProviderRecords.get(i).appendClientState(state);
956            }
957
958            try {
959                synchronized (mService.mLock) {
960                    // Update the UserRecord.
961                    mUserRecord.mState = state;
962
963                    // Collect all clients.
964                    final int count = mUserRecord.mClientRecords.size();
965                    for (int i = 0; i < count; i++) {
966                        mTempClients.add(mUserRecord.mClientRecords.get(i).mClient);
967                    }
968                }
969
970                // Notify all clients (outside of the lock).
971                final int count = mTempClients.size();
972                for (int i = 0; i < count; i++) {
973                    try {
974                        mTempClients.get(i).onStateChanged();
975                    } catch (RemoteException ex) {
976                        // ignore errors, client probably died
977                    }
978                }
979            } finally {
980                // Clear the list in preparation for the next time.
981                mTempClients.clear();
982            }
983        }
984
985        private int findProviderRecord(RemoteDisplayProviderProxy provider) {
986            final int count = mProviderRecords.size();
987            for (int i = 0; i < count; i++) {
988                ProviderRecord record = mProviderRecords.get(i);
989                if (record.getProvider() == provider) {
990                    return i;
991                }
992            }
993            return -1;
994        }
995
996        private RouteRecord findRouteRecord(String uniqueId) {
997            final int count = mProviderRecords.size();
998            for (int i = 0; i < count; i++) {
999                RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId);
1000                if (record != null) {
1001                    return record;
1002                }
1003            }
1004            return null;
1005        }
1006
1007        static final class ProviderRecord {
1008            private final RemoteDisplayProviderProxy mProvider;
1009            private final String mUniquePrefix;
1010            private final ArrayList<RouteRecord> mRoutes = new ArrayList<RouteRecord>();
1011            private RemoteDisplayState mDescriptor;
1012
1013            public ProviderRecord(RemoteDisplayProviderProxy provider) {
1014                mProvider = provider;
1015                mUniquePrefix = provider.getFlattenedComponentName() + ":";
1016            }
1017
1018            public RemoteDisplayProviderProxy getProvider() {
1019                return mProvider;
1020            }
1021
1022            public String getUniquePrefix() {
1023                return mUniquePrefix;
1024            }
1025
1026            public boolean updateDescriptor(RemoteDisplayState descriptor) {
1027                boolean changed = false;
1028                if (mDescriptor != descriptor) {
1029                    mDescriptor = descriptor;
1030
1031                    // Update all existing routes and reorder them to match
1032                    // the order of their descriptors.
1033                    int targetIndex = 0;
1034                    if (descriptor != null) {
1035                        if (descriptor.isValid()) {
1036                            final List<RemoteDisplayInfo> routeDescriptors = descriptor.displays;
1037                            final int routeCount = routeDescriptors.size();
1038                            for (int i = 0; i < routeCount; i++) {
1039                                final RemoteDisplayInfo routeDescriptor =
1040                                        routeDescriptors.get(i);
1041                                final String descriptorId = routeDescriptor.id;
1042                                final int sourceIndex = findRouteByDescriptorId(descriptorId);
1043                                if (sourceIndex < 0) {
1044                                    // Add the route to the provider.
1045                                    String uniqueId = assignRouteUniqueId(descriptorId);
1046                                    RouteRecord route =
1047                                            new RouteRecord(this, descriptorId, uniqueId);
1048                                    mRoutes.add(targetIndex++, route);
1049                                    route.updateDescriptor(routeDescriptor);
1050                                    changed = true;
1051                                } else if (sourceIndex < targetIndex) {
1052                                    // Ignore route with duplicate id.
1053                                    Slog.w(TAG, "Ignoring route descriptor with duplicate id: "
1054                                            + routeDescriptor);
1055                                } else {
1056                                    // Reorder existing route within the list.
1057                                    RouteRecord route = mRoutes.get(sourceIndex);
1058                                    Collections.swap(mRoutes, sourceIndex, targetIndex++);
1059                                    changed |= route.updateDescriptor(routeDescriptor);
1060                                }
1061                            }
1062                        } else {
1063                            Slog.w(TAG, "Ignoring invalid descriptor from media route provider: "
1064                                    + mProvider.getFlattenedComponentName());
1065                        }
1066                    }
1067
1068                    // Dispose all remaining routes that do not have matching descriptors.
1069                    for (int i = mRoutes.size() - 1; i >= targetIndex; i--) {
1070                        RouteRecord route = mRoutes.remove(i);
1071                        route.updateDescriptor(null); // mark route invalid
1072                        changed = true;
1073                    }
1074                }
1075                return changed;
1076            }
1077
1078            public void appendClientState(MediaRouterClientState state) {
1079                final int routeCount = mRoutes.size();
1080                for (int i = 0; i < routeCount; i++) {
1081                    state.routes.add(mRoutes.get(i).getInfo());
1082                }
1083            }
1084
1085            public RouteRecord findRouteByUniqueId(String uniqueId) {
1086                final int routeCount = mRoutes.size();
1087                for (int i = 0; i < routeCount; i++) {
1088                    RouteRecord route = mRoutes.get(i);
1089                    if (route.getUniqueId().equals(uniqueId)) {
1090                        return route;
1091                    }
1092                }
1093                return null;
1094            }
1095
1096            private int findRouteByDescriptorId(String descriptorId) {
1097                final int routeCount = mRoutes.size();
1098                for (int i = 0; i < routeCount; i++) {
1099                    RouteRecord route = mRoutes.get(i);
1100                    if (route.getDescriptorId().equals(descriptorId)) {
1101                        return i;
1102                    }
1103                }
1104                return -1;
1105            }
1106
1107            public void dump(PrintWriter pw, String prefix) {
1108                pw.println(prefix + this);
1109
1110                final String indent = prefix + "  ";
1111                mProvider.dump(pw, indent);
1112
1113                final int routeCount = mRoutes.size();
1114                if (routeCount != 0) {
1115                    for (int i = 0; i < routeCount; i++) {
1116                        mRoutes.get(i).dump(pw, indent);
1117                    }
1118                } else {
1119                    pw.println(indent + "<no routes>");
1120                }
1121            }
1122
1123            @Override
1124            public String toString() {
1125                return "Provider " + mProvider.getFlattenedComponentName();
1126            }
1127
1128            private String assignRouteUniqueId(String descriptorId) {
1129                return mUniquePrefix + descriptorId;
1130            }
1131        }
1132
1133        static final class RouteRecord {
1134            private final ProviderRecord mProviderRecord;
1135            private final String mDescriptorId;
1136            private final MediaRouterClientState.RouteInfo mMutableInfo;
1137            private MediaRouterClientState.RouteInfo mImmutableInfo;
1138            private RemoteDisplayInfo mDescriptor;
1139
1140            public RouteRecord(ProviderRecord providerRecord,
1141                    String descriptorId, String uniqueId) {
1142                mProviderRecord = providerRecord;
1143                mDescriptorId = descriptorId;
1144                mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId);
1145            }
1146
1147            public RemoteDisplayProviderProxy getProvider() {
1148                return mProviderRecord.getProvider();
1149            }
1150
1151            public ProviderRecord getProviderRecord() {
1152                return mProviderRecord;
1153            }
1154
1155            public String getDescriptorId() {
1156                return mDescriptorId;
1157            }
1158
1159            public String getUniqueId() {
1160                return mMutableInfo.id;
1161            }
1162
1163            public MediaRouterClientState.RouteInfo getInfo() {
1164                if (mImmutableInfo == null) {
1165                    mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo);
1166                }
1167                return mImmutableInfo;
1168            }
1169
1170            public boolean isValid() {
1171                return mDescriptor != null;
1172            }
1173
1174            public boolean isEnabled() {
1175                return mMutableInfo.enabled;
1176            }
1177
1178            public int getStatus() {
1179                return mMutableInfo.statusCode;
1180            }
1181
1182            public boolean updateDescriptor(RemoteDisplayInfo descriptor) {
1183                boolean changed = false;
1184                if (mDescriptor != descriptor) {
1185                    mDescriptor = descriptor;
1186                    if (descriptor != null) {
1187                        final String name = computeName(descriptor);
1188                        if (!Objects.equal(mMutableInfo.name, name)) {
1189                            mMutableInfo.name = name;
1190                            changed = true;
1191                        }
1192                        final String description = computeDescription(descriptor);
1193                        if (!Objects.equal(mMutableInfo.description, description)) {
1194                            mMutableInfo.description = description;
1195                            changed = true;
1196                        }
1197                        final int supportedTypes = computeSupportedTypes(descriptor);
1198                        if (mMutableInfo.supportedTypes != supportedTypes) {
1199                            mMutableInfo.supportedTypes = supportedTypes;
1200                            changed = true;
1201                        }
1202                        final boolean enabled = computeEnabled(descriptor);
1203                        if (mMutableInfo.enabled != enabled) {
1204                            mMutableInfo.enabled = enabled;
1205                            changed = true;
1206                        }
1207                        final int statusCode = computeStatusCode(descriptor);
1208                        if (mMutableInfo.statusCode != statusCode) {
1209                            mMutableInfo.statusCode = statusCode;
1210                            changed = true;
1211                        }
1212                        final int playbackType = computePlaybackType(descriptor);
1213                        if (mMutableInfo.playbackType != playbackType) {
1214                            mMutableInfo.playbackType = playbackType;
1215                            changed = true;
1216                        }
1217                        final int playbackStream = computePlaybackStream(descriptor);
1218                        if (mMutableInfo.playbackStream != playbackStream) {
1219                            mMutableInfo.playbackStream = playbackStream;
1220                            changed = true;
1221                        }
1222                        final int volume = computeVolume(descriptor);
1223                        if (mMutableInfo.volume != volume) {
1224                            mMutableInfo.volume = volume;
1225                            changed = true;
1226                        }
1227                        final int volumeMax = computeVolumeMax(descriptor);
1228                        if (mMutableInfo.volumeMax != volumeMax) {
1229                            mMutableInfo.volumeMax = volumeMax;
1230                            changed = true;
1231                        }
1232                        final int volumeHandling = computeVolumeHandling(descriptor);
1233                        if (mMutableInfo.volumeHandling != volumeHandling) {
1234                            mMutableInfo.volumeHandling = volumeHandling;
1235                            changed = true;
1236                        }
1237                        final int presentationDisplayId = computePresentationDisplayId(descriptor);
1238                        if (mMutableInfo.presentationDisplayId != presentationDisplayId) {
1239                            mMutableInfo.presentationDisplayId = presentationDisplayId;
1240                            changed = true;
1241                        }
1242                    }
1243                }
1244                if (changed) {
1245                    mImmutableInfo = null;
1246                }
1247                return changed;
1248            }
1249
1250            public void dump(PrintWriter pw, String prefix) {
1251                pw.println(prefix + this);
1252
1253                final String indent = prefix + "  ";
1254                pw.println(indent + "mMutableInfo=" + mMutableInfo);
1255                pw.println(indent + "mDescriptorId=" + mDescriptorId);
1256                pw.println(indent + "mDescriptor=" + mDescriptor);
1257            }
1258
1259            @Override
1260            public String toString() {
1261                return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")";
1262            }
1263
1264            private static String computeName(RemoteDisplayInfo descriptor) {
1265                // Note that isValid() already ensures the name is non-empty.
1266                return descriptor.name;
1267            }
1268
1269            private static String computeDescription(RemoteDisplayInfo descriptor) {
1270                final String description = descriptor.description;
1271                return TextUtils.isEmpty(description) ? null : description;
1272            }
1273
1274            private static int computeSupportedTypes(RemoteDisplayInfo descriptor) {
1275                return MediaRouter.ROUTE_TYPE_LIVE_AUDIO
1276                        | MediaRouter.ROUTE_TYPE_LIVE_VIDEO
1277                        | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
1278            }
1279
1280            private static boolean computeEnabled(RemoteDisplayInfo descriptor) {
1281                switch (descriptor.status) {
1282                    case RemoteDisplayInfo.STATUS_CONNECTED:
1283                    case RemoteDisplayInfo.STATUS_CONNECTING:
1284                    case RemoteDisplayInfo.STATUS_AVAILABLE:
1285                        return true;
1286                    default:
1287                        return false;
1288                }
1289            }
1290
1291            private static int computeStatusCode(RemoteDisplayInfo descriptor) {
1292                switch (descriptor.status) {
1293                    case RemoteDisplayInfo.STATUS_NOT_AVAILABLE:
1294                        return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE;
1295                    case RemoteDisplayInfo.STATUS_AVAILABLE:
1296                        return MediaRouter.RouteInfo.STATUS_AVAILABLE;
1297                    case RemoteDisplayInfo.STATUS_IN_USE:
1298                        return MediaRouter.RouteInfo.STATUS_IN_USE;
1299                    case RemoteDisplayInfo.STATUS_CONNECTING:
1300                        return MediaRouter.RouteInfo.STATUS_CONNECTING;
1301                    case RemoteDisplayInfo.STATUS_CONNECTED:
1302                        return MediaRouter.RouteInfo.STATUS_CONNECTED;
1303                    default:
1304                        return MediaRouter.RouteInfo.STATUS_NONE;
1305                }
1306            }
1307
1308            private static int computePlaybackType(RemoteDisplayInfo descriptor) {
1309                return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
1310            }
1311
1312            private static int computePlaybackStream(RemoteDisplayInfo descriptor) {
1313                return AudioSystem.STREAM_MUSIC;
1314            }
1315
1316            private static int computeVolume(RemoteDisplayInfo descriptor) {
1317                final int volume = descriptor.volume;
1318                final int volumeMax = descriptor.volumeMax;
1319                if (volume < 0) {
1320                    return 0;
1321                } else if (volume > volumeMax) {
1322                    return volumeMax;
1323                }
1324                return volume;
1325            }
1326
1327            private static int computeVolumeMax(RemoteDisplayInfo descriptor) {
1328                final int volumeMax = descriptor.volumeMax;
1329                return volumeMax > 0 ? volumeMax : 0;
1330            }
1331
1332            private static int computeVolumeHandling(RemoteDisplayInfo descriptor) {
1333                final int volumeHandling = descriptor.volumeHandling;
1334                switch (volumeHandling) {
1335                    case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE:
1336                        return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
1337                    case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED:
1338                    default:
1339                        return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
1340                }
1341            }
1342
1343            private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) {
1344                // The MediaRouter class validates that the id corresponds to an extant
1345                // presentation display.  So all we do here is canonicalize the null case.
1346                final int displayId = descriptor.presentationDisplayId;
1347                return displayId < 0 ? -1 : displayId;
1348            }
1349        }
1350    }
1351}
1352