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