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