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