MediaRouter.java revision 0e43c503ab73f0a66875ed4cd9b40e68bbb405aa
1/*
2 * Copyright (C) 2012 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 android.media;
18
19import android.Manifest;
20import android.annotation.DrawableRes;
21import android.annotation.IntDef;
22import android.annotation.NonNull;
23import android.annotation.SystemService;
24import android.app.ActivityThread;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.content.pm.PackageManager;
30import android.content.res.Resources;
31import android.graphics.drawable.Drawable;
32import android.hardware.display.DisplayManager;
33import android.hardware.display.WifiDisplay;
34import android.hardware.display.WifiDisplayStatus;
35import android.media.session.MediaSession;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.Process;
39import android.os.RemoteException;
40import android.os.ServiceManager;
41import android.os.UserHandle;
42import android.text.TextUtils;
43import android.util.Log;
44import android.view.Display;
45
46import java.lang.annotation.Retention;
47import java.lang.annotation.RetentionPolicy;
48import java.util.ArrayList;
49import java.util.HashMap;
50import java.util.List;
51import java.util.Objects;
52import java.util.concurrent.CopyOnWriteArrayList;
53
54/**
55 * MediaRouter allows applications to control the routing of media channels
56 * and streams from the current device to external speakers and destination devices.
57 *
58 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
59 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
60 * Context.MEDIA_ROUTER_SERVICE}.
61 *
62 * <p>The media router API is not thread-safe; all interactions with it must be
63 * done from the main thread of the process.</p>
64 */
65@SystemService(Context.MEDIA_ROUTER_SERVICE)
66public class MediaRouter {
67    private static final String TAG = "MediaRouter";
68    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
69
70    static class Static implements DisplayManager.DisplayListener {
71        final String mPackageName;
72        final Resources mResources;
73        final IAudioService mAudioService;
74        final DisplayManager mDisplayService;
75        final IMediaRouterService mMediaRouterService;
76        final Handler mHandler;
77        final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
78                new CopyOnWriteArrayList<CallbackInfo>();
79
80        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
81        final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
82
83        final RouteCategory mSystemCategory;
84
85        final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
86
87        RouteInfo mDefaultAudioVideo;
88        RouteInfo mBluetoothA2dpRoute;
89
90        RouteInfo mSelectedRoute;
91
92        final boolean mCanConfigureWifiDisplays;
93        boolean mActivelyScanningWifiDisplays;
94        String mPreviousActiveWifiDisplayAddress;
95
96        int mDiscoveryRequestRouteTypes;
97        boolean mDiscoverRequestActiveScan;
98
99        int mCurrentUserId = -1;
100        IMediaRouterClient mClient;
101        MediaRouterClientState mClientState;
102
103        final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
104            @Override
105            public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
106                mHandler.post(new Runnable() {
107                    @Override public void run() {
108                        updateAudioRoutes(newRoutes);
109                    }
110                });
111            }
112        };
113
114        Static(Context appContext) {
115            mPackageName = appContext.getPackageName();
116            mResources = appContext.getResources();
117            mHandler = new Handler(appContext.getMainLooper());
118
119            IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
120            mAudioService = IAudioService.Stub.asInterface(b);
121
122            mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
123
124            mMediaRouterService = IMediaRouterService.Stub.asInterface(
125                    ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
126
127            mSystemCategory = new RouteCategory(
128                    com.android.internal.R.string.default_audio_route_category_name,
129                    ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
130            mSystemCategory.mIsSystem = true;
131
132            // Only the system can configure wifi displays.  The display manager
133            // enforces this with a permission check.  Set a flag here so that we
134            // know whether this process is actually allowed to scan and connect.
135            mCanConfigureWifiDisplays = appContext.checkPermission(
136                    Manifest.permission.CONFIGURE_WIFI_DISPLAY,
137                    Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
138        }
139
140        // Called after sStatic is initialized
141        void startMonitoringRoutes(Context appContext) {
142            mDefaultAudioVideo = new RouteInfo(mSystemCategory);
143            mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
144            mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
145            mDefaultAudioVideo.updatePresentationDisplay();
146            if (((AudioManager) appContext.getSystemService(Context.AUDIO_SERVICE))
147                    .isVolumeFixed()) {
148                mDefaultAudioVideo.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
149            }
150
151            addRouteStatic(mDefaultAudioVideo);
152
153            // This will select the active wifi display route if there is one.
154            updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
155
156            appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
157                    new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
158            appContext.registerReceiver(new VolumeChangeReceiver(),
159                    new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
160
161            mDisplayService.registerDisplayListener(this, mHandler);
162
163            AudioRoutesInfo newAudioRoutes = null;
164            try {
165                newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
166            } catch (RemoteException e) {
167            }
168            if (newAudioRoutes != null) {
169                // This will select the active BT route if there is one and the current
170                // selected route is the default system route, or if there is no selected
171                // route yet.
172                updateAudioRoutes(newAudioRoutes);
173            }
174
175            // Bind to the media router service.
176            rebindAsUser(UserHandle.myUserId());
177
178            // Select the default route if the above didn't sync us up
179            // appropriately with relevant system state.
180            if (mSelectedRoute == null) {
181                selectDefaultRouteStatic();
182            }
183        }
184
185        void updateAudioRoutes(AudioRoutesInfo newRoutes) {
186            boolean audioRoutesChanged = false;
187            boolean forceUseDefaultRoute = false;
188
189            if (newRoutes.mainType != mCurAudioRoutesInfo.mainType) {
190                mCurAudioRoutesInfo.mainType = newRoutes.mainType;
191                int name;
192                if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
193                        || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
194                    name = com.android.internal.R.string.default_audio_route_name_headphones;
195                } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
196                    name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
197                } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
198                    name = com.android.internal.R.string.default_media_route_name_hdmi;
199                } else {
200                    name = com.android.internal.R.string.default_audio_route_name;
201                }
202                mDefaultAudioVideo.mNameResId = name;
203                dispatchRouteChanged(mDefaultAudioVideo);
204
205                if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET
206                        | AudioRoutesInfo.MAIN_HEADPHONES | AudioRoutesInfo.MAIN_USB)) != 0) {
207                    forceUseDefaultRoute = true;
208                }
209                audioRoutesChanged = true;
210            }
211
212            if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) {
213                forceUseDefaultRoute = false;
214                mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
215                if (mCurAudioRoutesInfo.bluetoothName != null) {
216                    if (mBluetoothA2dpRoute == null) {
217                        // BT connected
218                        final RouteInfo info = new RouteInfo(mSystemCategory);
219                        info.mName = mCurAudioRoutesInfo.bluetoothName;
220                        info.mDescription = mResources.getText(
221                                com.android.internal.R.string.bluetooth_a2dp_audio_route_name);
222                        info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
223                        info.mDeviceType = RouteInfo.DEVICE_TYPE_BLUETOOTH;
224                        mBluetoothA2dpRoute = info;
225                        addRouteStatic(mBluetoothA2dpRoute);
226                    } else {
227                        mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.bluetoothName;
228                        dispatchRouteChanged(mBluetoothA2dpRoute);
229                    }
230                } else if (mBluetoothA2dpRoute != null) {
231                    // BT disconnected
232                    removeRouteStatic(mBluetoothA2dpRoute);
233                    mBluetoothA2dpRoute = null;
234                }
235                audioRoutesChanged = true;
236            }
237
238            if (audioRoutesChanged) {
239                Log.v(TAG, "Audio routes updated: " + newRoutes + ", a2dp=" + isBluetoothA2dpOn());
240                if (mSelectedRoute == null || mSelectedRoute == mDefaultAudioVideo
241                        || mSelectedRoute == mBluetoothA2dpRoute) {
242                    if (forceUseDefaultRoute || mBluetoothA2dpRoute == null) {
243                        selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false);
244                    } else {
245                        selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false);
246                    }
247                }
248            }
249        }
250
251        boolean isBluetoothA2dpOn() {
252            try {
253                return mBluetoothA2dpRoute != null && mAudioService.isBluetoothA2dpOn();
254            } catch (RemoteException e) {
255                Log.e(TAG, "Error querying Bluetooth A2DP state", e);
256                return false;
257            }
258        }
259
260        void updateDiscoveryRequest() {
261            // What are we looking for today?
262            int routeTypes = 0;
263            int passiveRouteTypes = 0;
264            boolean activeScan = false;
265            boolean activeScanWifiDisplay = false;
266            final int count = mCallbacks.size();
267            for (int i = 0; i < count; i++) {
268                CallbackInfo cbi = mCallbacks.get(i);
269                if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
270                        | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) {
271                    // Discovery explicitly requested.
272                    routeTypes |= cbi.type;
273                } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) {
274                    // Discovery only passively requested.
275                    passiveRouteTypes |= cbi.type;
276                } else {
277                    // Legacy case since applications don't specify the discovery flag.
278                    // Unfortunately we just have to assume they always need discovery
279                    // whenever they have a callback registered.
280                    routeTypes |= cbi.type;
281                }
282                if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
283                    activeScan = true;
284                    if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
285                        activeScanWifiDisplay = true;
286                    }
287                }
288            }
289            if (routeTypes != 0 || activeScan) {
290                // If someone else requests discovery then enable the passive listeners.
291                // This is used by the MediaRouteButton and MediaRouteActionProvider since
292                // they don't receive lifecycle callbacks from the Activity.
293                routeTypes |= passiveRouteTypes;
294            }
295
296            // Update wifi display scanning.
297            // TODO: All of this should be managed by the media router service.
298            if (mCanConfigureWifiDisplays) {
299                if (mSelectedRoute != null
300                        && mSelectedRoute.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) {
301                    // Don't scan while already connected to a remote display since
302                    // it may interfere with the ongoing transmission.
303                    activeScanWifiDisplay = false;
304                }
305                if (activeScanWifiDisplay) {
306                    if (!mActivelyScanningWifiDisplays) {
307                        mActivelyScanningWifiDisplays = true;
308                        mDisplayService.startWifiDisplayScan();
309                    }
310                } else {
311                    if (mActivelyScanningWifiDisplays) {
312                        mActivelyScanningWifiDisplays = false;
313                        mDisplayService.stopWifiDisplayScan();
314                    }
315                }
316            }
317
318            // Tell the media router service all about it.
319            if (routeTypes != mDiscoveryRequestRouteTypes
320                    || activeScan != mDiscoverRequestActiveScan) {
321                mDiscoveryRequestRouteTypes = routeTypes;
322                mDiscoverRequestActiveScan = activeScan;
323                publishClientDiscoveryRequest();
324            }
325        }
326
327        @Override
328        public void onDisplayAdded(int displayId) {
329            updatePresentationDisplays(displayId);
330        }
331
332        @Override
333        public void onDisplayChanged(int displayId) {
334            updatePresentationDisplays(displayId);
335        }
336
337        @Override
338        public void onDisplayRemoved(int displayId) {
339            updatePresentationDisplays(displayId);
340        }
341
342        public Display[] getAllPresentationDisplays() {
343            return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
344        }
345
346        private void updatePresentationDisplays(int changedDisplayId) {
347            final int count = mRoutes.size();
348            for (int i = 0; i < count; i++) {
349                final RouteInfo route = mRoutes.get(i);
350                if (route.updatePresentationDisplay() || (route.mPresentationDisplay != null
351                        && route.mPresentationDisplay.getDisplayId() == changedDisplayId)) {
352                    dispatchRoutePresentationDisplayChanged(route);
353                }
354            }
355        }
356
357        void setSelectedRoute(RouteInfo info, boolean explicit) {
358            // Must be non-reentrant.
359            mSelectedRoute = info;
360            publishClientSelectedRoute(explicit);
361        }
362
363        void rebindAsUser(int userId) {
364            if (mCurrentUserId != userId || userId < 0 || mClient == null) {
365                if (mClient != null) {
366                    try {
367                        mMediaRouterService.unregisterClient(mClient);
368                    } catch (RemoteException ex) {
369                        Log.e(TAG, "Unable to unregister media router client.", ex);
370                    }
371                    mClient = null;
372                }
373
374                mCurrentUserId = userId;
375
376                try {
377                    Client client = new Client();
378                    mMediaRouterService.registerClientAsUser(client, mPackageName, userId);
379                    mClient = client;
380                } catch (RemoteException ex) {
381                    Log.e(TAG, "Unable to register media router client.", ex);
382                }
383
384                publishClientDiscoveryRequest();
385                publishClientSelectedRoute(false);
386                updateClientState();
387            }
388        }
389
390        void publishClientDiscoveryRequest() {
391            if (mClient != null) {
392                try {
393                    mMediaRouterService.setDiscoveryRequest(mClient,
394                            mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan);
395                } catch (RemoteException ex) {
396                    Log.e(TAG, "Unable to publish media router client discovery request.", ex);
397                }
398            }
399        }
400
401        void publishClientSelectedRoute(boolean explicit) {
402            if (mClient != null) {
403                try {
404                    mMediaRouterService.setSelectedRoute(mClient,
405                            mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null,
406                            explicit);
407                } catch (RemoteException ex) {
408                    Log.e(TAG, "Unable to publish media router client selected route.", ex);
409                }
410            }
411        }
412
413        void updateClientState() {
414            // Update the client state.
415            mClientState = null;
416            if (mClient != null) {
417                try {
418                    mClientState = mMediaRouterService.getState(mClient);
419                } catch (RemoteException ex) {
420                    Log.e(TAG, "Unable to retrieve media router client state.", ex);
421                }
422            }
423            final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes =
424                    mClientState != null ? mClientState.routes : null;
425
426            // Add or update routes.
427            final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0;
428            for (int i = 0; i < globalRouteCount; i++) {
429                final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i);
430                RouteInfo route = findGlobalRoute(globalRoute.id);
431                if (route == null) {
432                    route = makeGlobalRoute(globalRoute);
433                    addRouteStatic(route);
434                } else {
435                    updateGlobalRoute(route, globalRoute);
436                }
437            }
438
439            // Remove defunct routes.
440            outer: for (int i = mRoutes.size(); i-- > 0; ) {
441                final RouteInfo route = mRoutes.get(i);
442                final String globalRouteId = route.mGlobalRouteId;
443                if (globalRouteId != null) {
444                    for (int j = 0; j < globalRouteCount; j++) {
445                        MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j);
446                        if (globalRouteId.equals(globalRoute.id)) {
447                            continue outer; // found
448                        }
449                    }
450                    // not found
451                    removeRouteStatic(route);
452                }
453            }
454        }
455
456        void requestSetVolume(RouteInfo route, int volume) {
457            if (route.mGlobalRouteId != null && mClient != null) {
458                try {
459                    mMediaRouterService.requestSetVolume(mClient,
460                            route.mGlobalRouteId, volume);
461                } catch (RemoteException ex) {
462                    Log.w(TAG, "Unable to request volume change.", ex);
463                }
464            }
465        }
466
467        void requestUpdateVolume(RouteInfo route, int direction) {
468            if (route.mGlobalRouteId != null && mClient != null) {
469                try {
470                    mMediaRouterService.requestUpdateVolume(mClient,
471                            route.mGlobalRouteId, direction);
472                } catch (RemoteException ex) {
473                    Log.w(TAG, "Unable to request volume change.", ex);
474                }
475            }
476        }
477
478        RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) {
479            RouteInfo route = new RouteInfo(mSystemCategory);
480            route.mGlobalRouteId = globalRoute.id;
481            route.mName = globalRoute.name;
482            route.mDescription = globalRoute.description;
483            route.mSupportedTypes = globalRoute.supportedTypes;
484            route.mDeviceType = globalRoute.deviceType;
485            route.mEnabled = globalRoute.enabled;
486            route.setRealStatusCode(globalRoute.statusCode);
487            route.mPlaybackType = globalRoute.playbackType;
488            route.mPlaybackStream = globalRoute.playbackStream;
489            route.mVolume = globalRoute.volume;
490            route.mVolumeMax = globalRoute.volumeMax;
491            route.mVolumeHandling = globalRoute.volumeHandling;
492            route.mPresentationDisplayId = globalRoute.presentationDisplayId;
493            route.updatePresentationDisplay();
494            return route;
495        }
496
497        void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) {
498            boolean changed = false;
499            boolean volumeChanged = false;
500            boolean presentationDisplayChanged = false;
501
502            if (!Objects.equals(route.mName, globalRoute.name)) {
503                route.mName = globalRoute.name;
504                changed = true;
505            }
506            if (!Objects.equals(route.mDescription, globalRoute.description)) {
507                route.mDescription = globalRoute.description;
508                changed = true;
509            }
510            final int oldSupportedTypes = route.mSupportedTypes;
511            if (oldSupportedTypes != globalRoute.supportedTypes) {
512                route.mSupportedTypes = globalRoute.supportedTypes;
513                changed = true;
514            }
515            if (route.mEnabled != globalRoute.enabled) {
516                route.mEnabled = globalRoute.enabled;
517                changed = true;
518            }
519            if (route.mRealStatusCode != globalRoute.statusCode) {
520                route.setRealStatusCode(globalRoute.statusCode);
521                changed = true;
522            }
523            if (route.mPlaybackType != globalRoute.playbackType) {
524                route.mPlaybackType = globalRoute.playbackType;
525                changed = true;
526            }
527            if (route.mPlaybackStream != globalRoute.playbackStream) {
528                route.mPlaybackStream = globalRoute.playbackStream;
529                changed = true;
530            }
531            if (route.mVolume != globalRoute.volume) {
532                route.mVolume = globalRoute.volume;
533                changed = true;
534                volumeChanged = true;
535            }
536            if (route.mVolumeMax != globalRoute.volumeMax) {
537                route.mVolumeMax = globalRoute.volumeMax;
538                changed = true;
539                volumeChanged = true;
540            }
541            if (route.mVolumeHandling != globalRoute.volumeHandling) {
542                route.mVolumeHandling = globalRoute.volumeHandling;
543                changed = true;
544                volumeChanged = true;
545            }
546            if (route.mPresentationDisplayId != globalRoute.presentationDisplayId) {
547                route.mPresentationDisplayId = globalRoute.presentationDisplayId;
548                route.updatePresentationDisplay();
549                changed = true;
550                presentationDisplayChanged = true;
551            }
552
553            if (changed) {
554                dispatchRouteChanged(route, oldSupportedTypes);
555            }
556            if (volumeChanged) {
557                dispatchRouteVolumeChanged(route);
558            }
559            if (presentationDisplayChanged) {
560                dispatchRoutePresentationDisplayChanged(route);
561            }
562        }
563
564        RouteInfo findGlobalRoute(String globalRouteId) {
565            final int count = mRoutes.size();
566            for (int i = 0; i < count; i++) {
567                final RouteInfo route = mRoutes.get(i);
568                if (globalRouteId.equals(route.mGlobalRouteId)) {
569                    return route;
570                }
571            }
572            return null;
573        }
574
575        boolean isPlaybackActive() {
576            if (mClient != null) {
577                try {
578                    return mMediaRouterService.isPlaybackActive(mClient);
579                } catch (RemoteException ex) {
580                    Log.e(TAG, "Unable to retrieve playback active state.", ex);
581                }
582            }
583            return false;
584        }
585
586        final class Client extends IMediaRouterClient.Stub {
587            @Override
588            public void onStateChanged() {
589                mHandler.post(new Runnable() {
590                    @Override
591                    public void run() {
592                        if (Client.this == mClient) {
593                            updateClientState();
594                        }
595                    }
596                });
597            }
598
599            @Override
600            public void onRestoreRoute() {
601                // Skip restoring route if the selected route is not a system audio route, or
602                // MediaRouter is initializing.
603                if ((mSelectedRoute != mDefaultAudioVideo && mSelectedRoute != mBluetoothA2dpRoute)
604                        || mSelectedRoute == null) {
605                    return;
606                }
607                Log.v(TAG, "onRestoreRoute() : a2dp=" + isBluetoothA2dpOn());
608                mSelectedRoute.select();
609            }
610        }
611    }
612
613    static Static sStatic;
614
615    /**
616     * Route type flag for live audio.
617     *
618     * <p>A device that supports live audio routing will allow the media audio stream
619     * to be routed to supported destinations. This can include internal speakers or
620     * audio jacks on the device itself, A2DP devices, and more.</p>
621     *
622     * <p>Once initiated this routing is transparent to the application. All audio
623     * played on the media stream will be routed to the selected destination.</p>
624     */
625    public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0;
626
627    /**
628     * Route type flag for live video.
629     *
630     * <p>A device that supports live video routing will allow a mirrored version
631     * of the device's primary display or a customized
632     * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p>
633     *
634     * <p>Once initiated, display mirroring is transparent to the application.
635     * While remote routing is active the application may use a
636     * {@link android.app.Presentation Presentation} to replace the mirrored view
637     * on the external display with different content.</p>
638     *
639     * @see RouteInfo#getPresentationDisplay()
640     * @see android.app.Presentation
641     */
642    public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1;
643
644    /**
645     * Temporary interop constant to identify remote displays.
646     * @hide To be removed when media router API is updated.
647     */
648    public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2;
649
650    /**
651     * Route type flag for application-specific usage.
652     *
653     * <p>Unlike other media route types, user routes are managed by the application.
654     * The MediaRouter will manage and dispatch events for user routes, but the application
655     * is expected to interpret the meaning of these events and perform the requested
656     * routing tasks.</p>
657     */
658    public static final int ROUTE_TYPE_USER = 1 << 23;
659
660    static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
661            | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER;
662
663    /**
664     * Flag for {@link #addCallback}: Actively scan for routes while this callback
665     * is registered.
666     * <p>
667     * When this flag is specified, the media router will actively scan for new
668     * routes.  Certain routes, such as wifi display routes, may not be discoverable
669     * except when actively scanning.  This flag is typically used when the route picker
670     * dialog has been opened by the user to ensure that the route information is
671     * up to date.
672     * </p><p>
673     * Active scanning may consume a significant amount of power and may have intrusive
674     * effects on wireless connectivity.  Therefore it is important that active scanning
675     * only be requested when it is actually needed to satisfy a user request to
676     * discover and select a new route.
677     * </p>
678     */
679    public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
680
681    /**
682     * Flag for {@link #addCallback}: Do not filter route events.
683     * <p>
684     * When this flag is specified, the callback will be invoked for event that affect any
685     * route even if they do not match the callback's filter.
686     * </p>
687     */
688    public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
689
690    /**
691     * Explicitly requests discovery.
692     *
693     * @hide Future API ported from support library.  Revisit this later.
694     */
695    public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
696
697    /**
698     * Requests that discovery be performed but only if there is some other active
699     * callback already registered.
700     *
701     * @hide Compatibility workaround for the fact that applications do not currently
702     * request discovery explicitly (except when using the support library API).
703     */
704    public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3;
705
706    /**
707     * Flag for {@link #isRouteAvailable}: Ignore the default route.
708     * <p>
709     * This flag is used to determine whether a matching non-default route is available.
710     * This constraint may be used to decide whether to offer the route chooser dialog
711     * to the user.  There is no point offering the chooser if there are no
712     * non-default choices.
713     * </p>
714     *
715     * @hide Future API ported from support library.  Revisit this later.
716     */
717    public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
718
719    // Maps application contexts
720    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
721
722    static String typesToString(int types) {
723        final StringBuilder result = new StringBuilder();
724        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
725            result.append("ROUTE_TYPE_LIVE_AUDIO ");
726        }
727        if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) {
728            result.append("ROUTE_TYPE_LIVE_VIDEO ");
729        }
730        if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
731            result.append("ROUTE_TYPE_REMOTE_DISPLAY ");
732        }
733        if ((types & ROUTE_TYPE_USER) != 0) {
734            result.append("ROUTE_TYPE_USER ");
735        }
736        return result.toString();
737    }
738
739    /** @hide */
740    public MediaRouter(Context context) {
741        synchronized (Static.class) {
742            if (sStatic == null) {
743                final Context appContext = context.getApplicationContext();
744                sStatic = new Static(appContext);
745                sStatic.startMonitoringRoutes(appContext);
746            }
747        }
748    }
749
750    /**
751     * Gets the default route for playing media content on the system.
752     * <p>
753     * The system always provides a default route.
754     * </p>
755     *
756     * @return The default route, which is guaranteed to never be null.
757     */
758    public RouteInfo getDefaultRoute() {
759        return sStatic.mDefaultAudioVideo;
760    }
761
762    /**
763     * Returns a Bluetooth route if available, otherwise the default route.
764     * @hide
765     */
766    public RouteInfo getFallbackRoute() {
767        return (sStatic.mBluetoothA2dpRoute != null)
768                ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo;
769    }
770
771    /**
772     * @hide for use by framework routing UI
773     */
774    public RouteCategory getSystemCategory() {
775        return sStatic.mSystemCategory;
776    }
777
778    /** @hide */
779    public RouteInfo getSelectedRoute() {
780        return getSelectedRoute(ROUTE_TYPE_ANY);
781    }
782
783    /**
784     * Return the currently selected route for any of the given types
785     *
786     * @param type route types
787     * @return the selected route
788     */
789    public RouteInfo getSelectedRoute(int type) {
790        if (sStatic.mSelectedRoute != null &&
791                (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) {
792            // If the selected route supports any of the types supplied, it's still considered
793            // 'selected' for that type.
794            return sStatic.mSelectedRoute;
795        } else if (type == ROUTE_TYPE_USER) {
796            // The caller specifically asked for a user route and the currently selected route
797            // doesn't qualify.
798            return null;
799        }
800        // If the above didn't match and we're not specifically asking for a user route,
801        // consider the default selected.
802        return sStatic.mDefaultAudioVideo;
803    }
804
805    /**
806     * Returns true if there is a route that matches the specified types.
807     * <p>
808     * This method returns true if there are any available routes that match the types
809     * regardless of whether they are enabled or disabled.  If the
810     * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
811     * the method will only consider non-default routes.
812     * </p>
813     *
814     * @param types The types to match.
815     * @param flags Flags to control the determination of whether a route may be available.
816     * May be zero or {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE}.
817     * @return True if a matching route may be available.
818     *
819     * @hide Future API ported from support library.  Revisit this later.
820     */
821    public boolean isRouteAvailable(int types, int flags) {
822        final int count = sStatic.mRoutes.size();
823        for (int i = 0; i < count; i++) {
824            RouteInfo route = sStatic.mRoutes.get(i);
825            if (route.matchesTypes(types)) {
826                if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) == 0
827                        || route != sStatic.mDefaultAudioVideo) {
828                    return true;
829                }
830            }
831        }
832
833        // It doesn't look like we can find a matching route right now.
834        return false;
835    }
836
837    /**
838     * Add a callback to listen to events about specific kinds of media routes.
839     * If the specified callback is already registered, its registration will be updated for any
840     * additional route types specified.
841     * <p>
842     * This is a convenience method that has the same effect as calling
843     * {@link #addCallback(int, Callback, int)} without flags.
844     * </p>
845     *
846     * @param types Types of routes this callback is interested in
847     * @param cb Callback to add
848     */
849    public void addCallback(int types, Callback cb) {
850        addCallback(types, cb, 0);
851    }
852
853    /**
854     * Add a callback to listen to events about specific kinds of media routes.
855     * If the specified callback is already registered, its registration will be updated for any
856     * additional route types specified.
857     * <p>
858     * By default, the callback will only be invoked for events that affect routes
859     * that match the specified selector.  The filtering may be disabled by specifying
860     * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag.
861     * </p>
862     *
863     * @param types Types of routes this callback is interested in
864     * @param cb Callback to add
865     * @param flags Flags to control the behavior of the callback.
866     * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
867     * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
868     */
869    public void addCallback(int types, Callback cb, int flags) {
870        CallbackInfo info;
871        int index = findCallbackInfo(cb);
872        if (index >= 0) {
873            info = sStatic.mCallbacks.get(index);
874            info.type |= types;
875            info.flags |= flags;
876        } else {
877            info = new CallbackInfo(cb, types, flags, this);
878            sStatic.mCallbacks.add(info);
879        }
880        sStatic.updateDiscoveryRequest();
881    }
882
883    /**
884     * Remove the specified callback. It will no longer receive events about media routing.
885     *
886     * @param cb Callback to remove
887     */
888    public void removeCallback(Callback cb) {
889        int index = findCallbackInfo(cb);
890        if (index >= 0) {
891            sStatic.mCallbacks.remove(index);
892            sStatic.updateDiscoveryRequest();
893        } else {
894            Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
895        }
896    }
897
898    private int findCallbackInfo(Callback cb) {
899        final int count = sStatic.mCallbacks.size();
900        for (int i = 0; i < count; i++) {
901            final CallbackInfo info = sStatic.mCallbacks.get(i);
902            if (info.cb == cb) {
903                return i;
904            }
905        }
906        return -1;
907    }
908
909    /**
910     * Select the specified route to use for output of the given media types.
911     * <p class="note">
912     * As API version 18, this function may be used to select any route.
913     * In prior versions, this function could only be used to select user
914     * routes and would ignore any attempt to select a system route.
915     * </p>
916     *
917     * @param types type flags indicating which types this route should be used for.
918     *              The route must support at least a subset.
919     * @param route Route to select
920     * @throws IllegalArgumentException if the given route is {@code null}
921     */
922    public void selectRoute(int types, @NonNull RouteInfo route) {
923        if (route == null) {
924            throw new IllegalArgumentException("Route cannot be null.");
925        }
926        selectRouteStatic(types, route, true);
927    }
928
929    /**
930     * @hide internal use
931     */
932    public void selectRouteInt(int types, RouteInfo route, boolean explicit) {
933        selectRouteStatic(types, route, explicit);
934    }
935
936    static void selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit) {
937        Log.v(TAG, "Selecting route: " + route);
938        assert(route != null);
939        final RouteInfo oldRoute = sStatic.mSelectedRoute;
940        final RouteInfo currentSystemRoute = sStatic.isBluetoothA2dpOn()
941                ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo;
942        boolean wasDefaultOrBluetoothRoute = (oldRoute == sStatic.mDefaultAudioVideo
943                || oldRoute == sStatic.mBluetoothA2dpRoute);
944        if (oldRoute == route
945                && (!wasDefaultOrBluetoothRoute || route == currentSystemRoute)) {
946            return;
947        }
948        if (!route.matchesTypes(types)) {
949            Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
950                    typesToString(route.getSupportedTypes()) + " into route types " +
951                    typesToString(types));
952            return;
953        }
954
955        final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
956        if (sStatic.isPlaybackActive() && btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0
957                && (route == btRoute || route == sStatic.mDefaultAudioVideo)) {
958            try {
959                sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
960                // TODO: Remove the following logging when no longer needed.
961                if (route != btRoute) {
962                    StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
963                    StringBuffer sb = new StringBuffer();
964                    // callStack[3] is the caller of this method.
965                    for (int i = 3; i < callStack.length; i++) {
966                        StackTraceElement caller = callStack[i];
967                        sb.append(caller.getClassName() + "." + caller.getMethodName()
968                                + ":" + caller.getLineNumber()).append("  ");
969                    }
970                    Log.w(TAG, "Default route is selected while a BT route is available: pkgName="
971                            + sStatic.mPackageName + ", callers=" + sb.toString());
972                }
973            } catch (RemoteException e) {
974                Log.e(TAG, "Error changing Bluetooth A2DP state", e);
975            }
976        }
977
978        final WifiDisplay activeDisplay =
979                sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
980        final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
981        final boolean newRouteHasAddress = route.mDeviceAddress != null;
982        if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
983            if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
984                if (sStatic.mCanConfigureWifiDisplays) {
985                    sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
986                } else {
987                    Log.e(TAG, "Cannot connect to wifi displays because this process "
988                            + "is not allowed to do so.");
989                }
990            } else if (activeDisplay != null && !newRouteHasAddress) {
991                sStatic.mDisplayService.disconnectWifiDisplay();
992            }
993        }
994
995        sStatic.setSelectedRoute(route, explicit);
996
997        if (oldRoute != null) {
998            dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
999            if (oldRoute.resolveStatusCode()) {
1000                dispatchRouteChanged(oldRoute);
1001            }
1002        }
1003        if (route != null) {
1004            if (route.resolveStatusCode()) {
1005                dispatchRouteChanged(route);
1006            }
1007            dispatchRouteSelected(types & route.getSupportedTypes(), route);
1008        }
1009
1010        // The behavior of active scans may depend on the currently selected route.
1011        sStatic.updateDiscoveryRequest();
1012    }
1013
1014    static void selectDefaultRouteStatic() {
1015        // TODO: Be smarter about the route types here; this selects for all valid.
1016        if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute && sStatic.isBluetoothA2dpOn()) {
1017            selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false);
1018        } else {
1019            selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false);
1020        }
1021    }
1022
1023    /**
1024     * Compare the device address of a display and a route.
1025     * Nulls/no device address will match another null/no address.
1026     */
1027    static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) {
1028        final boolean routeHasAddress = info != null && info.mDeviceAddress != null;
1029        if (display == null && !routeHasAddress) {
1030            return true;
1031        }
1032
1033        if (display != null && routeHasAddress) {
1034            return display.getDeviceAddress().equals(info.mDeviceAddress);
1035        }
1036        return false;
1037    }
1038
1039    /**
1040     * Add an app-specified route for media to the MediaRouter.
1041     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
1042     *
1043     * @param info Definition of the route to add
1044     * @see #createUserRoute(RouteCategory)
1045     * @see #removeUserRoute(UserRouteInfo)
1046     */
1047    public void addUserRoute(UserRouteInfo info) {
1048        addRouteStatic(info);
1049    }
1050
1051    /**
1052     * @hide Framework use only
1053     */
1054    public void addRouteInt(RouteInfo info) {
1055        addRouteStatic(info);
1056    }
1057
1058    static void addRouteStatic(RouteInfo info) {
1059        Log.v(TAG, "Adding route: " + info);
1060        final RouteCategory cat = info.getCategory();
1061        if (!sStatic.mCategories.contains(cat)) {
1062            sStatic.mCategories.add(cat);
1063        }
1064        if (cat.isGroupable() && !(info instanceof RouteGroup)) {
1065            // Enforce that any added route in a groupable category must be in a group.
1066            final RouteGroup group = new RouteGroup(info.getCategory());
1067            group.mSupportedTypes = info.mSupportedTypes;
1068            sStatic.mRoutes.add(group);
1069            dispatchRouteAdded(group);
1070            group.addRoute(info);
1071
1072            info = group;
1073        } else {
1074            sStatic.mRoutes.add(info);
1075            dispatchRouteAdded(info);
1076        }
1077    }
1078
1079    /**
1080     * Remove an app-specified route for media from the MediaRouter.
1081     *
1082     * @param info Definition of the route to remove
1083     * @see #addUserRoute(UserRouteInfo)
1084     */
1085    public void removeUserRoute(UserRouteInfo info) {
1086        removeRouteStatic(info);
1087    }
1088
1089    /**
1090     * Remove all app-specified routes from the MediaRouter.
1091     *
1092     * @see #removeUserRoute(UserRouteInfo)
1093     */
1094    public void clearUserRoutes() {
1095        for (int i = 0; i < sStatic.mRoutes.size(); i++) {
1096            final RouteInfo info = sStatic.mRoutes.get(i);
1097            // TODO Right now, RouteGroups only ever contain user routes.
1098            // The code below will need to change if this assumption does.
1099            if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
1100                removeRouteStatic(info);
1101                i--;
1102            }
1103        }
1104    }
1105
1106    /**
1107     * @hide internal use only
1108     */
1109    public void removeRouteInt(RouteInfo info) {
1110        removeRouteStatic(info);
1111    }
1112
1113    static void removeRouteStatic(RouteInfo info) {
1114        Log.v(TAG, "Removing route: " + info);
1115        if (sStatic.mRoutes.remove(info)) {
1116            final RouteCategory removingCat = info.getCategory();
1117            final int count = sStatic.mRoutes.size();
1118            boolean found = false;
1119            for (int i = 0; i < count; i++) {
1120                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
1121                if (removingCat == cat) {
1122                    found = true;
1123                    break;
1124                }
1125            }
1126            if (info.isSelected()) {
1127                // Removing the currently selected route? Select the default before we remove it.
1128                selectDefaultRouteStatic();
1129            }
1130            if (!found) {
1131                sStatic.mCategories.remove(removingCat);
1132            }
1133            dispatchRouteRemoved(info);
1134        }
1135    }
1136
1137    /**
1138     * Return the number of {@link MediaRouter.RouteCategory categories} currently
1139     * represented by routes known to this MediaRouter.
1140     *
1141     * @return the number of unique categories represented by this MediaRouter's known routes
1142     */
1143    public int getCategoryCount() {
1144        return sStatic.mCategories.size();
1145    }
1146
1147    /**
1148     * Return the {@link MediaRouter.RouteCategory category} at the given index.
1149     * Valid indices are in the range [0-getCategoryCount).
1150     *
1151     * @param index which category to return
1152     * @return the category at index
1153     */
1154    public RouteCategory getCategoryAt(int index) {
1155        return sStatic.mCategories.get(index);
1156    }
1157
1158    /**
1159     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
1160     * to this MediaRouter.
1161     *
1162     * @return the number of routes tracked by this router
1163     */
1164    public int getRouteCount() {
1165        return sStatic.mRoutes.size();
1166    }
1167
1168    /**
1169     * Return the route at the specified index.
1170     *
1171     * @param index index of the route to return
1172     * @return the route at index
1173     */
1174    public RouteInfo getRouteAt(int index) {
1175        return sStatic.mRoutes.get(index);
1176    }
1177
1178    static int getRouteCountStatic() {
1179        return sStatic.mRoutes.size();
1180    }
1181
1182    static RouteInfo getRouteAtStatic(int index) {
1183        return sStatic.mRoutes.get(index);
1184    }
1185
1186    /**
1187     * Create a new user route that may be modified and registered for use by the application.
1188     *
1189     * @param category The category the new route will belong to
1190     * @return A new UserRouteInfo for use by the application
1191     *
1192     * @see #addUserRoute(UserRouteInfo)
1193     * @see #removeUserRoute(UserRouteInfo)
1194     * @see #createRouteCategory(CharSequence, boolean)
1195     */
1196    public UserRouteInfo createUserRoute(RouteCategory category) {
1197        return new UserRouteInfo(category);
1198    }
1199
1200    /**
1201     * Create a new route category. Each route must belong to a category.
1202     *
1203     * @param name Name of the new category
1204     * @param isGroupable true if routes in this category may be grouped with one another
1205     * @return the new RouteCategory
1206     */
1207    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
1208        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
1209    }
1210
1211    /**
1212     * Create a new route category. Each route must belong to a category.
1213     *
1214     * @param nameResId Resource ID of the name of the new category
1215     * @param isGroupable true if routes in this category may be grouped with one another
1216     * @return the new RouteCategory
1217     */
1218    public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
1219        return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
1220    }
1221
1222    /**
1223     * Rebinds the media router to handle routes that belong to the specified user.
1224     * Requires the interact across users permission to access the routes of another user.
1225     * <p>
1226     * This method is a complete hack to work around the singleton nature of the
1227     * media router when running inside of singleton processes like QuickSettings.
1228     * This mechanism should be burned to the ground when MediaRouter is redesigned.
1229     * Ideally the current user would be pulled from the Context but we need to break
1230     * down MediaRouter.Static before we can get there.
1231     * </p>
1232     *
1233     * @hide
1234     */
1235    public void rebindAsUser(int userId) {
1236        sStatic.rebindAsUser(userId);
1237    }
1238
1239    static void updateRoute(final RouteInfo info) {
1240        dispatchRouteChanged(info);
1241    }
1242
1243    static void dispatchRouteSelected(int type, RouteInfo info) {
1244        for (CallbackInfo cbi : sStatic.mCallbacks) {
1245            if (cbi.filterRouteEvent(info)) {
1246                cbi.cb.onRouteSelected(cbi.router, type, info);
1247            }
1248        }
1249    }
1250
1251    static void dispatchRouteUnselected(int type, RouteInfo info) {
1252        for (CallbackInfo cbi : sStatic.mCallbacks) {
1253            if (cbi.filterRouteEvent(info)) {
1254                cbi.cb.onRouteUnselected(cbi.router, type, info);
1255            }
1256        }
1257    }
1258
1259    static void dispatchRouteChanged(RouteInfo info) {
1260        dispatchRouteChanged(info, info.mSupportedTypes);
1261    }
1262
1263    static void dispatchRouteChanged(RouteInfo info, int oldSupportedTypes) {
1264        if (DEBUG) {
1265            Log.d(TAG, "Dispatching route change: " + info);
1266        }
1267        final int newSupportedTypes = info.mSupportedTypes;
1268        for (CallbackInfo cbi : sStatic.mCallbacks) {
1269            // Reconstruct some of the history for callbacks that may not have observed
1270            // all of the events needed to correctly interpret the current state.
1271            // FIXME: This is a strong signal that we should deprecate route type filtering
1272            // completely in the future because it can lead to inconsistencies in
1273            // applications.
1274            final boolean oldVisibility = cbi.filterRouteEvent(oldSupportedTypes);
1275            final boolean newVisibility = cbi.filterRouteEvent(newSupportedTypes);
1276            if (!oldVisibility && newVisibility) {
1277                cbi.cb.onRouteAdded(cbi.router, info);
1278                if (info.isSelected()) {
1279                    cbi.cb.onRouteSelected(cbi.router, newSupportedTypes, info);
1280                }
1281            }
1282            if (oldVisibility || newVisibility) {
1283                cbi.cb.onRouteChanged(cbi.router, info);
1284            }
1285            if (oldVisibility && !newVisibility) {
1286                if (info.isSelected()) {
1287                    cbi.cb.onRouteUnselected(cbi.router, oldSupportedTypes, info);
1288                }
1289                cbi.cb.onRouteRemoved(cbi.router, info);
1290            }
1291        }
1292    }
1293
1294    static void dispatchRouteAdded(RouteInfo info) {
1295        for (CallbackInfo cbi : sStatic.mCallbacks) {
1296            if (cbi.filterRouteEvent(info)) {
1297                cbi.cb.onRouteAdded(cbi.router, info);
1298            }
1299        }
1300    }
1301
1302    static void dispatchRouteRemoved(RouteInfo info) {
1303        for (CallbackInfo cbi : sStatic.mCallbacks) {
1304            if (cbi.filterRouteEvent(info)) {
1305                cbi.cb.onRouteRemoved(cbi.router, info);
1306            }
1307        }
1308    }
1309
1310    static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
1311        for (CallbackInfo cbi : sStatic.mCallbacks) {
1312            if (cbi.filterRouteEvent(group)) {
1313                cbi.cb.onRouteGrouped(cbi.router, info, group, index);
1314            }
1315        }
1316    }
1317
1318    static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
1319        for (CallbackInfo cbi : sStatic.mCallbacks) {
1320            if (cbi.filterRouteEvent(group)) {
1321                cbi.cb.onRouteUngrouped(cbi.router, info, group);
1322            }
1323        }
1324    }
1325
1326    static void dispatchRouteVolumeChanged(RouteInfo info) {
1327        for (CallbackInfo cbi : sStatic.mCallbacks) {
1328            if (cbi.filterRouteEvent(info)) {
1329                cbi.cb.onRouteVolumeChanged(cbi.router, info);
1330            }
1331        }
1332    }
1333
1334    static void dispatchRoutePresentationDisplayChanged(RouteInfo info) {
1335        for (CallbackInfo cbi : sStatic.mCallbacks) {
1336            if (cbi.filterRouteEvent(info)) {
1337                cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info);
1338            }
1339        }
1340    }
1341
1342    static void systemVolumeChanged(int newValue) {
1343        final RouteInfo selectedRoute = sStatic.mSelectedRoute;
1344        if (selectedRoute == null) return;
1345
1346        if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
1347                selectedRoute == sStatic.mDefaultAudioVideo) {
1348            dispatchRouteVolumeChanged(selectedRoute);
1349        } else if (sStatic.mBluetoothA2dpRoute != null) {
1350            try {
1351                dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
1352                        sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo);
1353            } catch (RemoteException e) {
1354                Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
1355            }
1356        } else {
1357            dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo);
1358        }
1359    }
1360
1361    static void updateWifiDisplayStatus(WifiDisplayStatus status) {
1362        WifiDisplay[] displays;
1363        WifiDisplay activeDisplay;
1364        if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
1365            displays = status.getDisplays();
1366            activeDisplay = status.getActiveDisplay();
1367
1368            // Only the system is able to connect to wifi display routes.
1369            // The display manager will enforce this with a permission check but it
1370            // still publishes information about all available displays.
1371            // Filter the list down to just the active display.
1372            if (!sStatic.mCanConfigureWifiDisplays) {
1373                if (activeDisplay != null) {
1374                    displays = new WifiDisplay[] { activeDisplay };
1375                } else {
1376                    displays = WifiDisplay.EMPTY_ARRAY;
1377                }
1378            }
1379        } else {
1380            displays = WifiDisplay.EMPTY_ARRAY;
1381            activeDisplay = null;
1382        }
1383        String activeDisplayAddress = activeDisplay != null ?
1384                activeDisplay.getDeviceAddress() : null;
1385
1386        // Add or update routes.
1387        for (int i = 0; i < displays.length; i++) {
1388            final WifiDisplay d = displays[i];
1389            if (shouldShowWifiDisplay(d, activeDisplay)) {
1390                RouteInfo route = findWifiDisplayRoute(d);
1391                if (route == null) {
1392                    route = makeWifiDisplayRoute(d, status);
1393                    addRouteStatic(route);
1394                } else {
1395                    String address = d.getDeviceAddress();
1396                    boolean disconnected = !address.equals(activeDisplayAddress)
1397                            && address.equals(sStatic.mPreviousActiveWifiDisplayAddress);
1398                    updateWifiDisplayRoute(route, d, status, disconnected);
1399                }
1400                if (d.equals(activeDisplay)) {
1401                    selectRouteStatic(route.getSupportedTypes(), route, false);
1402                }
1403            }
1404        }
1405
1406        // Remove stale routes.
1407        for (int i = sStatic.mRoutes.size(); i-- > 0; ) {
1408            RouteInfo route = sStatic.mRoutes.get(i);
1409            if (route.mDeviceAddress != null) {
1410                WifiDisplay d = findWifiDisplay(displays, route.mDeviceAddress);
1411                if (d == null || !shouldShowWifiDisplay(d, activeDisplay)) {
1412                    removeRouteStatic(route);
1413                }
1414            }
1415        }
1416
1417        // Remember the current active wifi display address so that we can infer disconnections.
1418        // TODO: This hack will go away once all of this is moved into the media router service.
1419        sStatic.mPreviousActiveWifiDisplayAddress = activeDisplayAddress;
1420    }
1421
1422    private static boolean shouldShowWifiDisplay(WifiDisplay d, WifiDisplay activeDisplay) {
1423        return d.isRemembered() || d.equals(activeDisplay);
1424    }
1425
1426    static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) {
1427        int newStatus;
1428        if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) {
1429            newStatus = RouteInfo.STATUS_SCANNING;
1430        } else if (d.isAvailable()) {
1431            newStatus = d.canConnect() ?
1432                    RouteInfo.STATUS_AVAILABLE: RouteInfo.STATUS_IN_USE;
1433        } else {
1434            newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
1435        }
1436
1437        if (d.equals(wfdStatus.getActiveDisplay())) {
1438            final int activeState = wfdStatus.getActiveDisplayState();
1439            switch (activeState) {
1440                case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
1441                    newStatus = RouteInfo.STATUS_CONNECTED;
1442                    break;
1443                case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
1444                    newStatus = RouteInfo.STATUS_CONNECTING;
1445                    break;
1446                case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
1447                    Log.e(TAG, "Active display is not connected!");
1448                    break;
1449            }
1450        }
1451
1452        return newStatus;
1453    }
1454
1455    static boolean isWifiDisplayEnabled(WifiDisplay d, WifiDisplayStatus wfdStatus) {
1456        return d.isAvailable() && (d.canConnect() || d.equals(wfdStatus.getActiveDisplay()));
1457    }
1458
1459    static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) {
1460        final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
1461        newRoute.mDeviceAddress = display.getDeviceAddress();
1462        newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
1463                | ROUTE_TYPE_REMOTE_DISPLAY;
1464        newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
1465        newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
1466
1467        newRoute.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
1468        newRoute.mEnabled = isWifiDisplayEnabled(display, wfdStatus);
1469        newRoute.mName = display.getFriendlyDisplayName();
1470        newRoute.mDescription = sStatic.mResources.getText(
1471                com.android.internal.R.string.wireless_display_route_description);
1472        newRoute.updatePresentationDisplay();
1473        newRoute.mDeviceType = RouteInfo.DEVICE_TYPE_TV;
1474        return newRoute;
1475    }
1476
1477    private static void updateWifiDisplayRoute(
1478            RouteInfo route, WifiDisplay display, WifiDisplayStatus wfdStatus,
1479            boolean disconnected) {
1480        boolean changed = false;
1481        final String newName = display.getFriendlyDisplayName();
1482        if (!route.getName().equals(newName)) {
1483            route.mName = newName;
1484            changed = true;
1485        }
1486
1487        boolean enabled = isWifiDisplayEnabled(display, wfdStatus);
1488        changed |= route.mEnabled != enabled;
1489        route.mEnabled = enabled;
1490
1491        changed |= route.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
1492
1493        if (changed) {
1494            dispatchRouteChanged(route);
1495        }
1496
1497        if ((!enabled || disconnected) && route.isSelected()) {
1498            // Oops, no longer available. Reselect the default.
1499            selectDefaultRouteStatic();
1500        }
1501    }
1502
1503    private static WifiDisplay findWifiDisplay(WifiDisplay[] displays, String deviceAddress) {
1504        for (int i = 0; i < displays.length; i++) {
1505            final WifiDisplay d = displays[i];
1506            if (d.getDeviceAddress().equals(deviceAddress)) {
1507                return d;
1508            }
1509        }
1510        return null;
1511    }
1512
1513    private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
1514        final int count = sStatic.mRoutes.size();
1515        for (int i = 0; i < count; i++) {
1516            final RouteInfo info = sStatic.mRoutes.get(i);
1517            if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
1518                return info;
1519            }
1520        }
1521        return null;
1522    }
1523
1524    /**
1525     * Information about a media route.
1526     */
1527    public static class RouteInfo {
1528        CharSequence mName;
1529        int mNameResId;
1530        CharSequence mDescription;
1531        private CharSequence mStatus;
1532        int mSupportedTypes;
1533        int mDeviceType;
1534        RouteGroup mGroup;
1535        final RouteCategory mCategory;
1536        Drawable mIcon;
1537        // playback information
1538        int mPlaybackType = PLAYBACK_TYPE_LOCAL;
1539        int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1540        int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1541        int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
1542        int mPlaybackStream = AudioManager.STREAM_MUSIC;
1543        VolumeCallbackInfo mVcb;
1544        Display mPresentationDisplay;
1545        int mPresentationDisplayId = -1;
1546
1547        String mDeviceAddress;
1548        boolean mEnabled = true;
1549
1550        // An id by which the route is known to the media router service.
1551        // Null if this route only exists as an artifact within this process.
1552        String mGlobalRouteId;
1553
1554        // A predetermined connection status that can override mStatus
1555        private int mRealStatusCode;
1556        private int mResolvedStatusCode;
1557
1558        /** @hide */ public static final int STATUS_NONE = 0;
1559        /** @hide */ public static final int STATUS_SCANNING = 1;
1560        /** @hide */ public static final int STATUS_CONNECTING = 2;
1561        /** @hide */ public static final int STATUS_AVAILABLE = 3;
1562        /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
1563        /** @hide */ public static final int STATUS_IN_USE = 5;
1564        /** @hide */ public static final int STATUS_CONNECTED = 6;
1565
1566        /** @hide */
1567        @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH})
1568        @Retention(RetentionPolicy.SOURCE)
1569        public @interface DeviceType {}
1570
1571        /**
1572         * The default receiver device type of the route indicating the type is unknown.
1573         *
1574         * @see #getDeviceType
1575         */
1576        public static final int DEVICE_TYPE_UNKNOWN = 0;
1577
1578        /**
1579         * A receiver device type of the route indicating the presentation of the media is happening
1580         * on a TV.
1581         *
1582         * @see #getDeviceType
1583         */
1584        public static final int DEVICE_TYPE_TV = 1;
1585
1586        /**
1587         * A receiver device type of the route indicating the presentation of the media is happening
1588         * on a speaker.
1589         *
1590         * @see #getDeviceType
1591         */
1592        public static final int DEVICE_TYPE_SPEAKER = 2;
1593
1594        /**
1595         * A receiver device type of the route indicating the presentation of the media is happening
1596         * on a bluetooth device such as a bluetooth speaker.
1597         *
1598         * @see #getDeviceType
1599         */
1600        public static final int DEVICE_TYPE_BLUETOOTH = 3;
1601
1602        private Object mTag;
1603
1604        /** @hide */
1605        @IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE})
1606        @Retention(RetentionPolicy.SOURCE)
1607        public @interface PlaybackType {}
1608
1609        /**
1610         * The default playback type, "local", indicating the presentation of the media is happening
1611         * on the same device (e&#46;g&#46; a phone, a tablet) as where it is controlled from.
1612         * @see #getPlaybackType()
1613         */
1614        public final static int PLAYBACK_TYPE_LOCAL = 0;
1615
1616        /**
1617         * A playback type indicating the presentation of the media is happening on
1618         * a different device (i&#46;e&#46; the remote device) than where it is controlled from.
1619         * @see #getPlaybackType()
1620         */
1621        public final static int PLAYBACK_TYPE_REMOTE = 1;
1622
1623        /** @hide */
1624         @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
1625         @Retention(RetentionPolicy.SOURCE)
1626         private @interface PlaybackVolume {}
1627
1628        /**
1629         * Playback information indicating the playback volume is fixed, i&#46;e&#46; it cannot be
1630         * controlled from this object. An example of fixed playback volume is a remote player,
1631         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
1632         * than attenuate at the source.
1633         * @see #getVolumeHandling()
1634         */
1635        public final static int PLAYBACK_VOLUME_FIXED = 0;
1636        /**
1637         * Playback information indicating the playback volume is variable and can be controlled
1638         * from this object.
1639         * @see #getVolumeHandling()
1640         */
1641        public final static int PLAYBACK_VOLUME_VARIABLE = 1;
1642
1643        RouteInfo(RouteCategory category) {
1644            mCategory = category;
1645            mDeviceType = DEVICE_TYPE_UNKNOWN;
1646        }
1647
1648        /**
1649         * Gets the user-visible name of the route.
1650         * <p>
1651         * The route name identifies the destination represented by the route.
1652         * It may be a user-supplied name, an alias, or device serial number.
1653         * </p>
1654         *
1655         * @return The user-visible name of a media route.  This is the string presented
1656         * to users who may select this as the active route.
1657         */
1658        public CharSequence getName() {
1659            return getName(sStatic.mResources);
1660        }
1661
1662        /**
1663         * Return the properly localized/resource user-visible name of this route.
1664         * <p>
1665         * The route name identifies the destination represented by the route.
1666         * It may be a user-supplied name, an alias, or device serial number.
1667         * </p>
1668         *
1669         * @param context Context used to resolve the correct configuration to load
1670         * @return The user-visible name of a media route.  This is the string presented
1671         * to users who may select this as the active route.
1672         */
1673        public CharSequence getName(Context context) {
1674            return getName(context.getResources());
1675        }
1676
1677        CharSequence getName(Resources res) {
1678            if (mNameResId != 0) {
1679                return res.getText(mNameResId);
1680            }
1681            return mName;
1682        }
1683
1684        /**
1685         * Gets the user-visible description of the route.
1686         * <p>
1687         * The route description describes the kind of destination represented by the route.
1688         * It may be a user-supplied string, a model number or brand of device.
1689         * </p>
1690         *
1691         * @return The description of the route, or null if none.
1692         */
1693        public CharSequence getDescription() {
1694            return mDescription;
1695        }
1696
1697        /**
1698         * @return The user-visible status for a media route. This may include a description
1699         * of the currently playing media, if available.
1700         */
1701        public CharSequence getStatus() {
1702            return mStatus;
1703        }
1704
1705        /**
1706         * Set this route's status by predetermined status code. If the caller
1707         * should dispatch a route changed event this call will return true;
1708         */
1709        boolean setRealStatusCode(int statusCode) {
1710            if (mRealStatusCode != statusCode) {
1711                mRealStatusCode = statusCode;
1712                return resolveStatusCode();
1713            }
1714            return false;
1715        }
1716
1717        /**
1718         * Resolves the status code whenever the real status code or selection state
1719         * changes.
1720         */
1721        boolean resolveStatusCode() {
1722            int statusCode = mRealStatusCode;
1723            if (isSelected()) {
1724                switch (statusCode) {
1725                    // If the route is selected and its status appears to be between states
1726                    // then report it as connecting even though it has not yet had a chance
1727                    // to officially move into the CONNECTING state.  Note that routes in
1728                    // the NONE state are assumed to not require an explicit connection
1729                    // lifecycle whereas those that are AVAILABLE are assumed to have
1730                    // to eventually proceed to CONNECTED.
1731                    case STATUS_AVAILABLE:
1732                    case STATUS_SCANNING:
1733                        statusCode = STATUS_CONNECTING;
1734                        break;
1735                }
1736            }
1737            if (mResolvedStatusCode == statusCode) {
1738                return false;
1739            }
1740
1741            mResolvedStatusCode = statusCode;
1742            int resId;
1743            switch (statusCode) {
1744                case STATUS_SCANNING:
1745                    resId = com.android.internal.R.string.media_route_status_scanning;
1746                    break;
1747                case STATUS_CONNECTING:
1748                    resId = com.android.internal.R.string.media_route_status_connecting;
1749                    break;
1750                case STATUS_AVAILABLE:
1751                    resId = com.android.internal.R.string.media_route_status_available;
1752                    break;
1753                case STATUS_NOT_AVAILABLE:
1754                    resId = com.android.internal.R.string.media_route_status_not_available;
1755                    break;
1756                case STATUS_IN_USE:
1757                    resId = com.android.internal.R.string.media_route_status_in_use;
1758                    break;
1759                case STATUS_CONNECTED:
1760                case STATUS_NONE:
1761                default:
1762                    resId = 0;
1763                    break;
1764            }
1765            mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
1766            return true;
1767        }
1768
1769        /**
1770         * @hide
1771         */
1772        public int getStatusCode() {
1773            return mResolvedStatusCode;
1774        }
1775
1776        /**
1777         * @return A media type flag set describing which types this route supports.
1778         */
1779        public int getSupportedTypes() {
1780            return mSupportedTypes;
1781        }
1782
1783        /**
1784         * Gets the type of the receiver device associated with this route.
1785         *
1786         * @return The type of the receiver device associated with this route:
1787         * {@link #DEVICE_TYPE_BLUETOOTH}, {@link #DEVICE_TYPE_TV}, {@link #DEVICE_TYPE_SPEAKER},
1788         * or {@link #DEVICE_TYPE_UNKNOWN}.
1789         */
1790        @DeviceType
1791        public int getDeviceType() {
1792            return mDeviceType;
1793        }
1794
1795        /** @hide */
1796        public boolean matchesTypes(int types) {
1797            return (mSupportedTypes & types) != 0;
1798        }
1799
1800        /**
1801         * @return The group that this route belongs to.
1802         */
1803        public RouteGroup getGroup() {
1804            return mGroup;
1805        }
1806
1807        /**
1808         * @return the category this route belongs to.
1809         */
1810        public RouteCategory getCategory() {
1811            return mCategory;
1812        }
1813
1814        /**
1815         * Get the icon representing this route.
1816         * This icon will be used in picker UIs if available.
1817         *
1818         * @return the icon representing this route or null if no icon is available
1819         */
1820        public Drawable getIconDrawable() {
1821            return mIcon;
1822        }
1823
1824        /**
1825         * Set an application-specific tag object for this route.
1826         * The application may use this to store arbitrary data associated with the
1827         * route for internal tracking.
1828         *
1829         * <p>Note that the lifespan of a route may be well past the lifespan of
1830         * an Activity or other Context; take care that objects you store here
1831         * will not keep more data in memory alive than you intend.</p>
1832         *
1833         * @param tag Arbitrary, app-specific data for this route to hold for later use
1834         */
1835        public void setTag(Object tag) {
1836            mTag = tag;
1837            routeUpdated();
1838        }
1839
1840        /**
1841         * @return The tag object previously set by the application
1842         * @see #setTag(Object)
1843         */
1844        public Object getTag() {
1845            return mTag;
1846        }
1847
1848        /**
1849         * @return the type of playback associated with this route
1850         * @see UserRouteInfo#setPlaybackType(int)
1851         */
1852        @PlaybackType
1853        public int getPlaybackType() {
1854            return mPlaybackType;
1855        }
1856
1857        /**
1858         * @return the stream over which the playback associated with this route is performed
1859         * @see UserRouteInfo#setPlaybackStream(int)
1860         */
1861        public int getPlaybackStream() {
1862            return mPlaybackStream;
1863        }
1864
1865        /**
1866         * Return the current volume for this route. Depending on the route, this may only
1867         * be valid if the route is currently selected.
1868         *
1869         * @return the volume at which the playback associated with this route is performed
1870         * @see UserRouteInfo#setVolume(int)
1871         */
1872        public int getVolume() {
1873            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1874                int vol = 0;
1875                try {
1876                    vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
1877                } catch (RemoteException e) {
1878                    Log.e(TAG, "Error getting local stream volume", e);
1879                }
1880                return vol;
1881            } else {
1882                return mVolume;
1883            }
1884        }
1885
1886        /**
1887         * Request a volume change for this route.
1888         * @param volume value between 0 and getVolumeMax
1889         */
1890        public void requestSetVolume(int volume) {
1891            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1892                try {
1893                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
1894                            ActivityThread.currentPackageName());
1895                } catch (RemoteException e) {
1896                    Log.e(TAG, "Error setting local stream volume", e);
1897                }
1898            } else {
1899                sStatic.requestSetVolume(this, volume);
1900            }
1901        }
1902
1903        /**
1904         * Request an incremental volume update for this route.
1905         * @param direction Delta to apply to the current volume
1906         */
1907        public void requestUpdateVolume(int direction) {
1908            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1909                try {
1910                    final int volume =
1911                            Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
1912                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
1913                            ActivityThread.currentPackageName());
1914                } catch (RemoteException e) {
1915                    Log.e(TAG, "Error setting local stream volume", e);
1916                }
1917            } else {
1918                sStatic.requestUpdateVolume(this, direction);
1919            }
1920        }
1921
1922        /**
1923         * @return the maximum volume at which the playback associated with this route is performed
1924         * @see UserRouteInfo#setVolumeMax(int)
1925         */
1926        public int getVolumeMax() {
1927            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1928                int volMax = 0;
1929                try {
1930                    volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
1931                } catch (RemoteException e) {
1932                    Log.e(TAG, "Error getting local stream volume", e);
1933                }
1934                return volMax;
1935            } else {
1936                return mVolumeMax;
1937            }
1938        }
1939
1940        /**
1941         * @return how volume is handling on the route
1942         * @see UserRouteInfo#setVolumeHandling(int)
1943         */
1944        @PlaybackVolume
1945        public int getVolumeHandling() {
1946            return mVolumeHandling;
1947        }
1948
1949        /**
1950         * Gets the {@link Display} that should be used by the application to show
1951         * a {@link android.app.Presentation} on an external display when this route is selected.
1952         * Depending on the route, this may only be valid if the route is currently
1953         * selected.
1954         * <p>
1955         * The preferred presentation display may change independently of the route
1956         * being selected or unselected.  For example, the presentation display
1957         * of the default system route may change when an external HDMI display is connected
1958         * or disconnected even though the route itself has not changed.
1959         * </p><p>
1960         * This method may return null if there is no external display associated with
1961         * the route or if the display is not ready to show UI yet.
1962         * </p><p>
1963         * The application should listen for changes to the presentation display
1964         * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
1965         * show or dismiss its {@link android.app.Presentation} accordingly when the display
1966         * becomes available or is removed.
1967         * </p><p>
1968         * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes.
1969         * </p>
1970         *
1971         * @return The preferred presentation display to use when this route is
1972         * selected or null if none.
1973         *
1974         * @see #ROUTE_TYPE_LIVE_VIDEO
1975         * @see android.app.Presentation
1976         */
1977        public Display getPresentationDisplay() {
1978            return mPresentationDisplay;
1979        }
1980
1981        boolean updatePresentationDisplay() {
1982            Display display = choosePresentationDisplay();
1983            if (mPresentationDisplay != display) {
1984                mPresentationDisplay = display;
1985                return true;
1986            }
1987            return false;
1988        }
1989
1990        private Display choosePresentationDisplay() {
1991            if ((mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
1992                Display[] displays = sStatic.getAllPresentationDisplays();
1993
1994                // Ensure that the specified display is valid for presentations.
1995                // This check will normally disallow the default display unless it was
1996                // configured as a presentation display for some reason.
1997                if (mPresentationDisplayId >= 0) {
1998                    for (Display display : displays) {
1999                        if (display.getDisplayId() == mPresentationDisplayId) {
2000                            return display;
2001                        }
2002                    }
2003                    return null;
2004                }
2005
2006                // Find the indicated Wifi display by its address.
2007                if (mDeviceAddress != null) {
2008                    for (Display display : displays) {
2009                        if (display.getType() == Display.TYPE_WIFI
2010                                && mDeviceAddress.equals(display.getAddress())) {
2011                            return display;
2012                        }
2013                    }
2014                    return null;
2015                }
2016
2017                // For the default route, choose the first presentation display from the list.
2018                if (this == sStatic.mDefaultAudioVideo && displays.length > 0) {
2019                    return displays[0];
2020                }
2021            }
2022            return null;
2023        }
2024
2025        /** @hide */
2026        public String getDeviceAddress() {
2027            return mDeviceAddress;
2028        }
2029
2030        /**
2031         * Returns true if this route is enabled and may be selected.
2032         *
2033         * @return True if this route is enabled.
2034         */
2035        public boolean isEnabled() {
2036            return mEnabled;
2037        }
2038
2039        /**
2040         * Returns true if the route is in the process of connecting and is not
2041         * yet ready for use.
2042         *
2043         * @return True if this route is in the process of connecting.
2044         */
2045        public boolean isConnecting() {
2046            return mResolvedStatusCode == STATUS_CONNECTING;
2047        }
2048
2049        /** @hide */
2050        public boolean isSelected() {
2051            return this == sStatic.mSelectedRoute;
2052        }
2053
2054        /** @hide */
2055        public boolean isDefault() {
2056            return this == sStatic.mDefaultAudioVideo;
2057        }
2058
2059        /** @hide */
2060        public boolean isBluetooth() {
2061            return this == sStatic.mBluetoothA2dpRoute;
2062        }
2063
2064        /** @hide */
2065        public void select() {
2066            selectRouteStatic(mSupportedTypes, this, true);
2067        }
2068
2069        void setStatusInt(CharSequence status) {
2070            if (!status.equals(mStatus)) {
2071                mStatus = status;
2072                if (mGroup != null) {
2073                    mGroup.memberStatusChanged(this, status);
2074                }
2075                routeUpdated();
2076            }
2077        }
2078
2079        final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
2080            @Override
2081            public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
2082                sStatic.mHandler.post(new Runnable() {
2083                    @Override
2084                    public void run() {
2085                        if (mVcb != null) {
2086                            if (direction != 0) {
2087                                mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
2088                            } else {
2089                                mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
2090                            }
2091                        }
2092                    }
2093                });
2094            }
2095        };
2096
2097        void routeUpdated() {
2098            updateRoute(this);
2099        }
2100
2101        @Override
2102        public String toString() {
2103            String supportedTypes = typesToString(getSupportedTypes());
2104            return getClass().getSimpleName() + "{ name=" + getName() +
2105                    ", description=" + getDescription() +
2106                    ", status=" + getStatus() +
2107                    ", category=" + getCategory() +
2108                    ", supportedTypes=" + supportedTypes +
2109                    ", presentationDisplay=" + mPresentationDisplay + " }";
2110        }
2111    }
2112
2113    /**
2114     * Information about a route that the application may define and modify.
2115     * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
2116     * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
2117     *
2118     * @see MediaRouter.RouteInfo
2119     */
2120    public static class UserRouteInfo extends RouteInfo {
2121        RemoteControlClient mRcc;
2122        SessionVolumeProvider mSvp;
2123
2124        UserRouteInfo(RouteCategory category) {
2125            super(category);
2126            mSupportedTypes = ROUTE_TYPE_USER;
2127            mPlaybackType = PLAYBACK_TYPE_REMOTE;
2128            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
2129        }
2130
2131        /**
2132         * Set the user-visible name of this route.
2133         * @param name Name to display to the user to describe this route
2134         */
2135        public void setName(CharSequence name) {
2136            mNameResId = 0;
2137            mName = name;
2138            routeUpdated();
2139        }
2140
2141        /**
2142         * Set the user-visible name of this route.
2143         * <p>
2144         * The route name identifies the destination represented by the route.
2145         * It may be a user-supplied name, an alias, or device serial number.
2146         * </p>
2147         *
2148         * @param resId Resource ID of the name to display to the user to describe this route
2149         */
2150        public void setName(int resId) {
2151            mNameResId = resId;
2152            mName = null;
2153            routeUpdated();
2154        }
2155
2156        /**
2157         * Set the user-visible description of this route.
2158         * <p>
2159         * The route description describes the kind of destination represented by the route.
2160         * It may be a user-supplied string, a model number or brand of device.
2161         * </p>
2162         *
2163         * @param description The description of the route, or null if none.
2164         */
2165        public void setDescription(CharSequence description) {
2166            mDescription = description;
2167            routeUpdated();
2168        }
2169
2170        /**
2171         * Set the current user-visible status for this route.
2172         * @param status Status to display to the user to describe what the endpoint
2173         * of this route is currently doing
2174         */
2175        public void setStatus(CharSequence status) {
2176            setStatusInt(status);
2177        }
2178
2179        /**
2180         * Set the RemoteControlClient responsible for reporting playback info for this
2181         * user route.
2182         *
2183         * <p>If this route manages remote playback, the data exposed by this
2184         * RemoteControlClient will be used to reflect and update information
2185         * such as route volume info in related UIs.</p>
2186         *
2187         * <p>The RemoteControlClient must have been previously registered with
2188         * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
2189         *
2190         * @param rcc RemoteControlClient associated with this route
2191         */
2192        public void setRemoteControlClient(RemoteControlClient rcc) {
2193            mRcc = rcc;
2194            updatePlaybackInfoOnRcc();
2195        }
2196
2197        /**
2198         * Retrieve the RemoteControlClient associated with this route, if one has been set.
2199         *
2200         * @return the RemoteControlClient associated with this route
2201         * @see #setRemoteControlClient(RemoteControlClient)
2202         */
2203        public RemoteControlClient getRemoteControlClient() {
2204            return mRcc;
2205        }
2206
2207        /**
2208         * Set an icon that will be used to represent this route.
2209         * The system may use this icon in picker UIs or similar.
2210         *
2211         * @param icon icon drawable to use to represent this route
2212         */
2213        public void setIconDrawable(Drawable icon) {
2214            mIcon = icon;
2215        }
2216
2217        /**
2218         * Set an icon that will be used to represent this route.
2219         * The system may use this icon in picker UIs or similar.
2220         *
2221         * @param resId Resource ID of an icon drawable to use to represent this route
2222         */
2223        public void setIconResource(@DrawableRes int resId) {
2224            setIconDrawable(sStatic.mResources.getDrawable(resId));
2225        }
2226
2227        /**
2228         * Set a callback to be notified of volume update requests
2229         * @param vcb
2230         */
2231        public void setVolumeCallback(VolumeCallback vcb) {
2232            mVcb = new VolumeCallbackInfo(vcb, this);
2233        }
2234
2235        /**
2236         * Defines whether playback associated with this route is "local"
2237         *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
2238         *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
2239         * @param type
2240         */
2241        public void setPlaybackType(@RouteInfo.PlaybackType int type) {
2242            if (mPlaybackType != type) {
2243                mPlaybackType = type;
2244                configureSessionVolume();
2245            }
2246        }
2247
2248        /**
2249         * Defines whether volume for the playback associated with this route is fixed
2250         * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
2251         * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
2252         * @param volumeHandling
2253         */
2254        public void setVolumeHandling(@RouteInfo.PlaybackVolume int volumeHandling) {
2255            if (mVolumeHandling != volumeHandling) {
2256                mVolumeHandling = volumeHandling;
2257                configureSessionVolume();
2258            }
2259        }
2260
2261        /**
2262         * Defines at what volume the playback associated with this route is performed (for user
2263         * feedback purposes). This information is only used when the playback is not local.
2264         * @param volume
2265         */
2266        public void setVolume(int volume) {
2267            volume = Math.max(0, Math.min(volume, getVolumeMax()));
2268            if (mVolume != volume) {
2269                mVolume = volume;
2270                if (mSvp != null) {
2271                    mSvp.setCurrentVolume(mVolume);
2272                }
2273                dispatchRouteVolumeChanged(this);
2274                if (mGroup != null) {
2275                    mGroup.memberVolumeChanged(this);
2276                }
2277            }
2278        }
2279
2280        @Override
2281        public void requestSetVolume(int volume) {
2282            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
2283                if (mVcb == null) {
2284                    Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
2285                    return;
2286                }
2287                mVcb.vcb.onVolumeSetRequest(this, volume);
2288            }
2289        }
2290
2291        @Override
2292        public void requestUpdateVolume(int direction) {
2293            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
2294                if (mVcb == null) {
2295                    Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
2296                    return;
2297                }
2298                mVcb.vcb.onVolumeUpdateRequest(this, direction);
2299            }
2300        }
2301
2302        /**
2303         * Defines the maximum volume at which the playback associated with this route is performed
2304         * (for user feedback purposes). This information is only used when the playback is not
2305         * local.
2306         * @param volumeMax
2307         */
2308        public void setVolumeMax(int volumeMax) {
2309            if (mVolumeMax != volumeMax) {
2310                mVolumeMax = volumeMax;
2311                configureSessionVolume();
2312            }
2313        }
2314
2315        /**
2316         * Defines over what stream type the media is presented.
2317         * @param stream
2318         */
2319        public void setPlaybackStream(int stream) {
2320            if (mPlaybackStream != stream) {
2321                mPlaybackStream = stream;
2322                configureSessionVolume();
2323            }
2324        }
2325
2326        private void updatePlaybackInfoOnRcc() {
2327            configureSessionVolume();
2328        }
2329
2330        private void configureSessionVolume() {
2331            if (mRcc == null) {
2332                if (DEBUG) {
2333                    Log.d(TAG, "No Rcc to configure volume for route " + getName());
2334                }
2335                return;
2336            }
2337            MediaSession session = mRcc.getMediaSession();
2338            if (session == null) {
2339                if (DEBUG) {
2340                    Log.d(TAG, "Rcc has no session to configure volume");
2341                }
2342                return;
2343            }
2344            if (mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
2345                @VolumeProvider.ControlType int volumeControl =
2346                        VolumeProvider.VOLUME_CONTROL_FIXED;
2347                switch (mVolumeHandling) {
2348                    case RemoteControlClient.PLAYBACK_VOLUME_VARIABLE:
2349                        volumeControl = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
2350                        break;
2351                    case RemoteControlClient.PLAYBACK_VOLUME_FIXED:
2352                    default:
2353                        break;
2354                }
2355                // Only register a new listener if necessary
2356                if (mSvp == null || mSvp.getVolumeControl() != volumeControl
2357                        || mSvp.getMaxVolume() != mVolumeMax) {
2358                    mSvp = new SessionVolumeProvider(volumeControl, mVolumeMax, mVolume);
2359                    session.setPlaybackToRemote(mSvp);
2360                }
2361            } else {
2362                // We only know how to handle local and remote, fall back to local if not remote.
2363                AudioAttributes.Builder bob = new AudioAttributes.Builder();
2364                bob.setLegacyStreamType(mPlaybackStream);
2365                session.setPlaybackToLocal(bob.build());
2366                mSvp = null;
2367            }
2368        }
2369
2370        class SessionVolumeProvider extends VolumeProvider {
2371
2372            public SessionVolumeProvider(@VolumeProvider.ControlType int volumeControl,
2373                    int maxVolume, int currentVolume) {
2374                super(volumeControl, maxVolume, currentVolume);
2375            }
2376
2377            @Override
2378            public void onSetVolumeTo(final int volume) {
2379                sStatic.mHandler.post(new Runnable() {
2380                    @Override
2381                    public void run() {
2382                        if (mVcb != null) {
2383                            mVcb.vcb.onVolumeSetRequest(mVcb.route, volume);
2384                        }
2385                    }
2386                });
2387            }
2388
2389            @Override
2390            public void onAdjustVolume(final int direction) {
2391                sStatic.mHandler.post(new Runnable() {
2392                    @Override
2393                    public void run() {
2394                        if (mVcb != null) {
2395                            mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
2396                        }
2397                    }
2398                });
2399            }
2400        }
2401    }
2402
2403    /**
2404     * Information about a route that consists of multiple other routes in a group.
2405     */
2406    public static class RouteGroup extends RouteInfo {
2407        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
2408        private boolean mUpdateName;
2409
2410        RouteGroup(RouteCategory category) {
2411            super(category);
2412            mGroup = this;
2413            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
2414        }
2415
2416        @Override
2417        CharSequence getName(Resources res) {
2418            if (mUpdateName) updateName();
2419            return super.getName(res);
2420        }
2421
2422        /**
2423         * Add a route to this group. The route must not currently belong to another group.
2424         *
2425         * @param route route to add to this group
2426         */
2427        public void addRoute(RouteInfo route) {
2428            if (route.getGroup() != null) {
2429                throw new IllegalStateException("Route " + route + " is already part of a group.");
2430            }
2431            if (route.getCategory() != mCategory) {
2432                throw new IllegalArgumentException(
2433                        "Route cannot be added to a group with a different category. " +
2434                            "(Route category=" + route.getCategory() +
2435                            " group category=" + mCategory + ")");
2436            }
2437            final int at = mRoutes.size();
2438            mRoutes.add(route);
2439            route.mGroup = this;
2440            mUpdateName = true;
2441            updateVolume();
2442            routeUpdated();
2443            dispatchRouteGrouped(route, this, at);
2444        }
2445
2446        /**
2447         * Add a route to this group before the specified index.
2448         *
2449         * @param route route to add
2450         * @param insertAt insert the new route before this index
2451         */
2452        public void addRoute(RouteInfo route, int insertAt) {
2453            if (route.getGroup() != null) {
2454                throw new IllegalStateException("Route " + route + " is already part of a group.");
2455            }
2456            if (route.getCategory() != mCategory) {
2457                throw new IllegalArgumentException(
2458                        "Route cannot be added to a group with a different category. " +
2459                            "(Route category=" + route.getCategory() +
2460                            " group category=" + mCategory + ")");
2461            }
2462            mRoutes.add(insertAt, route);
2463            route.mGroup = this;
2464            mUpdateName = true;
2465            updateVolume();
2466            routeUpdated();
2467            dispatchRouteGrouped(route, this, insertAt);
2468        }
2469
2470        /**
2471         * Remove a route from this group.
2472         *
2473         * @param route route to remove
2474         */
2475        public void removeRoute(RouteInfo route) {
2476            if (route.getGroup() != this) {
2477                throw new IllegalArgumentException("Route " + route +
2478                        " is not a member of this group.");
2479            }
2480            mRoutes.remove(route);
2481            route.mGroup = null;
2482            mUpdateName = true;
2483            updateVolume();
2484            dispatchRouteUngrouped(route, this);
2485            routeUpdated();
2486        }
2487
2488        /**
2489         * Remove the route at the specified index from this group.
2490         *
2491         * @param index index of the route to remove
2492         */
2493        public void removeRoute(int index) {
2494            RouteInfo route = mRoutes.remove(index);
2495            route.mGroup = null;
2496            mUpdateName = true;
2497            updateVolume();
2498            dispatchRouteUngrouped(route, this);
2499            routeUpdated();
2500        }
2501
2502        /**
2503         * @return The number of routes in this group
2504         */
2505        public int getRouteCount() {
2506            return mRoutes.size();
2507        }
2508
2509        /**
2510         * Return the route in this group at the specified index
2511         *
2512         * @param index Index to fetch
2513         * @return The route at index
2514         */
2515        public RouteInfo getRouteAt(int index) {
2516            return mRoutes.get(index);
2517        }
2518
2519        /**
2520         * Set an icon that will be used to represent this group.
2521         * The system may use this icon in picker UIs or similar.
2522         *
2523         * @param icon icon drawable to use to represent this group
2524         */
2525        public void setIconDrawable(Drawable icon) {
2526            mIcon = icon;
2527        }
2528
2529        /**
2530         * Set an icon that will be used to represent this group.
2531         * The system may use this icon in picker UIs or similar.
2532         *
2533         * @param resId Resource ID of an icon drawable to use to represent this group
2534         */
2535        public void setIconResource(@DrawableRes int resId) {
2536            setIconDrawable(sStatic.mResources.getDrawable(resId));
2537        }
2538
2539        @Override
2540        public void requestSetVolume(int volume) {
2541            final int maxVol = getVolumeMax();
2542            if (maxVol == 0) {
2543                return;
2544            }
2545
2546            final float scaledVolume = (float) volume / maxVol;
2547            final int routeCount = getRouteCount();
2548            for (int i = 0; i < routeCount; i++) {
2549                final RouteInfo route = getRouteAt(i);
2550                final int routeVol = (int) (scaledVolume * route.getVolumeMax());
2551                route.requestSetVolume(routeVol);
2552            }
2553            if (volume != mVolume) {
2554                mVolume = volume;
2555                dispatchRouteVolumeChanged(this);
2556            }
2557        }
2558
2559        @Override
2560        public void requestUpdateVolume(int direction) {
2561            final int maxVol = getVolumeMax();
2562            if (maxVol == 0) {
2563                return;
2564            }
2565
2566            final int routeCount = getRouteCount();
2567            int volume = 0;
2568            for (int i = 0; i < routeCount; i++) {
2569                final RouteInfo route = getRouteAt(i);
2570                route.requestUpdateVolume(direction);
2571                final int routeVol = route.getVolume();
2572                if (routeVol > volume) {
2573                    volume = routeVol;
2574                }
2575            }
2576            if (volume != mVolume) {
2577                mVolume = volume;
2578                dispatchRouteVolumeChanged(this);
2579            }
2580        }
2581
2582        void memberNameChanged(RouteInfo info, CharSequence name) {
2583            mUpdateName = true;
2584            routeUpdated();
2585        }
2586
2587        void memberStatusChanged(RouteInfo info, CharSequence status) {
2588            setStatusInt(status);
2589        }
2590
2591        void memberVolumeChanged(RouteInfo info) {
2592            updateVolume();
2593        }
2594
2595        void updateVolume() {
2596            // A group always represents the highest component volume value.
2597            final int routeCount = getRouteCount();
2598            int volume = 0;
2599            for (int i = 0; i < routeCount; i++) {
2600                final int routeVol = getRouteAt(i).getVolume();
2601                if (routeVol > volume) {
2602                    volume = routeVol;
2603                }
2604            }
2605            if (volume != mVolume) {
2606                mVolume = volume;
2607                dispatchRouteVolumeChanged(this);
2608            }
2609        }
2610
2611        @Override
2612        void routeUpdated() {
2613            int types = 0;
2614            final int count = mRoutes.size();
2615            if (count == 0) {
2616                // Don't keep empty groups in the router.
2617                MediaRouter.removeRouteStatic(this);
2618                return;
2619            }
2620
2621            int maxVolume = 0;
2622            boolean isLocal = true;
2623            boolean isFixedVolume = true;
2624            for (int i = 0; i < count; i++) {
2625                final RouteInfo route = mRoutes.get(i);
2626                types |= route.mSupportedTypes;
2627                final int routeMaxVolume = route.getVolumeMax();
2628                if (routeMaxVolume > maxVolume) {
2629                    maxVolume = routeMaxVolume;
2630                }
2631                isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
2632                isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
2633            }
2634            mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
2635            mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
2636            mSupportedTypes = types;
2637            mVolumeMax = maxVolume;
2638            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
2639            super.routeUpdated();
2640        }
2641
2642        void updateName() {
2643            final StringBuilder sb = new StringBuilder();
2644            final int count = mRoutes.size();
2645            for (int i = 0; i < count; i++) {
2646                final RouteInfo info = mRoutes.get(i);
2647                // TODO: There's probably a much more correct way to localize this.
2648                if (i > 0) {
2649                    sb.append(", ");
2650                }
2651                sb.append(info.getName());
2652            }
2653            mName = sb.toString();
2654            mUpdateName = false;
2655        }
2656
2657        @Override
2658        public String toString() {
2659            StringBuilder sb = new StringBuilder(super.toString());
2660            sb.append('[');
2661            final int count = mRoutes.size();
2662            for (int i = 0; i < count; i++) {
2663                if (i > 0) sb.append(", ");
2664                sb.append(mRoutes.get(i));
2665            }
2666            sb.append(']');
2667            return sb.toString();
2668        }
2669    }
2670
2671    /**
2672     * Definition of a category of routes. All routes belong to a category.
2673     */
2674    public static class RouteCategory {
2675        CharSequence mName;
2676        int mNameResId;
2677        int mTypes;
2678        final boolean mGroupable;
2679        boolean mIsSystem;
2680
2681        RouteCategory(CharSequence name, int types, boolean groupable) {
2682            mName = name;
2683            mTypes = types;
2684            mGroupable = groupable;
2685        }
2686
2687        RouteCategory(int nameResId, int types, boolean groupable) {
2688            mNameResId = nameResId;
2689            mTypes = types;
2690            mGroupable = groupable;
2691        }
2692
2693        /**
2694         * @return the name of this route category
2695         */
2696        public CharSequence getName() {
2697            return getName(sStatic.mResources);
2698        }
2699
2700        /**
2701         * Return the properly localized/configuration dependent name of this RouteCategory.
2702         *
2703         * @param context Context to resolve name resources
2704         * @return the name of this route category
2705         */
2706        public CharSequence getName(Context context) {
2707            return getName(context.getResources());
2708        }
2709
2710        CharSequence getName(Resources res) {
2711            if (mNameResId != 0) {
2712                return res.getText(mNameResId);
2713            }
2714            return mName;
2715        }
2716
2717        /**
2718         * Return the current list of routes in this category that have been added
2719         * to the MediaRouter.
2720         *
2721         * <p>This list will not include routes that are nested within RouteGroups.
2722         * A RouteGroup is treated as a single route within its category.</p>
2723         *
2724         * @param out a List to fill with the routes in this category. If this parameter is
2725         *            non-null, it will be cleared, filled with the current routes with this
2726         *            category, and returned. If this parameter is null, a new List will be
2727         *            allocated to report the category's current routes.
2728         * @return A list with the routes in this category that have been added to the MediaRouter.
2729         */
2730        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
2731            if (out == null) {
2732                out = new ArrayList<RouteInfo>();
2733            } else {
2734                out.clear();
2735            }
2736
2737            final int count = getRouteCountStatic();
2738            for (int i = 0; i < count; i++) {
2739                final RouteInfo route = getRouteAtStatic(i);
2740                if (route.mCategory == this) {
2741                    out.add(route);
2742                }
2743            }
2744            return out;
2745        }
2746
2747        /**
2748         * @return Flag set describing the route types supported by this category
2749         */
2750        public int getSupportedTypes() {
2751            return mTypes;
2752        }
2753
2754        /**
2755         * Return whether or not this category supports grouping.
2756         *
2757         * <p>If this method returns true, all routes obtained from this category
2758         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
2759         *
2760         * @return true if this category supports
2761         */
2762        public boolean isGroupable() {
2763            return mGroupable;
2764        }
2765
2766        /**
2767         * @return true if this is the category reserved for system routes.
2768         * @hide
2769         */
2770        public boolean isSystem() {
2771            return mIsSystem;
2772        }
2773
2774        @Override
2775        public String toString() {
2776            return "RouteCategory{ name=" + getName() + " types=" + typesToString(mTypes) +
2777                    " groupable=" + mGroupable + " }";
2778        }
2779    }
2780
2781    static class CallbackInfo {
2782        public int type;
2783        public int flags;
2784        public final Callback cb;
2785        public final MediaRouter router;
2786
2787        public CallbackInfo(Callback cb, int type, int flags, MediaRouter router) {
2788            this.cb = cb;
2789            this.type = type;
2790            this.flags = flags;
2791            this.router = router;
2792        }
2793
2794        public boolean filterRouteEvent(RouteInfo route) {
2795            return filterRouteEvent(route.mSupportedTypes);
2796        }
2797
2798        public boolean filterRouteEvent(int supportedTypes) {
2799            return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
2800                    || (type & supportedTypes) != 0;
2801        }
2802    }
2803
2804    /**
2805     * Interface for receiving events about media routing changes.
2806     * All methods of this interface will be called from the application's main thread.
2807     * <p>
2808     * A Callback will only receive events relevant to routes that the callback
2809     * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
2810     * flag was specified in {@link MediaRouter#addCallback(int, Callback, int)}.
2811     * </p>
2812     *
2813     * @see MediaRouter#addCallback(int, Callback, int)
2814     * @see MediaRouter#removeCallback(Callback)
2815     */
2816    public static abstract class Callback {
2817        /**
2818         * Called when the supplied route becomes selected as the active route
2819         * for the given route type.
2820         *
2821         * @param router the MediaRouter reporting the event
2822         * @param type Type flag set indicating the routes that have been selected
2823         * @param info Route that has been selected for the given route types
2824         */
2825        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
2826
2827        /**
2828         * Called when the supplied route becomes unselected as the active route
2829         * for the given route type.
2830         *
2831         * @param router the MediaRouter reporting the event
2832         * @param type Type flag set indicating the routes that have been unselected
2833         * @param info Route that has been unselected for the given route types
2834         */
2835        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
2836
2837        /**
2838         * Called when a route for the specified type was added.
2839         *
2840         * @param router the MediaRouter reporting the event
2841         * @param info Route that has become available for use
2842         */
2843        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
2844
2845        /**
2846         * Called when a route for the specified type was removed.
2847         *
2848         * @param router the MediaRouter reporting the event
2849         * @param info Route that has been removed from availability
2850         */
2851        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
2852
2853        /**
2854         * Called when an aspect of the indicated route has changed.
2855         *
2856         * <p>This will not indicate that the types supported by this route have
2857         * changed, only that cosmetic info such as name or status have been updated.</p>
2858         *
2859         * @param router the MediaRouter reporting the event
2860         * @param info The route that was changed
2861         */
2862        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
2863
2864        /**
2865         * Called when a route is added to a group.
2866         *
2867         * @param router the MediaRouter reporting the event
2868         * @param info The route that was added
2869         * @param group The group the route was added to
2870         * @param index The route index within group that info was added at
2871         */
2872        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2873                int index);
2874
2875        /**
2876         * Called when a route is removed from a group.
2877         *
2878         * @param router the MediaRouter reporting the event
2879         * @param info The route that was removed
2880         * @param group The group the route was removed from
2881         */
2882        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
2883
2884        /**
2885         * Called when a route's volume changes.
2886         *
2887         * @param router the MediaRouter reporting the event
2888         * @param info The route with altered volume
2889         */
2890        public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
2891
2892        /**
2893         * Called when a route's presentation display changes.
2894         * <p>
2895         * This method is called whenever the route's presentation display becomes
2896         * available, is removes or has changes to some of its properties (such as its size).
2897         * </p>
2898         *
2899         * @param router the MediaRouter reporting the event
2900         * @param info The route whose presentation display changed
2901         *
2902         * @see RouteInfo#getPresentationDisplay()
2903         */
2904        public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
2905        }
2906    }
2907
2908    /**
2909     * Stub implementation of {@link MediaRouter.Callback}.
2910     * Each abstract method is defined as a no-op. Override just the ones
2911     * you need.
2912     */
2913    public static class SimpleCallback extends Callback {
2914
2915        @Override
2916        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
2917        }
2918
2919        @Override
2920        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
2921        }
2922
2923        @Override
2924        public void onRouteAdded(MediaRouter router, RouteInfo info) {
2925        }
2926
2927        @Override
2928        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
2929        }
2930
2931        @Override
2932        public void onRouteChanged(MediaRouter router, RouteInfo info) {
2933        }
2934
2935        @Override
2936        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2937                int index) {
2938        }
2939
2940        @Override
2941        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
2942        }
2943
2944        @Override
2945        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
2946        }
2947    }
2948
2949    static class VolumeCallbackInfo {
2950        public final VolumeCallback vcb;
2951        public final RouteInfo route;
2952
2953        public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
2954            this.vcb = vcb;
2955            this.route = route;
2956        }
2957    }
2958
2959    /**
2960     * Interface for receiving events about volume changes.
2961     * All methods of this interface will be called from the application's main thread.
2962     *
2963     * <p>A VolumeCallback will only receive events relevant to routes that the callback
2964     * was registered for.</p>
2965     *
2966     * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
2967     */
2968    public static abstract class VolumeCallback {
2969        /**
2970         * Called when the volume for the route should be increased or decreased.
2971         * @param info the route affected by this event
2972         * @param direction an integer indicating whether the volume is to be increased
2973         *     (positive value) or decreased (negative value).
2974         *     For bundled changes, the absolute value indicates the number of changes
2975         *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
2976         */
2977        public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
2978        /**
2979         * Called when the volume for the route should be set to the given value
2980         * @param info the route affected by this event
2981         * @param volume an integer indicating the new volume value that should be used, always
2982         *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
2983         */
2984        public abstract void onVolumeSetRequest(RouteInfo info, int volume);
2985    }
2986
2987    static class VolumeChangeReceiver extends BroadcastReceiver {
2988        @Override
2989        public void onReceive(Context context, Intent intent) {
2990            if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
2991                final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
2992                        -1);
2993                if (streamType != AudioManager.STREAM_MUSIC) {
2994                    return;
2995                }
2996
2997                final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
2998                final int oldVolume = intent.getIntExtra(
2999                        AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
3000                if (newVolume != oldVolume) {
3001                    systemVolumeChanged(newVolume);
3002                }
3003            }
3004        }
3005    }
3006
3007    static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
3008        @Override
3009        public void onReceive(Context context, Intent intent) {
3010            if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
3011                updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
3012                        DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
3013            }
3014        }
3015    }
3016}
3017