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