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