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