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