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