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