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