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