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