MediaRouter.java revision a27b8fb4296c1271ddf5916843ddffda6764e65f
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.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.res.Resources;
24import android.graphics.drawable.Drawable;
25import android.hardware.display.DisplayManager;
26import android.hardware.display.WifiDisplay;
27import android.hardware.display.WifiDisplayStatus;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.text.TextUtils;
33import android.util.Log;
34import android.view.Display;
35
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.List;
39import java.util.concurrent.CopyOnWriteArrayList;
40
41/**
42 * MediaRouter allows applications to control the routing of media channels
43 * and streams from the current device to external speakers and destination devices.
44 *
45 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
46 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
47 * Context.MEDIA_ROUTER_SERVICE}.
48 *
49 * <p>The media router API is not thread-safe; all interactions with it must be
50 * done from the main thread of the process.</p>
51 */
52public class MediaRouter {
53    private static final String TAG = "MediaRouter";
54
55    static class Static implements DisplayManager.DisplayListener {
56        // Time between wifi display scans when actively scanning in milliseconds.
57        private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
58
59        final Resources mResources;
60        final IAudioService mAudioService;
61        final DisplayManager mDisplayService;
62        final Handler mHandler;
63        final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
64                new CopyOnWriteArrayList<CallbackInfo>();
65
66        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
67        final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
68
69        final RouteCategory mSystemCategory;
70
71        final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
72
73        RouteInfo mDefaultAudioVideo;
74        RouteInfo mBluetoothA2dpRoute;
75
76        RouteInfo mSelectedRoute;
77
78        WifiDisplayStatus mLastKnownWifiDisplayStatus;
79        boolean mActivelyScanningWifiDisplays;
80
81        final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
82            @Override
83            public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
84                mHandler.post(new Runnable() {
85                    @Override public void run() {
86                        updateAudioRoutes(newRoutes);
87                    }
88                });
89            }
90        };
91
92        final Runnable mScanWifiDisplays = new Runnable() {
93            @Override
94            public void run() {
95                if (mActivelyScanningWifiDisplays) {
96                    mDisplayService.scanWifiDisplays();
97                    mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
98                }
99            }
100        };
101
102        Static(Context appContext) {
103            mResources = Resources.getSystem();
104            mHandler = new Handler(appContext.getMainLooper());
105
106            IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
107            mAudioService = IAudioService.Stub.asInterface(b);
108
109            mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
110
111            mSystemCategory = new RouteCategory(
112                    com.android.internal.R.string.default_audio_route_category_name,
113                    ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
114            mSystemCategory.mIsSystem = true;
115        }
116
117        // Called after sStatic is initialized
118        void startMonitoringRoutes(Context appContext) {
119            mDefaultAudioVideo = new RouteInfo(mSystemCategory);
120            mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
121            mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
122            mDefaultAudioVideo.mPresentationDisplay = choosePresentationDisplayForRoute(
123                    mDefaultAudioVideo, getAllPresentationDisplays());
124            addRouteStatic(mDefaultAudioVideo);
125
126            // This will select the active wifi display route if there is one.
127            updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
128
129            appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
130                    new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
131            appContext.registerReceiver(new VolumeChangeReceiver(),
132                    new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
133
134            mDisplayService.registerDisplayListener(this, mHandler);
135
136            AudioRoutesInfo newAudioRoutes = null;
137            try {
138                newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
139            } catch (RemoteException e) {
140            }
141            if (newAudioRoutes != null) {
142                // This will select the active BT route if there is one and the current
143                // selected route is the default system route, or if there is no selected
144                // route yet.
145                updateAudioRoutes(newAudioRoutes);
146            }
147
148            // Select the default route if the above didn't sync us up
149            // appropriately with relevant system state.
150            if (mSelectedRoute == null) {
151                selectRouteStatic(mDefaultAudioVideo.getSupportedTypes(), mDefaultAudioVideo);
152            }
153        }
154
155        void updateAudioRoutes(AudioRoutesInfo newRoutes) {
156            if (newRoutes.mMainType != mCurAudioRoutesInfo.mMainType) {
157                mCurAudioRoutesInfo.mMainType = newRoutes.mMainType;
158                int name;
159                if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0
160                        || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) {
161                    name = com.android.internal.R.string.default_audio_route_name_headphones;
162                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
163                    name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
164                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
165                    name = com.android.internal.R.string.default_media_route_name_hdmi;
166                } else {
167                    name = com.android.internal.R.string.default_audio_route_name;
168                }
169                sStatic.mDefaultAudioVideo.mNameResId = name;
170                dispatchRouteChanged(sStatic.mDefaultAudioVideo);
171            }
172
173            final int mainType = mCurAudioRoutesInfo.mMainType;
174
175            boolean a2dpEnabled;
176            try {
177                a2dpEnabled = mAudioService.isBluetoothA2dpOn();
178            } catch (RemoteException e) {
179                Log.e(TAG, "Error querying Bluetooth A2DP state", e);
180                a2dpEnabled = false;
181            }
182
183            if (!TextUtils.equals(newRoutes.mBluetoothName, mCurAudioRoutesInfo.mBluetoothName)) {
184                mCurAudioRoutesInfo.mBluetoothName = newRoutes.mBluetoothName;
185                if (mCurAudioRoutesInfo.mBluetoothName != null) {
186                    if (sStatic.mBluetoothA2dpRoute == null) {
187                        final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
188                        info.mName = mCurAudioRoutesInfo.mBluetoothName;
189                        info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
190                        sStatic.mBluetoothA2dpRoute = info;
191                        addRouteStatic(sStatic.mBluetoothA2dpRoute);
192                    } else {
193                        sStatic.mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.mBluetoothName;
194                        dispatchRouteChanged(sStatic.mBluetoothA2dpRoute);
195                    }
196                } else if (sStatic.mBluetoothA2dpRoute != null) {
197                    removeRoute(sStatic.mBluetoothA2dpRoute);
198                    sStatic.mBluetoothA2dpRoute = null;
199                }
200            }
201
202            if (mBluetoothA2dpRoute != null) {
203                if (mainType != AudioRoutesInfo.MAIN_SPEAKER &&
204                        mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) {
205                    selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo);
206                } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) &&
207                        a2dpEnabled) {
208                    selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute);
209                }
210            }
211        }
212
213        void updateActiveScan() {
214            if (hasActiveScanCallbackOfType(ROUTE_TYPE_LIVE_VIDEO)) {
215                if (!mActivelyScanningWifiDisplays) {
216                    mActivelyScanningWifiDisplays = true;
217                    mHandler.post(mScanWifiDisplays);
218                }
219            } else {
220                if (mActivelyScanningWifiDisplays) {
221                    mActivelyScanningWifiDisplays = false;
222                    mHandler.removeCallbacks(mScanWifiDisplays);
223                }
224            }
225        }
226
227        private boolean hasActiveScanCallbackOfType(int type) {
228            final int count = mCallbacks.size();
229            for (int i = 0; i < count; i++) {
230                CallbackInfo cbi = mCallbacks.get(i);
231                if ((cbi.flags & CALLBACK_FLAG_ACTIVE_SCAN) != 0
232                        && (cbi.type & type) != 0) {
233                    return true;
234                }
235            }
236            return false;
237        }
238
239        @Override
240        public void onDisplayAdded(int displayId) {
241            updatePresentationDisplays(displayId);
242        }
243
244        @Override
245        public void onDisplayChanged(int displayId) {
246            updatePresentationDisplays(displayId);
247        }
248
249        @Override
250        public void onDisplayRemoved(int displayId) {
251            updatePresentationDisplays(displayId);
252        }
253
254        public Display[] getAllPresentationDisplays() {
255            return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
256        }
257
258        private void updatePresentationDisplays(int changedDisplayId) {
259            final Display[] displays = getAllPresentationDisplays();
260            final int count = mRoutes.size();
261            for (int i = 0; i < count; i++) {
262                final RouteInfo info = mRoutes.get(i);
263                Display display = choosePresentationDisplayForRoute(info, displays);
264                if (display != info.mPresentationDisplay
265                        || (display != null && display.getDisplayId() == changedDisplayId)) {
266                    info.mPresentationDisplay = display;
267                    dispatchRoutePresentationDisplayChanged(info);
268                }
269            }
270        }
271    }
272
273    static Static sStatic;
274
275    /**
276     * Route type flag for live audio.
277     *
278     * <p>A device that supports live audio routing will allow the media audio stream
279     * to be routed to supported destinations. This can include internal speakers or
280     * audio jacks on the device itself, A2DP devices, and more.</p>
281     *
282     * <p>Once initiated this routing is transparent to the application. All audio
283     * played on the media stream will be routed to the selected destination.</p>
284     */
285    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
286
287    /**
288     * Route type flag for live video.
289     *
290     * <p>A device that supports live video routing will allow a mirrored version
291     * of the device's primary display or a customized
292     * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p>
293     *
294     * <p>Once initiated, display mirroring is transparent to the application.
295     * While remote routing is active the application may use a
296     * {@link android.app.Presentation Presentation} to replace the mirrored view
297     * on the external display with different content.</p>
298     *
299     * @see RouteInfo#getPresentationDisplay()
300     * @see android.app.Presentation
301     */
302    public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
303
304    /**
305     * Route type flag for application-specific usage.
306     *
307     * <p>Unlike other media route types, user routes are managed by the application.
308     * The MediaRouter will manage and dispatch events for user routes, but the application
309     * is expected to interpret the meaning of these events and perform the requested
310     * routing tasks.</p>
311     */
312    public static final int ROUTE_TYPE_USER = 0x00800000;
313
314    /**
315     * Flag for {@link #addCallback}: Actively scan for routes while this callback
316     * is registered.
317     * <p>
318     * When this flag is specified, the media router will actively scan for new
319     * routes.  Certain routes, such as wifi display routes, may not be discoverable
320     * except when actively scanning.  This flag is typically used when the route picker
321     * dialog has been opened by the user to ensure that the route information is
322     * up to date.
323     * </p><p>
324     * Active scanning may consume a significant amount of power and may have intrusive
325     * effects on wireless connectivity.  Therefore it is important that active scanning
326     * only be requested when it is actually needed to satisfy a user request to
327     * discover and select a new route.
328     * </p>
329     */
330    public static final int CALLBACK_FLAG_ACTIVE_SCAN = 1 << 0;
331
332    /**
333     * Flag for {@link #addCallback}: Do not filter route events.
334     * <p>
335     * When this flag is specified, the callback will be invoked for event that affect any
336     * route event if they do not match the callback's associated media route selector.
337     * </p>
338     */
339    public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
340
341    // Maps application contexts
342    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
343
344    static String typesToString(int types) {
345        final StringBuilder result = new StringBuilder();
346        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
347            result.append("ROUTE_TYPE_LIVE_AUDIO ");
348        }
349        if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) {
350            result.append("ROUTE_TYPE_LIVE_VIDEO ");
351        }
352        if ((types & ROUTE_TYPE_USER) != 0) {
353            result.append("ROUTE_TYPE_USER ");
354        }
355        return result.toString();
356    }
357
358    /** @hide */
359    public MediaRouter(Context context) {
360        synchronized (Static.class) {
361            if (sStatic == null) {
362                final Context appContext = context.getApplicationContext();
363                sStatic = new Static(appContext);
364                sStatic.startMonitoringRoutes(appContext);
365            }
366        }
367    }
368
369    /**
370     * Gets the default route for playing media content on the system.
371     * <p>
372     * The system always provides a default route.
373     * </p>
374     *
375     * @return The default route, which is guaranteed to never be null.
376     */
377    public RouteInfo getDefaultRoute() {
378        return sStatic.mDefaultAudioVideo;
379    }
380
381    /**
382     * @hide for use by framework routing UI
383     */
384    public RouteCategory getSystemCategory() {
385        return sStatic.mSystemCategory;
386    }
387
388    /**
389     * Return the currently selected route for any of the given types
390     *
391     * @param type route types
392     * @return the selected route
393     */
394    public RouteInfo getSelectedRoute(int type) {
395        if (sStatic.mSelectedRoute != null &&
396                (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) {
397            // If the selected route supports any of the types supplied, it's still considered
398            // 'selected' for that type.
399            return sStatic.mSelectedRoute;
400        } else if (type == ROUTE_TYPE_USER) {
401            // The caller specifically asked for a user route and the currently selected route
402            // doesn't qualify.
403            return null;
404        }
405        // If the above didn't match and we're not specifically asking for a user route,
406        // consider the default selected.
407        return sStatic.mDefaultAudioVideo;
408    }
409
410    /**
411     * Add a callback to listen to events about specific kinds of media routes.
412     * If the specified callback is already registered, its registration will be updated for any
413     * additional route types specified.
414     * <p>
415     * This is a convenience method that has the same effect as calling
416     * {@link #addCallback(int, Callback, int)} without flags.
417     * </p>
418     *
419     * @param types Types of routes this callback is interested in
420     * @param cb Callback to add
421     */
422    public void addCallback(int types, Callback cb) {
423        addCallback(types, cb, 0);
424    }
425
426    /**
427     * Add a callback to listen to events about specific kinds of media routes.
428     * If the specified callback is already registered, its registration will be updated for any
429     * additional route types specified.
430     * <p>
431     * By default, the callback will only be invoked for events that affect routes
432     * that match the specified selector.  The filtering may be disabled by specifying
433     * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag.
434     * </p>
435     *
436     * @param types Types of routes this callback is interested in
437     * @param cb Callback to add
438     * @param flags Flags to control the behavior of the callback.
439     * May be zero or a combination of {@link #CALLBACK_FLAG_ACTIVE_SCAN} and
440     * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
441     */
442    public void addCallback(int types, Callback cb, int flags) {
443        CallbackInfo info;
444        int index = findCallbackInfo(cb);
445        if (index >= 0) {
446            info = sStatic.mCallbacks.get(index);
447            info.type |= types;
448            info.flags |= flags;
449        } else {
450            info = new CallbackInfo(cb, types, flags, this);
451            sStatic.mCallbacks.add(info);
452        }
453        if ((info.flags & CALLBACK_FLAG_ACTIVE_SCAN) != 0) {
454            sStatic.updateActiveScan();
455        }
456    }
457
458    /**
459     * Remove the specified callback. It will no longer receive events about media routing.
460     *
461     * @param cb Callback to remove
462     */
463    public void removeCallback(Callback cb) {
464        int index = findCallbackInfo(cb);
465        if (index >= 0) {
466            CallbackInfo info = sStatic.mCallbacks.remove(index);
467            if ((info.flags & CALLBACK_FLAG_ACTIVE_SCAN) != 0) {
468                sStatic.updateActiveScan();
469            }
470        } else {
471            Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
472        }
473    }
474
475    private int findCallbackInfo(Callback cb) {
476        final int count = sStatic.mCallbacks.size();
477        for (int i = 0; i < count; i++) {
478            final CallbackInfo info = sStatic.mCallbacks.get(i);
479            if (info.cb == cb) {
480                return i;
481            }
482        }
483        return -1;
484    }
485
486    /**
487     * Select the specified route to use for output of the given media types.
488     * <p class="note">
489     * As API version 18, this function may be used to select any route.
490     * In prior versions, this function could only be used to select user
491     * routes and would ignore any attempt to select a system route.
492     * </p>
493     *
494     * @param types type flags indicating which types this route should be used for.
495     *              The route must support at least a subset.
496     * @param route Route to select
497     */
498    public void selectRoute(int types, RouteInfo route) {
499        selectRouteStatic(types, route);
500    }
501
502    /**
503     * @hide internal use
504     */
505    public void selectRouteInt(int types, RouteInfo route) {
506        selectRouteStatic(types, route);
507    }
508
509    static void selectRouteStatic(int types, RouteInfo route) {
510        final RouteInfo oldRoute = sStatic.mSelectedRoute;
511        if (oldRoute == route) return;
512        if ((route.getSupportedTypes() & types) == 0) {
513            Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
514                    typesToString(route.getSupportedTypes()) + " into route types " +
515                    typesToString(types));
516            return;
517        }
518
519        final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
520        if (btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 &&
521                (route == btRoute || route == sStatic.mDefaultAudioVideo)) {
522            try {
523                sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
524            } catch (RemoteException e) {
525                Log.e(TAG, "Error changing Bluetooth A2DP state", e);
526            }
527        }
528
529        final WifiDisplay activeDisplay =
530                sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
531        final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
532        final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null;
533        if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
534            if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
535                sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
536            } else if (activeDisplay != null && !newRouteHasAddress) {
537                sStatic.mDisplayService.disconnectWifiDisplay();
538            }
539        }
540
541        if (oldRoute != null) {
542            dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
543        }
544        sStatic.mSelectedRoute = route;
545        if (route != null) {
546            dispatchRouteSelected(types & route.getSupportedTypes(), route);
547        }
548    }
549
550    /**
551     * Compare the device address of a display and a route.
552     * Nulls/no device address will match another null/no address.
553     */
554    static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) {
555        final boolean routeHasAddress = info != null && info.mDeviceAddress != null;
556        if (display == null && !routeHasAddress) {
557            return true;
558        }
559
560        if (display != null && routeHasAddress) {
561            return display.getDeviceAddress().equals(info.mDeviceAddress);
562        }
563        return false;
564    }
565
566    /**
567     * Add an app-specified route for media to the MediaRouter.
568     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
569     *
570     * @param info Definition of the route to add
571     * @see #createUserRoute(RouteCategory)
572     * @see #removeUserRoute(UserRouteInfo)
573     */
574    public void addUserRoute(UserRouteInfo info) {
575        addRouteStatic(info);
576    }
577
578    /**
579     * @hide Framework use only
580     */
581    public void addRouteInt(RouteInfo info) {
582        addRouteStatic(info);
583    }
584
585    static void addRouteStatic(RouteInfo info) {
586        final RouteCategory cat = info.getCategory();
587        if (!sStatic.mCategories.contains(cat)) {
588            sStatic.mCategories.add(cat);
589        }
590        if (cat.isGroupable() && !(info instanceof RouteGroup)) {
591            // Enforce that any added route in a groupable category must be in a group.
592            final RouteGroup group = new RouteGroup(info.getCategory());
593            group.mSupportedTypes = info.mSupportedTypes;
594            sStatic.mRoutes.add(group);
595            dispatchRouteAdded(group);
596            group.addRoute(info);
597
598            info = group;
599        } else {
600            sStatic.mRoutes.add(info);
601            dispatchRouteAdded(info);
602        }
603    }
604
605    /**
606     * Remove an app-specified route for media from the MediaRouter.
607     *
608     * @param info Definition of the route to remove
609     * @see #addUserRoute(UserRouteInfo)
610     */
611    public void removeUserRoute(UserRouteInfo info) {
612        removeRoute(info);
613    }
614
615    /**
616     * Remove all app-specified routes from the MediaRouter.
617     *
618     * @see #removeUserRoute(UserRouteInfo)
619     */
620    public void clearUserRoutes() {
621        for (int i = 0; i < sStatic.mRoutes.size(); i++) {
622            final RouteInfo info = sStatic.mRoutes.get(i);
623            // TODO Right now, RouteGroups only ever contain user routes.
624            // The code below will need to change if this assumption does.
625            if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
626                removeRouteAt(i);
627                i--;
628            }
629        }
630    }
631
632    /**
633     * @hide internal use only
634     */
635    public void removeRouteInt(RouteInfo info) {
636        removeRoute(info);
637    }
638
639    static void removeRoute(RouteInfo info) {
640        if (sStatic.mRoutes.remove(info)) {
641            final RouteCategory removingCat = info.getCategory();
642            final int count = sStatic.mRoutes.size();
643            boolean found = false;
644            for (int i = 0; i < count; i++) {
645                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
646                if (removingCat == cat) {
647                    found = true;
648                    break;
649                }
650            }
651            if (info == sStatic.mSelectedRoute) {
652                // Removing the currently selected route? Select the default before we remove it.
653                // TODO: Be smarter about the route types here; this selects for all valid.
654                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudioVideo);
655            }
656            if (!found) {
657                sStatic.mCategories.remove(removingCat);
658            }
659            dispatchRouteRemoved(info);
660        }
661    }
662
663    void removeRouteAt(int routeIndex) {
664        if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) {
665            final RouteInfo info = sStatic.mRoutes.remove(routeIndex);
666            final RouteCategory removingCat = info.getCategory();
667            final int count = sStatic.mRoutes.size();
668            boolean found = false;
669            for (int i = 0; i < count; i++) {
670                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
671                if (removingCat == cat) {
672                    found = true;
673                    break;
674                }
675            }
676            if (info == sStatic.mSelectedRoute) {
677                // Removing the currently selected route? Select the default before we remove it.
678                // TODO: Be smarter about the route types here; this selects for all valid.
679                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER,
680                        sStatic.mDefaultAudioVideo);
681            }
682            if (!found) {
683                sStatic.mCategories.remove(removingCat);
684            }
685            dispatchRouteRemoved(info);
686        }
687    }
688
689    /**
690     * Return the number of {@link MediaRouter.RouteCategory categories} currently
691     * represented by routes known to this MediaRouter.
692     *
693     * @return the number of unique categories represented by this MediaRouter's known routes
694     */
695    public int getCategoryCount() {
696        return sStatic.mCategories.size();
697    }
698
699    /**
700     * Return the {@link MediaRouter.RouteCategory category} at the given index.
701     * Valid indices are in the range [0-getCategoryCount).
702     *
703     * @param index which category to return
704     * @return the category at index
705     */
706    public RouteCategory getCategoryAt(int index) {
707        return sStatic.mCategories.get(index);
708    }
709
710    /**
711     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
712     * to this MediaRouter.
713     *
714     * @return the number of routes tracked by this router
715     */
716    public int getRouteCount() {
717        return sStatic.mRoutes.size();
718    }
719
720    /**
721     * Return the route at the specified index.
722     *
723     * @param index index of the route to return
724     * @return the route at index
725     */
726    public RouteInfo getRouteAt(int index) {
727        return sStatic.mRoutes.get(index);
728    }
729
730    static int getRouteCountStatic() {
731        return sStatic.mRoutes.size();
732    }
733
734    static RouteInfo getRouteAtStatic(int index) {
735        return sStatic.mRoutes.get(index);
736    }
737
738    /**
739     * Create a new user route that may be modified and registered for use by the application.
740     *
741     * @param category The category the new route will belong to
742     * @return A new UserRouteInfo for use by the application
743     *
744     * @see #addUserRoute(UserRouteInfo)
745     * @see #removeUserRoute(UserRouteInfo)
746     * @see #createRouteCategory(CharSequence)
747     */
748    public UserRouteInfo createUserRoute(RouteCategory category) {
749        return new UserRouteInfo(category);
750    }
751
752    /**
753     * Create a new route category. Each route must belong to a category.
754     *
755     * @param name Name of the new category
756     * @param isGroupable true if routes in this category may be grouped with one another
757     * @return the new RouteCategory
758     */
759    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
760        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
761    }
762
763    /**
764     * Create a new route category. Each route must belong to a category.
765     *
766     * @param nameResId Resource ID of the name of the new category
767     * @param isGroupable true if routes in this category may be grouped with one another
768     * @return the new RouteCategory
769     */
770    public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
771        return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
772    }
773
774    static void updateRoute(final RouteInfo info) {
775        dispatchRouteChanged(info);
776    }
777
778    static void dispatchRouteSelected(int type, RouteInfo info) {
779        for (CallbackInfo cbi : sStatic.mCallbacks) {
780            if (cbi.filterRouteEvent(info)) {
781                cbi.cb.onRouteSelected(cbi.router, type, info);
782            }
783        }
784    }
785
786    static void dispatchRouteUnselected(int type, RouteInfo info) {
787        for (CallbackInfo cbi : sStatic.mCallbacks) {
788            if (cbi.filterRouteEvent(info)) {
789                cbi.cb.onRouteUnselected(cbi.router, type, info);
790            }
791        }
792    }
793
794    static void dispatchRouteChanged(RouteInfo info) {
795        for (CallbackInfo cbi : sStatic.mCallbacks) {
796            if (cbi.filterRouteEvent(info)) {
797                cbi.cb.onRouteChanged(cbi.router, info);
798            }
799        }
800    }
801
802    static void dispatchRouteAdded(RouteInfo info) {
803        for (CallbackInfo cbi : sStatic.mCallbacks) {
804            if (cbi.filterRouteEvent(info)) {
805                cbi.cb.onRouteAdded(cbi.router, info);
806            }
807        }
808    }
809
810    static void dispatchRouteRemoved(RouteInfo info) {
811        for (CallbackInfo cbi : sStatic.mCallbacks) {
812            if (cbi.filterRouteEvent(info)) {
813                cbi.cb.onRouteRemoved(cbi.router, info);
814            }
815        }
816    }
817
818    static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
819        for (CallbackInfo cbi : sStatic.mCallbacks) {
820            if (cbi.filterRouteEvent(group)) {
821                cbi.cb.onRouteGrouped(cbi.router, info, group, index);
822            }
823        }
824    }
825
826    static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
827        for (CallbackInfo cbi : sStatic.mCallbacks) {
828            if (cbi.filterRouteEvent(group)) {
829                cbi.cb.onRouteUngrouped(cbi.router, info, group);
830            }
831        }
832    }
833
834    static void dispatchRouteVolumeChanged(RouteInfo info) {
835        for (CallbackInfo cbi : sStatic.mCallbacks) {
836            if (cbi.filterRouteEvent(info)) {
837                cbi.cb.onRouteVolumeChanged(cbi.router, info);
838            }
839        }
840    }
841
842    static void dispatchRoutePresentationDisplayChanged(RouteInfo info) {
843        for (CallbackInfo cbi : sStatic.mCallbacks) {
844            if (cbi.filterRouteEvent(info)) {
845                cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info);
846            }
847        }
848    }
849
850    static void systemVolumeChanged(int newValue) {
851        final RouteInfo selectedRoute = sStatic.mSelectedRoute;
852        if (selectedRoute == null) return;
853
854        if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
855                selectedRoute == sStatic.mDefaultAudioVideo) {
856            dispatchRouteVolumeChanged(selectedRoute);
857        } else if (sStatic.mBluetoothA2dpRoute != null) {
858            try {
859                dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
860                        sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo);
861            } catch (RemoteException e) {
862                Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
863            }
864        } else {
865            dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo);
866        }
867    }
868
869    static void updateWifiDisplayStatus(WifiDisplayStatus newStatus) {
870        final WifiDisplayStatus oldStatus = sStatic.mLastKnownWifiDisplayStatus;
871
872        // TODO Naive implementation. Make this smarter later.
873        boolean wantScan = false;
874        boolean blockScan = false;
875        WifiDisplay[] oldDisplays = oldStatus != null ?
876                oldStatus.getRememberedDisplays() : WifiDisplay.EMPTY_ARRAY;
877        WifiDisplay[] newDisplays;
878        WifiDisplay[] availableDisplays;
879        WifiDisplay activeDisplay;
880
881        if (newStatus.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
882            newDisplays = newStatus.getRememberedDisplays();
883            availableDisplays = newStatus.getAvailableDisplays();
884            activeDisplay = newStatus.getActiveDisplay();
885        } else {
886            newDisplays = availableDisplays = WifiDisplay.EMPTY_ARRAY;
887            activeDisplay = null;
888        }
889
890        for (int i = 0; i < newDisplays.length; i++) {
891            final WifiDisplay d = newDisplays[i];
892            final boolean available = findMatchingDisplay(d, availableDisplays) != null;
893            RouteInfo route = findWifiDisplayRoute(d);
894            if (route == null) {
895                route = makeWifiDisplayRoute(d, available);
896                addRouteStatic(route);
897                wantScan = true;
898            } else {
899                updateWifiDisplayRoute(route, d, available, newStatus);
900            }
901            if (d.equals(activeDisplay)) {
902                selectRouteStatic(route.getSupportedTypes(), route);
903
904                // Don't scan if we're already connected to a wifi display,
905                // the scanning process can cause a hiccup with some configurations.
906                blockScan = true;
907            }
908        }
909        for (int i = 0; i < oldDisplays.length; i++) {
910            final WifiDisplay d = oldDisplays[i];
911            final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays);
912            if (newDisplay == null) {
913                removeRoute(findWifiDisplayRoute(d));
914            }
915        }
916
917        if (wantScan && !blockScan) {
918            sStatic.mDisplayService.scanWifiDisplays();
919        }
920
921        sStatic.mLastKnownWifiDisplayStatus = newStatus;
922    }
923
924    static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) {
925        final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
926        newRoute.mDeviceAddress = display.getDeviceAddress();
927        newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
928        newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
929        newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
930
931        newRoute.setStatusCode(available ?
932                RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING);
933        newRoute.mEnabled = available;
934
935        newRoute.mName = display.getFriendlyDisplayName();
936
937        newRoute.mPresentationDisplay = choosePresentationDisplayForRoute(newRoute,
938                sStatic.getAllPresentationDisplays());
939        return newRoute;
940    }
941
942    private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display,
943            boolean available, WifiDisplayStatus wifiDisplayStatus) {
944        final boolean isScanning =
945                wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING;
946
947        boolean changed = false;
948        int newStatus = RouteInfo.STATUS_NONE;
949
950        if (available) {
951            newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE;
952        } else {
953            newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
954        }
955
956        if (display.equals(wifiDisplayStatus.getActiveDisplay())) {
957            final int activeState = wifiDisplayStatus.getActiveDisplayState();
958            switch (activeState) {
959                case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
960                    newStatus = RouteInfo.STATUS_NONE;
961                    break;
962                case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
963                    newStatus = RouteInfo.STATUS_CONNECTING;
964                    break;
965                case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
966                    Log.e(TAG, "Active display is not connected!");
967                    break;
968            }
969        }
970
971        final String newName = display.getFriendlyDisplayName();
972        if (!route.getName().equals(newName)) {
973            route.mName = newName;
974            changed = true;
975        }
976
977        changed |= route.mEnabled != available;
978        route.mEnabled = available;
979
980        changed |= route.setStatusCode(newStatus);
981
982        if (changed) {
983            dispatchRouteChanged(route);
984        }
985
986        if (!available && route == sStatic.mSelectedRoute) {
987            // Oops, no longer available. Reselect the default.
988            final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo;
989            selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute);
990        }
991    }
992
993    private static WifiDisplay findMatchingDisplay(WifiDisplay d, WifiDisplay[] displays) {
994        for (int i = 0; i < displays.length; i++) {
995            final WifiDisplay other = displays[i];
996            if (d.hasSameAddress(other)) {
997                return other;
998            }
999        }
1000        return null;
1001    }
1002
1003    private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
1004        final int count = sStatic.mRoutes.size();
1005        for (int i = 0; i < count; i++) {
1006            final RouteInfo info = sStatic.mRoutes.get(i);
1007            if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
1008                return info;
1009            }
1010        }
1011        return null;
1012    }
1013
1014    private static Display choosePresentationDisplayForRoute(RouteInfo route, Display[] displays) {
1015        if ((route.mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
1016            if (route.mDeviceAddress != null) {
1017                // Find the indicated Wifi display by its address.
1018                for (Display display : displays) {
1019                    if (display.getType() == Display.TYPE_WIFI
1020                            && route.mDeviceAddress.equals(display.getAddress())) {
1021                        return display;
1022                    }
1023                }
1024                return null;
1025            }
1026
1027            if (route == sStatic.mDefaultAudioVideo && displays.length > 0) {
1028                // Choose the first presentation display from the list.
1029                return displays[0];
1030            }
1031        }
1032        return null;
1033    }
1034
1035    /**
1036     * Information about a media route.
1037     */
1038    public static class RouteInfo {
1039        CharSequence mName;
1040        int mNameResId;
1041        private CharSequence mStatus;
1042        int mSupportedTypes;
1043        RouteGroup mGroup;
1044        final RouteCategory mCategory;
1045        Drawable mIcon;
1046        // playback information
1047        int mPlaybackType = PLAYBACK_TYPE_LOCAL;
1048        int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1049        int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
1050        int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
1051        int mPlaybackStream = AudioManager.STREAM_MUSIC;
1052        VolumeCallbackInfo mVcb;
1053        Display mPresentationDisplay;
1054
1055        String mDeviceAddress;
1056        boolean mEnabled = true;
1057
1058        // A predetermined connection status that can override mStatus
1059        private int mStatusCode;
1060
1061        /** @hide */ public static final int STATUS_NONE = 0;
1062        /** @hide */ public static final int STATUS_SCANNING = 1;
1063        /** @hide */ public static final int STATUS_CONNECTING = 2;
1064        /** @hide */ public static final int STATUS_AVAILABLE = 3;
1065        /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
1066
1067        private Object mTag;
1068
1069        /**
1070         * The default playback type, "local", indicating the presentation of the media is happening
1071         * on the same device (e.g. a phone, a tablet) as where it is controlled from.
1072         * @see #setPlaybackType(int)
1073         */
1074        public final static int PLAYBACK_TYPE_LOCAL = 0;
1075        /**
1076         * A playback type indicating the presentation of the media is happening on
1077         * a different device (i.e. the remote device) than where it is controlled from.
1078         * @see #setPlaybackType(int)
1079         */
1080        public final static int PLAYBACK_TYPE_REMOTE = 1;
1081        /**
1082         * Playback information indicating the playback volume is fixed, i.e. it cannot be
1083         * controlled from this object. An example of fixed playback volume is a remote player,
1084         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
1085         * than attenuate at the source.
1086         * @see #setVolumeHandling(int)
1087         */
1088        public final static int PLAYBACK_VOLUME_FIXED = 0;
1089        /**
1090         * Playback information indicating the playback volume is variable and can be controlled
1091         * from this object.
1092         */
1093        public final static int PLAYBACK_VOLUME_VARIABLE = 1;
1094
1095        RouteInfo(RouteCategory category) {
1096            mCategory = category;
1097        }
1098
1099        /**
1100         * @return The user-friendly name of a media route. This is the string presented
1101         * to users who may select this as the active route.
1102         */
1103        public CharSequence getName() {
1104            return getName(sStatic.mResources);
1105        }
1106
1107        /**
1108         * Return the properly localized/resource selected name of this route.
1109         *
1110         * @param context Context used to resolve the correct configuration to load
1111         * @return The user-friendly name of the media route. This is the string presented
1112         * to users who may select this as the active route.
1113         */
1114        public CharSequence getName(Context context) {
1115            return getName(context.getResources());
1116        }
1117
1118        CharSequence getName(Resources res) {
1119            if (mNameResId != 0) {
1120                return mName = res.getText(mNameResId);
1121            }
1122            return mName;
1123        }
1124
1125        /**
1126         * @return The user-friendly status for a media route. This may include a description
1127         * of the currently playing media, if available.
1128         */
1129        public CharSequence getStatus() {
1130            return mStatus;
1131        }
1132
1133        /**
1134         * Set this route's status by predetermined status code. If the caller
1135         * should dispatch a route changed event this call will return true;
1136         */
1137        boolean setStatusCode(int statusCode) {
1138            if (statusCode != mStatusCode) {
1139                mStatusCode = statusCode;
1140                int resId = 0;
1141                switch (statusCode) {
1142                    case STATUS_SCANNING:
1143                        resId = com.android.internal.R.string.media_route_status_scanning;
1144                        break;
1145                    case STATUS_CONNECTING:
1146                        resId = com.android.internal.R.string.media_route_status_connecting;
1147                        break;
1148                    case STATUS_AVAILABLE:
1149                        resId = com.android.internal.R.string.media_route_status_available;
1150                        break;
1151                    case STATUS_NOT_AVAILABLE:
1152                        resId = com.android.internal.R.string.media_route_status_not_available;
1153                        break;
1154                }
1155                mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
1156                return true;
1157            }
1158            return false;
1159        }
1160
1161        /**
1162         * @hide
1163         */
1164        public int getStatusCode() {
1165            return mStatusCode;
1166        }
1167
1168        /**
1169         * @return A media type flag set describing which types this route supports.
1170         */
1171        public int getSupportedTypes() {
1172            return mSupportedTypes;
1173        }
1174
1175        /**
1176         * @return The group that this route belongs to.
1177         */
1178        public RouteGroup getGroup() {
1179            return mGroup;
1180        }
1181
1182        /**
1183         * @return the category this route belongs to.
1184         */
1185        public RouteCategory getCategory() {
1186            return mCategory;
1187        }
1188
1189        /**
1190         * Get the icon representing this route.
1191         * This icon will be used in picker UIs if available.
1192         *
1193         * @return the icon representing this route or null if no icon is available
1194         */
1195        public Drawable getIconDrawable() {
1196            return mIcon;
1197        }
1198
1199        /**
1200         * Set an application-specific tag object for this route.
1201         * The application may use this to store arbitrary data associated with the
1202         * route for internal tracking.
1203         *
1204         * <p>Note that the lifespan of a route may be well past the lifespan of
1205         * an Activity or other Context; take care that objects you store here
1206         * will not keep more data in memory alive than you intend.</p>
1207         *
1208         * @param tag Arbitrary, app-specific data for this route to hold for later use
1209         */
1210        public void setTag(Object tag) {
1211            mTag = tag;
1212            routeUpdated();
1213        }
1214
1215        /**
1216         * @return The tag object previously set by the application
1217         * @see #setTag(Object)
1218         */
1219        public Object getTag() {
1220            return mTag;
1221        }
1222
1223        /**
1224         * @return the type of playback associated with this route
1225         * @see UserRouteInfo#setPlaybackType(int)
1226         */
1227        public int getPlaybackType() {
1228            return mPlaybackType;
1229        }
1230
1231        /**
1232         * @return the stream over which the playback associated with this route is performed
1233         * @see UserRouteInfo#setPlaybackStream(int)
1234         */
1235        public int getPlaybackStream() {
1236            return mPlaybackStream;
1237        }
1238
1239        /**
1240         * Return the current volume for this route. Depending on the route, this may only
1241         * be valid if the route is currently selected.
1242         *
1243         * @return the volume at which the playback associated with this route is performed
1244         * @see UserRouteInfo#setVolume(int)
1245         */
1246        public int getVolume() {
1247            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1248                int vol = 0;
1249                try {
1250                    vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
1251                } catch (RemoteException e) {
1252                    Log.e(TAG, "Error getting local stream volume", e);
1253                }
1254                return vol;
1255            } else {
1256                return mVolume;
1257            }
1258        }
1259
1260        /**
1261         * Request a volume change for this route.
1262         * @param volume value between 0 and getVolumeMax
1263         */
1264        public void requestSetVolume(int volume) {
1265            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1266                try {
1267                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1268                } catch (RemoteException e) {
1269                    Log.e(TAG, "Error setting local stream volume", e);
1270                }
1271            } else {
1272                Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " +
1273                        "Non-local volume playback on system route? " +
1274                        "Could not request volume change.");
1275            }
1276        }
1277
1278        /**
1279         * Request an incremental volume update for this route.
1280         * @param direction Delta to apply to the current volume
1281         */
1282        public void requestUpdateVolume(int direction) {
1283            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1284                try {
1285                    final int volume =
1286                            Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
1287                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1288                } catch (RemoteException e) {
1289                    Log.e(TAG, "Error setting local stream volume", e);
1290                }
1291            } else {
1292                Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " +
1293                        "Non-local volume playback on system route? " +
1294                        "Could not request volume change.");
1295            }
1296        }
1297
1298        /**
1299         * @return the maximum volume at which the playback associated with this route is performed
1300         * @see UserRouteInfo#setVolumeMax(int)
1301         */
1302        public int getVolumeMax() {
1303            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1304                int volMax = 0;
1305                try {
1306                    volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
1307                } catch (RemoteException e) {
1308                    Log.e(TAG, "Error getting local stream volume", e);
1309                }
1310                return volMax;
1311            } else {
1312                return mVolumeMax;
1313            }
1314        }
1315
1316        /**
1317         * @return how volume is handling on the route
1318         * @see UserRouteInfo#setVolumeHandling(int)
1319         */
1320        public int getVolumeHandling() {
1321            return mVolumeHandling;
1322        }
1323
1324        /**
1325         * Gets the {@link Display} that should be used by the application to show
1326         * a {@link android.app.Presentation} on an external display when this route is selected.
1327         * Depending on the route, this may only be valid if the route is currently
1328         * selected.
1329         * <p>
1330         * The preferred presentation display may change independently of the route
1331         * being selected or unselected.  For example, the presentation display
1332         * of the default system route may change when an external HDMI display is connected
1333         * or disconnected even though the route itself has not changed.
1334         * </p><p>
1335         * This method may return null if there is no external display associated with
1336         * the route or if the display is not ready to show UI yet.
1337         * </p><p>
1338         * The application should listen for changes to the presentation display
1339         * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
1340         * show or dismiss its {@link android.app.Presentation} accordingly when the display
1341         * becomes available or is removed.
1342         * </p><p>
1343         * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes.
1344         * </p>
1345         *
1346         * @return The preferred presentation display to use when this route is
1347         * selected or null if none.
1348         *
1349         * @see #ROUTE_TYPE_LIVE_VIDEO
1350         * @see android.app.Presentation
1351         */
1352        public Display getPresentationDisplay() {
1353            return mPresentationDisplay;
1354        }
1355
1356        /**
1357         * Returns true if this route is enabled and may be selected.
1358         *
1359         * @return True if this route is enabled.
1360         */
1361        public boolean isEnabled() {
1362            return mEnabled;
1363        }
1364
1365        /**
1366         * Returns true if the route is in the process of connecting and is not
1367         * yet ready for use.
1368         *
1369         * @return True if this route is in the process of connecting.
1370         */
1371        public boolean isConnecting() {
1372            return mStatusCode == STATUS_CONNECTING;
1373        }
1374
1375        void setStatusInt(CharSequence status) {
1376            if (!status.equals(mStatus)) {
1377                mStatus = status;
1378                if (mGroup != null) {
1379                    mGroup.memberStatusChanged(this, status);
1380                }
1381                routeUpdated();
1382            }
1383        }
1384
1385        final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
1386            public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
1387                sStatic.mHandler.post(new Runnable() {
1388                    @Override
1389                    public void run() {
1390                        if (mVcb != null) {
1391                            if (direction != 0) {
1392                                mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
1393                            } else {
1394                                mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
1395                            }
1396                        }
1397                    }
1398                });
1399            }
1400        };
1401
1402        void routeUpdated() {
1403            updateRoute(this);
1404        }
1405
1406        @Override
1407        public String toString() {
1408            String supportedTypes = typesToString(getSupportedTypes());
1409            return getClass().getSimpleName() + "{ name=" + getName() +
1410                    ", status=" + getStatus() +
1411                    ", category=" + getCategory() +
1412                    ", supportedTypes=" + supportedTypes +
1413                    ", presentationDisplay=" + mPresentationDisplay + "}";
1414        }
1415    }
1416
1417    /**
1418     * Information about a route that the application may define and modify.
1419     * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
1420     * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
1421     *
1422     * @see MediaRouter.RouteInfo
1423     */
1424    public static class UserRouteInfo extends RouteInfo {
1425        RemoteControlClient mRcc;
1426
1427        UserRouteInfo(RouteCategory category) {
1428            super(category);
1429            mSupportedTypes = ROUTE_TYPE_USER;
1430            mPlaybackType = PLAYBACK_TYPE_REMOTE;
1431            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1432        }
1433
1434        /**
1435         * Set the user-visible name of this route.
1436         * @param name Name to display to the user to describe this route
1437         */
1438        public void setName(CharSequence name) {
1439            mName = name;
1440            routeUpdated();
1441        }
1442
1443        /**
1444         * Set the user-visible name of this route.
1445         * @param resId Resource ID of the name to display to the user to describe this route
1446         */
1447        public void setName(int resId) {
1448            mNameResId = resId;
1449            mName = null;
1450            routeUpdated();
1451        }
1452
1453        /**
1454         * Set the current user-visible status for this route.
1455         * @param status Status to display to the user to describe what the endpoint
1456         * of this route is currently doing
1457         */
1458        public void setStatus(CharSequence status) {
1459            setStatusInt(status);
1460        }
1461
1462        /**
1463         * Set the RemoteControlClient responsible for reporting playback info for this
1464         * user route.
1465         *
1466         * <p>If this route manages remote playback, the data exposed by this
1467         * RemoteControlClient will be used to reflect and update information
1468         * such as route volume info in related UIs.</p>
1469         *
1470         * <p>The RemoteControlClient must have been previously registered with
1471         * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
1472         *
1473         * @param rcc RemoteControlClient associated with this route
1474         */
1475        public void setRemoteControlClient(RemoteControlClient rcc) {
1476            mRcc = rcc;
1477            updatePlaybackInfoOnRcc();
1478        }
1479
1480        /**
1481         * Retrieve the RemoteControlClient associated with this route, if one has been set.
1482         *
1483         * @return the RemoteControlClient associated with this route
1484         * @see #setRemoteControlClient(RemoteControlClient)
1485         */
1486        public RemoteControlClient getRemoteControlClient() {
1487            return mRcc;
1488        }
1489
1490        /**
1491         * Set an icon that will be used to represent this route.
1492         * The system may use this icon in picker UIs or similar.
1493         *
1494         * @param icon icon drawable to use to represent this route
1495         */
1496        public void setIconDrawable(Drawable icon) {
1497            mIcon = icon;
1498        }
1499
1500        /**
1501         * Set an icon that will be used to represent this route.
1502         * The system may use this icon in picker UIs or similar.
1503         *
1504         * @param resId Resource ID of an icon drawable to use to represent this route
1505         */
1506        public void setIconResource(int resId) {
1507            setIconDrawable(sStatic.mResources.getDrawable(resId));
1508        }
1509
1510        /**
1511         * Set a callback to be notified of volume update requests
1512         * @param vcb
1513         */
1514        public void setVolumeCallback(VolumeCallback vcb) {
1515            mVcb = new VolumeCallbackInfo(vcb, this);
1516        }
1517
1518        /**
1519         * Defines whether playback associated with this route is "local"
1520         *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
1521         *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
1522         * @param type
1523         */
1524        public void setPlaybackType(int type) {
1525            if (mPlaybackType != type) {
1526                mPlaybackType = type;
1527                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type);
1528            }
1529        }
1530
1531        /**
1532         * Defines whether volume for the playback associated with this route is fixed
1533         * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
1534         * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
1535         * @param volumeHandling
1536         */
1537        public void setVolumeHandling(int volumeHandling) {
1538            if (mVolumeHandling != volumeHandling) {
1539                mVolumeHandling = volumeHandling;
1540                setPlaybackInfoOnRcc(
1541                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling);
1542            }
1543        }
1544
1545        /**
1546         * Defines at what volume the playback associated with this route is performed (for user
1547         * feedback purposes). This information is only used when the playback is not local.
1548         * @param volume
1549         */
1550        public void setVolume(int volume) {
1551            volume = Math.max(0, Math.min(volume, getVolumeMax()));
1552            if (mVolume != volume) {
1553                mVolume = volume;
1554                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
1555                dispatchRouteVolumeChanged(this);
1556                if (mGroup != null) {
1557                    mGroup.memberVolumeChanged(this);
1558                }
1559            }
1560        }
1561
1562        @Override
1563        public void requestSetVolume(int volume) {
1564            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1565                if (mVcb == null) {
1566                    Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
1567                    return;
1568                }
1569                mVcb.vcb.onVolumeSetRequest(this, volume);
1570            }
1571        }
1572
1573        @Override
1574        public void requestUpdateVolume(int direction) {
1575            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1576                if (mVcb == null) {
1577                    Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
1578                    return;
1579                }
1580                mVcb.vcb.onVolumeUpdateRequest(this, direction);
1581            }
1582        }
1583
1584        /**
1585         * Defines the maximum volume at which the playback associated with this route is performed
1586         * (for user feedback purposes). This information is only used when the playback is not
1587         * local.
1588         * @param volumeMax
1589         */
1590        public void setVolumeMax(int volumeMax) {
1591            if (mVolumeMax != volumeMax) {
1592                mVolumeMax = volumeMax;
1593                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax);
1594            }
1595        }
1596
1597        /**
1598         * Defines over what stream type the media is presented.
1599         * @param stream
1600         */
1601        public void setPlaybackStream(int stream) {
1602            if (mPlaybackStream != stream) {
1603                mPlaybackStream = stream;
1604                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
1605            }
1606        }
1607
1608        private void updatePlaybackInfoOnRcc() {
1609            if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) {
1610                mRcc.setPlaybackInformation(
1611                        RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax);
1612                mRcc.setPlaybackInformation(
1613                        RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume);
1614                mRcc.setPlaybackInformation(
1615                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling);
1616                mRcc.setPlaybackInformation(
1617                        RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream);
1618                mRcc.setPlaybackInformation(
1619                        RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType);
1620                // let AudioService know whom to call when remote volume needs to be updated
1621                try {
1622                    sStatic.mAudioService.registerRemoteVolumeObserverForRcc(
1623                            mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */);
1624                } catch (RemoteException e) {
1625                    Log.e(TAG, "Error registering remote volume observer", e);
1626                }
1627            }
1628        }
1629
1630        private void setPlaybackInfoOnRcc(int what, int value) {
1631            if (mRcc != null) {
1632                mRcc.setPlaybackInformation(what, value);
1633            }
1634        }
1635    }
1636
1637    /**
1638     * Information about a route that consists of multiple other routes in a group.
1639     */
1640    public static class RouteGroup extends RouteInfo {
1641        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
1642        private boolean mUpdateName;
1643
1644        RouteGroup(RouteCategory category) {
1645            super(category);
1646            mGroup = this;
1647            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1648        }
1649
1650        CharSequence getName(Resources res) {
1651            if (mUpdateName) updateName();
1652            return super.getName(res);
1653        }
1654
1655        /**
1656         * Add a route to this group. The route must not currently belong to another group.
1657         *
1658         * @param route route to add to this group
1659         */
1660        public void addRoute(RouteInfo route) {
1661            if (route.getGroup() != null) {
1662                throw new IllegalStateException("Route " + route + " is already part of a group.");
1663            }
1664            if (route.getCategory() != mCategory) {
1665                throw new IllegalArgumentException(
1666                        "Route cannot be added to a group with a different category. " +
1667                            "(Route category=" + route.getCategory() +
1668                            " group category=" + mCategory + ")");
1669            }
1670            final int at = mRoutes.size();
1671            mRoutes.add(route);
1672            route.mGroup = this;
1673            mUpdateName = true;
1674            updateVolume();
1675            routeUpdated();
1676            dispatchRouteGrouped(route, this, at);
1677        }
1678
1679        /**
1680         * Add a route to this group before the specified index.
1681         *
1682         * @param route route to add
1683         * @param insertAt insert the new route before this index
1684         */
1685        public void addRoute(RouteInfo route, int insertAt) {
1686            if (route.getGroup() != null) {
1687                throw new IllegalStateException("Route " + route + " is already part of a group.");
1688            }
1689            if (route.getCategory() != mCategory) {
1690                throw new IllegalArgumentException(
1691                        "Route cannot be added to a group with a different category. " +
1692                            "(Route category=" + route.getCategory() +
1693                            " group category=" + mCategory + ")");
1694            }
1695            mRoutes.add(insertAt, route);
1696            route.mGroup = this;
1697            mUpdateName = true;
1698            updateVolume();
1699            routeUpdated();
1700            dispatchRouteGrouped(route, this, insertAt);
1701        }
1702
1703        /**
1704         * Remove a route from this group.
1705         *
1706         * @param route route to remove
1707         */
1708        public void removeRoute(RouteInfo route) {
1709            if (route.getGroup() != this) {
1710                throw new IllegalArgumentException("Route " + route +
1711                        " is not a member of this group.");
1712            }
1713            mRoutes.remove(route);
1714            route.mGroup = null;
1715            mUpdateName = true;
1716            updateVolume();
1717            dispatchRouteUngrouped(route, this);
1718            routeUpdated();
1719        }
1720
1721        /**
1722         * Remove the route at the specified index from this group.
1723         *
1724         * @param index index of the route to remove
1725         */
1726        public void removeRoute(int index) {
1727            RouteInfo route = mRoutes.remove(index);
1728            route.mGroup = null;
1729            mUpdateName = true;
1730            updateVolume();
1731            dispatchRouteUngrouped(route, this);
1732            routeUpdated();
1733        }
1734
1735        /**
1736         * @return The number of routes in this group
1737         */
1738        public int getRouteCount() {
1739            return mRoutes.size();
1740        }
1741
1742        /**
1743         * Return the route in this group at the specified index
1744         *
1745         * @param index Index to fetch
1746         * @return The route at index
1747         */
1748        public RouteInfo getRouteAt(int index) {
1749            return mRoutes.get(index);
1750        }
1751
1752        /**
1753         * Set an icon that will be used to represent this group.
1754         * The system may use this icon in picker UIs or similar.
1755         *
1756         * @param icon icon drawable to use to represent this group
1757         */
1758        public void setIconDrawable(Drawable icon) {
1759            mIcon = icon;
1760        }
1761
1762        /**
1763         * Set an icon that will be used to represent this group.
1764         * The system may use this icon in picker UIs or similar.
1765         *
1766         * @param resId Resource ID of an icon drawable to use to represent this group
1767         */
1768        public void setIconResource(int resId) {
1769            setIconDrawable(sStatic.mResources.getDrawable(resId));
1770        }
1771
1772        @Override
1773        public void requestSetVolume(int volume) {
1774            final int maxVol = getVolumeMax();
1775            if (maxVol == 0) {
1776                return;
1777            }
1778
1779            final float scaledVolume = (float) volume / maxVol;
1780            final int routeCount = getRouteCount();
1781            for (int i = 0; i < routeCount; i++) {
1782                final RouteInfo route = getRouteAt(i);
1783                final int routeVol = (int) (scaledVolume * route.getVolumeMax());
1784                route.requestSetVolume(routeVol);
1785            }
1786            if (volume != mVolume) {
1787                mVolume = volume;
1788                dispatchRouteVolumeChanged(this);
1789            }
1790        }
1791
1792        @Override
1793        public void requestUpdateVolume(int direction) {
1794            final int maxVol = getVolumeMax();
1795            if (maxVol == 0) {
1796                return;
1797            }
1798
1799            final int routeCount = getRouteCount();
1800            int volume = 0;
1801            for (int i = 0; i < routeCount; i++) {
1802                final RouteInfo route = getRouteAt(i);
1803                route.requestUpdateVolume(direction);
1804                final int routeVol = route.getVolume();
1805                if (routeVol > volume) {
1806                    volume = routeVol;
1807                }
1808            }
1809            if (volume != mVolume) {
1810                mVolume = volume;
1811                dispatchRouteVolumeChanged(this);
1812            }
1813        }
1814
1815        void memberNameChanged(RouteInfo info, CharSequence name) {
1816            mUpdateName = true;
1817            routeUpdated();
1818        }
1819
1820        void memberStatusChanged(RouteInfo info, CharSequence status) {
1821            setStatusInt(status);
1822        }
1823
1824        void memberVolumeChanged(RouteInfo info) {
1825            updateVolume();
1826        }
1827
1828        void updateVolume() {
1829            // A group always represents the highest component volume value.
1830            final int routeCount = getRouteCount();
1831            int volume = 0;
1832            for (int i = 0; i < routeCount; i++) {
1833                final int routeVol = getRouteAt(i).getVolume();
1834                if (routeVol > volume) {
1835                    volume = routeVol;
1836                }
1837            }
1838            if (volume != mVolume) {
1839                mVolume = volume;
1840                dispatchRouteVolumeChanged(this);
1841            }
1842        }
1843
1844        @Override
1845        void routeUpdated() {
1846            int types = 0;
1847            final int count = mRoutes.size();
1848            if (count == 0) {
1849                // Don't keep empty groups in the router.
1850                MediaRouter.removeRoute(this);
1851                return;
1852            }
1853
1854            int maxVolume = 0;
1855            boolean isLocal = true;
1856            boolean isFixedVolume = true;
1857            for (int i = 0; i < count; i++) {
1858                final RouteInfo route = mRoutes.get(i);
1859                types |= route.mSupportedTypes;
1860                final int routeMaxVolume = route.getVolumeMax();
1861                if (routeMaxVolume > maxVolume) {
1862                    maxVolume = routeMaxVolume;
1863                }
1864                isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
1865                isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
1866            }
1867            mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
1868            mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
1869            mSupportedTypes = types;
1870            mVolumeMax = maxVolume;
1871            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
1872            super.routeUpdated();
1873        }
1874
1875        void updateName() {
1876            final StringBuilder sb = new StringBuilder();
1877            final int count = mRoutes.size();
1878            for (int i = 0; i < count; i++) {
1879                final RouteInfo info = mRoutes.get(i);
1880                // TODO: There's probably a much more correct way to localize this.
1881                if (i > 0) sb.append(", ");
1882                sb.append(info.mName);
1883            }
1884            mName = sb.toString();
1885            mUpdateName = false;
1886        }
1887
1888        @Override
1889        public String toString() {
1890            StringBuilder sb = new StringBuilder(super.toString());
1891            sb.append('[');
1892            final int count = mRoutes.size();
1893            for (int i = 0; i < count; i++) {
1894                if (i > 0) sb.append(", ");
1895                sb.append(mRoutes.get(i));
1896            }
1897            sb.append(']');
1898            return sb.toString();
1899        }
1900    }
1901
1902    /**
1903     * Definition of a category of routes. All routes belong to a category.
1904     */
1905    public static class RouteCategory {
1906        CharSequence mName;
1907        int mNameResId;
1908        int mTypes;
1909        final boolean mGroupable;
1910        boolean mIsSystem;
1911
1912        RouteCategory(CharSequence name, int types, boolean groupable) {
1913            mName = name;
1914            mTypes = types;
1915            mGroupable = groupable;
1916        }
1917
1918        RouteCategory(int nameResId, int types, boolean groupable) {
1919            mNameResId = nameResId;
1920            mTypes = types;
1921            mGroupable = groupable;
1922        }
1923
1924        /**
1925         * @return the name of this route category
1926         */
1927        public CharSequence getName() {
1928            return getName(sStatic.mResources);
1929        }
1930
1931        /**
1932         * Return the properly localized/configuration dependent name of this RouteCategory.
1933         *
1934         * @param context Context to resolve name resources
1935         * @return the name of this route category
1936         */
1937        public CharSequence getName(Context context) {
1938            return getName(context.getResources());
1939        }
1940
1941        CharSequence getName(Resources res) {
1942            if (mNameResId != 0) {
1943                return res.getText(mNameResId);
1944            }
1945            return mName;
1946        }
1947
1948        /**
1949         * Return the current list of routes in this category that have been added
1950         * to the MediaRouter.
1951         *
1952         * <p>This list will not include routes that are nested within RouteGroups.
1953         * A RouteGroup is treated as a single route within its category.</p>
1954         *
1955         * @param out a List to fill with the routes in this category. If this parameter is
1956         *            non-null, it will be cleared, filled with the current routes with this
1957         *            category, and returned. If this parameter is null, a new List will be
1958         *            allocated to report the category's current routes.
1959         * @return A list with the routes in this category that have been added to the MediaRouter.
1960         */
1961        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
1962            if (out == null) {
1963                out = new ArrayList<RouteInfo>();
1964            } else {
1965                out.clear();
1966            }
1967
1968            final int count = getRouteCountStatic();
1969            for (int i = 0; i < count; i++) {
1970                final RouteInfo route = getRouteAtStatic(i);
1971                if (route.mCategory == this) {
1972                    out.add(route);
1973                }
1974            }
1975            return out;
1976        }
1977
1978        /**
1979         * @return Flag set describing the route types supported by this category
1980         */
1981        public int getSupportedTypes() {
1982            return mTypes;
1983        }
1984
1985        /**
1986         * Return whether or not this category supports grouping.
1987         *
1988         * <p>If this method returns true, all routes obtained from this category
1989         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
1990         *
1991         * @return true if this category supports
1992         */
1993        public boolean isGroupable() {
1994            return mGroupable;
1995        }
1996
1997        /**
1998         * @return true if this is the category reserved for system routes.
1999         * @hide
2000         */
2001        public boolean isSystem() {
2002            return mIsSystem;
2003        }
2004
2005        public String toString() {
2006            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
2007                    " groupable=" + mGroupable + " }";
2008        }
2009    }
2010
2011    static class CallbackInfo {
2012        public int type;
2013        public int flags;
2014        public final Callback cb;
2015        public final MediaRouter router;
2016
2017        public CallbackInfo(Callback cb, int type, int flags, MediaRouter router) {
2018            this.cb = cb;
2019            this.type = type;
2020            this.flags = flags;
2021            this.router = router;
2022        }
2023
2024        public boolean filterRouteEvent(RouteInfo route) {
2025            return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
2026                    || (type & route.mSupportedTypes) != 0;
2027        }
2028    }
2029
2030    /**
2031     * Interface for receiving events about media routing changes.
2032     * All methods of this interface will be called from the application's main thread.
2033     * <p>
2034     * A Callback will only receive events relevant to routes that the callback
2035     * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
2036     * flag was specified in {@link MediaRouter#addCallback(int, Callback, int)}.
2037     * </p>
2038     *
2039     * @see MediaRouter#addCallback(int, Callback, int)
2040     * @see MediaRouter#removeCallback(Callback)
2041     */
2042    public static abstract class Callback {
2043        /**
2044         * Called when the supplied route becomes selected as the active route
2045         * for the given route type.
2046         *
2047         * @param router the MediaRouter reporting the event
2048         * @param type Type flag set indicating the routes that have been selected
2049         * @param info Route that has been selected for the given route types
2050         */
2051        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
2052
2053        /**
2054         * Called when the supplied route becomes unselected as the active route
2055         * for the given route type.
2056         *
2057         * @param router the MediaRouter reporting the event
2058         * @param type Type flag set indicating the routes that have been unselected
2059         * @param info Route that has been unselected for the given route types
2060         */
2061        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
2062
2063        /**
2064         * Called when a route for the specified type was added.
2065         *
2066         * @param router the MediaRouter reporting the event
2067         * @param info Route that has become available for use
2068         */
2069        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
2070
2071        /**
2072         * Called when a route for the specified type was removed.
2073         *
2074         * @param router the MediaRouter reporting the event
2075         * @param info Route that has been removed from availability
2076         */
2077        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
2078
2079        /**
2080         * Called when an aspect of the indicated route has changed.
2081         *
2082         * <p>This will not indicate that the types supported by this route have
2083         * changed, only that cosmetic info such as name or status have been updated.</p>
2084         *
2085         * @param router the MediaRouter reporting the event
2086         * @param info The route that was changed
2087         */
2088        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
2089
2090        /**
2091         * Called when a route is added to a group.
2092         *
2093         * @param router the MediaRouter reporting the event
2094         * @param info The route that was added
2095         * @param group The group the route was added to
2096         * @param index The route index within group that info was added at
2097         */
2098        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2099                int index);
2100
2101        /**
2102         * Called when a route is removed from a group.
2103         *
2104         * @param router the MediaRouter reporting the event
2105         * @param info The route that was removed
2106         * @param group The group the route was removed from
2107         */
2108        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
2109
2110        /**
2111         * Called when a route's volume changes.
2112         *
2113         * @param router the MediaRouter reporting the event
2114         * @param info The route with altered volume
2115         */
2116        public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
2117
2118        /**
2119         * Called when a route's presentation display changes.
2120         * <p>
2121         * This method is called whenever the route's presentation display becomes
2122         * available, is removes or has changes to some of its properties (such as its size).
2123         * </p>
2124         *
2125         * @param router the MediaRouter reporting the event
2126         * @param info The route whose presentation display changed
2127         *
2128         * @see RouteInfo#getPresentationDisplay()
2129         */
2130        public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
2131        }
2132    }
2133
2134    /**
2135     * Stub implementation of {@link MediaRouter.Callback}.
2136     * Each abstract method is defined as a no-op. Override just the ones
2137     * you need.
2138     */
2139    public static class SimpleCallback extends Callback {
2140
2141        @Override
2142        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
2143        }
2144
2145        @Override
2146        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
2147        }
2148
2149        @Override
2150        public void onRouteAdded(MediaRouter router, RouteInfo info) {
2151        }
2152
2153        @Override
2154        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
2155        }
2156
2157        @Override
2158        public void onRouteChanged(MediaRouter router, RouteInfo info) {
2159        }
2160
2161        @Override
2162        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2163                int index) {
2164        }
2165
2166        @Override
2167        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
2168        }
2169
2170        @Override
2171        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
2172        }
2173    }
2174
2175    static class VolumeCallbackInfo {
2176        public final VolumeCallback vcb;
2177        public final RouteInfo route;
2178
2179        public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
2180            this.vcb = vcb;
2181            this.route = route;
2182        }
2183    }
2184
2185    /**
2186     * Interface for receiving events about volume changes.
2187     * All methods of this interface will be called from the application's main thread.
2188     *
2189     * <p>A VolumeCallback will only receive events relevant to routes that the callback
2190     * was registered for.</p>
2191     *
2192     * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
2193     */
2194    public static abstract class VolumeCallback {
2195        /**
2196         * Called when the volume for the route should be increased or decreased.
2197         * @param info the route affected by this event
2198         * @param direction an integer indicating whether the volume is to be increased
2199         *     (positive value) or decreased (negative value).
2200         *     For bundled changes, the absolute value indicates the number of changes
2201         *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
2202         */
2203        public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
2204        /**
2205         * Called when the volume for the route should be set to the given value
2206         * @param info the route affected by this event
2207         * @param volume an integer indicating the new volume value that should be used, always
2208         *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
2209         */
2210        public abstract void onVolumeSetRequest(RouteInfo info, int volume);
2211    }
2212
2213    static class VolumeChangeReceiver extends BroadcastReceiver {
2214        @Override
2215        public void onReceive(Context context, Intent intent) {
2216            if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
2217                final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
2218                        -1);
2219                if (streamType != AudioManager.STREAM_MUSIC) {
2220                    return;
2221                }
2222
2223                final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
2224                final int oldVolume = intent.getIntExtra(
2225                        AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
2226                if (newVolume != oldVolume) {
2227                    systemVolumeChanged(newVolume);
2228                }
2229            }
2230        }
2231    }
2232
2233    static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
2234        @Override
2235        public void onReceive(Context context, Intent intent) {
2236            if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
2237                updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
2238                        DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
2239            }
2240        }
2241    }
2242}
2243