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