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