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