MediaRouter.java revision 77e59fef24c02a109bb409afe38dbdf28e1fd914
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 boolean available = findMatchingDisplay(d, availableDisplays) != null;
787            RouteInfo route = findWifiDisplayRoute(d);
788            if (route == null) {
789                route = makeWifiDisplayRoute(d, available);
790                addRouteStatic(route);
791                wantScan = true;
792            } else {
793                updateWifiDisplayRoute(route, d, available, newStatus);
794            }
795            if (d.equals(activeDisplay)) {
796                selectRouteStatic(route.getSupportedTypes(), route);
797
798                // Don't scan if we're already connected to a wifi display,
799                // the scanning process can cause a hiccup with some configurations.
800                blockScan = true;
801            }
802        }
803        for (int i = 0; i < oldDisplays.length; i++) {
804            final WifiDisplay d = oldDisplays[i];
805            final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays);
806            if (newDisplay == null) {
807                removeRoute(findWifiDisplayRoute(d));
808            }
809        }
810
811        if (wantScan && !blockScan) {
812            sStatic.mDisplayService.scanWifiDisplays();
813        }
814
815        sStatic.mLastKnownWifiDisplayStatus = newStatus;
816    }
817
818    static RouteInfo makeWifiDisplayRoute(WifiDisplay display, boolean available) {
819        final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
820        newRoute.mDeviceAddress = display.getDeviceAddress();
821        newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
822        newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
823        newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
824
825        newRoute.setStatusCode(available ?
826                RouteInfo.STATUS_AVAILABLE : RouteInfo.STATUS_CONNECTING);
827        newRoute.mEnabled = available;
828
829        newRoute.mName = display.getFriendlyDisplayName();
830
831        newRoute.mPresentationDisplay = choosePresentationDisplayForRoute(newRoute,
832                sStatic.getAllPresentationDisplays());
833        return newRoute;
834    }
835
836    private static void updateWifiDisplayRoute(RouteInfo route, WifiDisplay display,
837            boolean available, WifiDisplayStatus wifiDisplayStatus) {
838        final boolean isScanning =
839                wifiDisplayStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING;
840
841        boolean changed = false;
842        int newStatus = RouteInfo.STATUS_NONE;
843
844        if (available) {
845            newStatus = isScanning ? RouteInfo.STATUS_SCANNING : RouteInfo.STATUS_AVAILABLE;
846        } else {
847            newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
848        }
849
850        if (display.equals(wifiDisplayStatus.getActiveDisplay())) {
851            final int activeState = wifiDisplayStatus.getActiveDisplayState();
852            switch (activeState) {
853                case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
854                    newStatus = RouteInfo.STATUS_NONE;
855                    break;
856                case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
857                    newStatus = RouteInfo.STATUS_CONNECTING;
858                    break;
859                case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
860                    Log.e(TAG, "Active display is not connected!");
861                    break;
862            }
863        }
864
865        final String newName = display.getFriendlyDisplayName();
866        if (!route.getName().equals(newName)) {
867            route.mName = newName;
868            changed = true;
869        }
870
871        changed |= route.mEnabled != available;
872        route.mEnabled = available;
873
874        changed |= route.setStatusCode(newStatus);
875
876        if (changed) {
877            dispatchRouteChanged(route);
878        }
879
880        if (!available && route == sStatic.mSelectedRoute) {
881            // Oops, no longer available. Reselect the default.
882            final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo;
883            selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute);
884        }
885    }
886
887    private static WifiDisplay findMatchingDisplay(WifiDisplay d, WifiDisplay[] displays) {
888        for (int i = 0; i < displays.length; i++) {
889            final WifiDisplay other = displays[i];
890            if (d.hasSameAddress(other)) {
891                return other;
892            }
893        }
894        return null;
895    }
896
897    private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
898        final int count = sStatic.mRoutes.size();
899        for (int i = 0; i < count; i++) {
900            final RouteInfo info = sStatic.mRoutes.get(i);
901            if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
902                return info;
903            }
904        }
905        return null;
906    }
907
908    private static Display choosePresentationDisplayForRoute(RouteInfo route, Display[] displays) {
909        if ((route.mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
910            if (route.mDeviceAddress != null) {
911                // Find the indicated Wifi display by its address.
912                for (Display display : displays) {
913                    if (display.getType() == Display.TYPE_WIFI
914                            && route.mDeviceAddress.equals(display.getAddress())) {
915                        return display;
916                    }
917                }
918                return null;
919            }
920
921            if (route == sStatic.mDefaultAudioVideo && displays.length > 0) {
922                // Choose the first presentation display from the list.
923                return displays[0];
924            }
925        }
926        return null;
927    }
928
929    /**
930     * Information about a media route.
931     */
932    public static class RouteInfo {
933        CharSequence mName;
934        int mNameResId;
935        private CharSequence mStatus;
936        int mSupportedTypes;
937        RouteGroup mGroup;
938        final RouteCategory mCategory;
939        Drawable mIcon;
940        // playback information
941        int mPlaybackType = PLAYBACK_TYPE_LOCAL;
942        int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
943        int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
944        int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
945        int mPlaybackStream = AudioManager.STREAM_MUSIC;
946        VolumeCallbackInfo mVcb;
947        Display mPresentationDisplay;
948
949        String mDeviceAddress;
950        boolean mEnabled = true;
951
952        // A predetermined connection status that can override mStatus
953        private int mStatusCode;
954
955        /** @hide */ public static final int STATUS_NONE = 0;
956        /** @hide */ public static final int STATUS_SCANNING = 1;
957        /** @hide */ public static final int STATUS_CONNECTING = 2;
958        /** @hide */ public static final int STATUS_AVAILABLE = 3;
959        /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
960
961        private Object mTag;
962
963        /**
964         * The default playback type, "local", indicating the presentation of the media is happening
965         * on the same device (e.g. a phone, a tablet) as where it is controlled from.
966         * @see #setPlaybackType(int)
967         */
968        public final static int PLAYBACK_TYPE_LOCAL = 0;
969        /**
970         * A playback type indicating the presentation of the media is happening on
971         * a different device (i.e. the remote device) than where it is controlled from.
972         * @see #setPlaybackType(int)
973         */
974        public final static int PLAYBACK_TYPE_REMOTE = 1;
975        /**
976         * Playback information indicating the playback volume is fixed, i.e. it cannot be
977         * controlled from this object. An example of fixed playback volume is a remote player,
978         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
979         * than attenuate at the source.
980         * @see #setVolumeHandling(int)
981         */
982        public final static int PLAYBACK_VOLUME_FIXED = 0;
983        /**
984         * Playback information indicating the playback volume is variable and can be controlled
985         * from this object.
986         */
987        public final static int PLAYBACK_VOLUME_VARIABLE = 1;
988
989        RouteInfo(RouteCategory category) {
990            mCategory = category;
991        }
992
993        /**
994         * @return The user-friendly name of a media route. This is the string presented
995         * to users who may select this as the active route.
996         */
997        public CharSequence getName() {
998            return getName(sStatic.mResources);
999        }
1000
1001        /**
1002         * Return the properly localized/resource selected name of this route.
1003         *
1004         * @param context Context used to resolve the correct configuration to load
1005         * @return The user-friendly name of the media route. This is the string presented
1006         * to users who may select this as the active route.
1007         */
1008        public CharSequence getName(Context context) {
1009            return getName(context.getResources());
1010        }
1011
1012        CharSequence getName(Resources res) {
1013            if (mNameResId != 0) {
1014                return mName = res.getText(mNameResId);
1015            }
1016            return mName;
1017        }
1018
1019        /**
1020         * @return The user-friendly status for a media route. This may include a description
1021         * of the currently playing media, if available.
1022         */
1023        public CharSequence getStatus() {
1024            return mStatus;
1025        }
1026
1027        /**
1028         * Set this route's status by predetermined status code. If the caller
1029         * should dispatch a route changed event this call will return true;
1030         */
1031        boolean setStatusCode(int statusCode) {
1032            if (statusCode != mStatusCode) {
1033                mStatusCode = statusCode;
1034                int resId = 0;
1035                switch (statusCode) {
1036                    case STATUS_SCANNING:
1037                        resId = com.android.internal.R.string.media_route_status_scanning;
1038                        break;
1039                    case STATUS_CONNECTING:
1040                        resId = com.android.internal.R.string.media_route_status_connecting;
1041                        break;
1042                    case STATUS_AVAILABLE:
1043                        resId = com.android.internal.R.string.media_route_status_available;
1044                        break;
1045                    case STATUS_NOT_AVAILABLE:
1046                        resId = com.android.internal.R.string.media_route_status_not_available;
1047                        break;
1048                }
1049                mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
1050                return true;
1051            }
1052            return false;
1053        }
1054
1055        /**
1056         * @hide
1057         */
1058        public int getStatusCode() {
1059            return mStatusCode;
1060        }
1061
1062        /**
1063         * @return A media type flag set describing which types this route supports.
1064         */
1065        public int getSupportedTypes() {
1066            return mSupportedTypes;
1067        }
1068
1069        /**
1070         * @return The group that this route belongs to.
1071         */
1072        public RouteGroup getGroup() {
1073            return mGroup;
1074        }
1075
1076        /**
1077         * @return the category this route belongs to.
1078         */
1079        public RouteCategory getCategory() {
1080            return mCategory;
1081        }
1082
1083        /**
1084         * Get the icon representing this route.
1085         * This icon will be used in picker UIs if available.
1086         *
1087         * @return the icon representing this route or null if no icon is available
1088         */
1089        public Drawable getIconDrawable() {
1090            return mIcon;
1091        }
1092
1093        /**
1094         * Set an application-specific tag object for this route.
1095         * The application may use this to store arbitrary data associated with the
1096         * route for internal tracking.
1097         *
1098         * <p>Note that the lifespan of a route may be well past the lifespan of
1099         * an Activity or other Context; take care that objects you store here
1100         * will not keep more data in memory alive than you intend.</p>
1101         *
1102         * @param tag Arbitrary, app-specific data for this route to hold for later use
1103         */
1104        public void setTag(Object tag) {
1105            mTag = tag;
1106            routeUpdated();
1107        }
1108
1109        /**
1110         * @return The tag object previously set by the application
1111         * @see #setTag(Object)
1112         */
1113        public Object getTag() {
1114            return mTag;
1115        }
1116
1117        /**
1118         * @return the type of playback associated with this route
1119         * @see UserRouteInfo#setPlaybackType(int)
1120         */
1121        public int getPlaybackType() {
1122            return mPlaybackType;
1123        }
1124
1125        /**
1126         * @return the stream over which the playback associated with this route is performed
1127         * @see UserRouteInfo#setPlaybackStream(int)
1128         */
1129        public int getPlaybackStream() {
1130            return mPlaybackStream;
1131        }
1132
1133        /**
1134         * Return the current volume for this route. Depending on the route, this may only
1135         * be valid if the route is currently selected.
1136         *
1137         * @return the volume at which the playback associated with this route is performed
1138         * @see UserRouteInfo#setVolume(int)
1139         */
1140        public int getVolume() {
1141            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1142                int vol = 0;
1143                try {
1144                    vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
1145                } catch (RemoteException e) {
1146                    Log.e(TAG, "Error getting local stream volume", e);
1147                }
1148                return vol;
1149            } else {
1150                return mVolume;
1151            }
1152        }
1153
1154        /**
1155         * Request a volume change for this route.
1156         * @param volume value between 0 and getVolumeMax
1157         */
1158        public void requestSetVolume(int volume) {
1159            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1160                try {
1161                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1162                } catch (RemoteException e) {
1163                    Log.e(TAG, "Error setting local stream volume", e);
1164                }
1165            } else {
1166                Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " +
1167                        "Non-local volume playback on system route? " +
1168                        "Could not request volume change.");
1169            }
1170        }
1171
1172        /**
1173         * Request an incremental volume update for this route.
1174         * @param direction Delta to apply to the current volume
1175         */
1176        public void requestUpdateVolume(int direction) {
1177            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1178                try {
1179                    final int volume =
1180                            Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
1181                    sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0);
1182                } catch (RemoteException e) {
1183                    Log.e(TAG, "Error setting local stream volume", e);
1184                }
1185            } else {
1186                Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " +
1187                        "Non-local volume playback on system route? " +
1188                        "Could not request volume change.");
1189            }
1190        }
1191
1192        /**
1193         * @return the maximum volume at which the playback associated with this route is performed
1194         * @see UserRouteInfo#setVolumeMax(int)
1195         */
1196        public int getVolumeMax() {
1197            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
1198                int volMax = 0;
1199                try {
1200                    volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
1201                } catch (RemoteException e) {
1202                    Log.e(TAG, "Error getting local stream volume", e);
1203                }
1204                return volMax;
1205            } else {
1206                return mVolumeMax;
1207            }
1208        }
1209
1210        /**
1211         * @return how volume is handling on the route
1212         * @see UserRouteInfo#setVolumeHandling(int)
1213         */
1214        public int getVolumeHandling() {
1215            return mVolumeHandling;
1216        }
1217
1218        /**
1219         * Gets the {@link Display} that should be used by the application to show
1220         * a {@link android.app.Presentation} on an external display when this route is selected.
1221         * Depending on the route, this may only be valid if the route is currently
1222         * selected.
1223         * <p>
1224         * The preferred presentation display may change independently of the route
1225         * being selected or unselected.  For example, the presentation display
1226         * of the default system route may change when an external HDMI display is connected
1227         * or disconnected even though the route itself has not changed.
1228         * </p><p>
1229         * This method may return null if there is no external display associated with
1230         * the route or if the display is not ready to show UI yet.
1231         * </p><p>
1232         * The application should listen for changes to the presentation display
1233         * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
1234         * show or dismiss its {@link android.app.Presentation} accordingly when the display
1235         * becomes available or is removed.
1236         * </p><p>
1237         * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes.
1238         * </p>
1239         *
1240         * @return The preferred presentation display to use when this route is
1241         * selected or null if none.
1242         *
1243         * @see #ROUTE_TYPE_LIVE_VIDEO
1244         * @see android.app.Presentation
1245         */
1246        public Display getPresentationDisplay() {
1247            return mPresentationDisplay;
1248        }
1249
1250        /**
1251         * @return true if this route is enabled and may be selected
1252         */
1253        public boolean isEnabled() {
1254            return mEnabled;
1255        }
1256
1257        void setStatusInt(CharSequence status) {
1258            if (!status.equals(mStatus)) {
1259                mStatus = status;
1260                if (mGroup != null) {
1261                    mGroup.memberStatusChanged(this, status);
1262                }
1263                routeUpdated();
1264            }
1265        }
1266
1267        final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
1268            public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
1269                sStatic.mHandler.post(new Runnable() {
1270                    @Override
1271                    public void run() {
1272                        if (mVcb != null) {
1273                            if (direction != 0) {
1274                                mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
1275                            } else {
1276                                mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
1277                            }
1278                        }
1279                    }
1280                });
1281            }
1282        };
1283
1284        void routeUpdated() {
1285            updateRoute(this);
1286        }
1287
1288        @Override
1289        public String toString() {
1290            String supportedTypes = typesToString(getSupportedTypes());
1291            return getClass().getSimpleName() + "{ name=" + getName() +
1292                    ", status=" + getStatus() +
1293                    ", category=" + getCategory() +
1294                    ", supportedTypes=" + supportedTypes +
1295                    ", presentationDisplay=" + mPresentationDisplay + "}";
1296        }
1297    }
1298
1299    /**
1300     * Information about a route that the application may define and modify.
1301     * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
1302     * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
1303     *
1304     * @see MediaRouter.RouteInfo
1305     */
1306    public static class UserRouteInfo extends RouteInfo {
1307        RemoteControlClient mRcc;
1308
1309        UserRouteInfo(RouteCategory category) {
1310            super(category);
1311            mSupportedTypes = ROUTE_TYPE_USER;
1312            mPlaybackType = PLAYBACK_TYPE_REMOTE;
1313            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1314        }
1315
1316        /**
1317         * Set the user-visible name of this route.
1318         * @param name Name to display to the user to describe this route
1319         */
1320        public void setName(CharSequence name) {
1321            mName = name;
1322            routeUpdated();
1323        }
1324
1325        /**
1326         * Set the user-visible name of this route.
1327         * @param resId Resource ID of the name to display to the user to describe this route
1328         */
1329        public void setName(int resId) {
1330            mNameResId = resId;
1331            mName = null;
1332            routeUpdated();
1333        }
1334
1335        /**
1336         * Set the current user-visible status for this route.
1337         * @param status Status to display to the user to describe what the endpoint
1338         * of this route is currently doing
1339         */
1340        public void setStatus(CharSequence status) {
1341            setStatusInt(status);
1342        }
1343
1344        /**
1345         * Set the RemoteControlClient responsible for reporting playback info for this
1346         * user route.
1347         *
1348         * <p>If this route manages remote playback, the data exposed by this
1349         * RemoteControlClient will be used to reflect and update information
1350         * such as route volume info in related UIs.</p>
1351         *
1352         * <p>The RemoteControlClient must have been previously registered with
1353         * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
1354         *
1355         * @param rcc RemoteControlClient associated with this route
1356         */
1357        public void setRemoteControlClient(RemoteControlClient rcc) {
1358            mRcc = rcc;
1359            updatePlaybackInfoOnRcc();
1360        }
1361
1362        /**
1363         * Retrieve the RemoteControlClient associated with this route, if one has been set.
1364         *
1365         * @return the RemoteControlClient associated with this route
1366         * @see #setRemoteControlClient(RemoteControlClient)
1367         */
1368        public RemoteControlClient getRemoteControlClient() {
1369            return mRcc;
1370        }
1371
1372        /**
1373         * Set an icon that will be used to represent this route.
1374         * The system may use this icon in picker UIs or similar.
1375         *
1376         * @param icon icon drawable to use to represent this route
1377         */
1378        public void setIconDrawable(Drawable icon) {
1379            mIcon = icon;
1380        }
1381
1382        /**
1383         * Set an icon that will be used to represent this route.
1384         * The system may use this icon in picker UIs or similar.
1385         *
1386         * @param resId Resource ID of an icon drawable to use to represent this route
1387         */
1388        public void setIconResource(int resId) {
1389            setIconDrawable(sStatic.mResources.getDrawable(resId));
1390        }
1391
1392        /**
1393         * Set a callback to be notified of volume update requests
1394         * @param vcb
1395         */
1396        public void setVolumeCallback(VolumeCallback vcb) {
1397            mVcb = new VolumeCallbackInfo(vcb, this);
1398        }
1399
1400        /**
1401         * Defines whether playback associated with this route is "local"
1402         *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
1403         *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
1404         * @param type
1405         */
1406        public void setPlaybackType(int type) {
1407            if (mPlaybackType != type) {
1408                mPlaybackType = type;
1409                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type);
1410            }
1411        }
1412
1413        /**
1414         * Defines whether volume for the playback associated with this route is fixed
1415         * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
1416         * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
1417         * @param volumeHandling
1418         */
1419        public void setVolumeHandling(int volumeHandling) {
1420            if (mVolumeHandling != volumeHandling) {
1421                mVolumeHandling = volumeHandling;
1422                setPlaybackInfoOnRcc(
1423                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling);
1424            }
1425        }
1426
1427        /**
1428         * Defines at what volume the playback associated with this route is performed (for user
1429         * feedback purposes). This information is only used when the playback is not local.
1430         * @param volume
1431         */
1432        public void setVolume(int volume) {
1433            volume = Math.max(0, Math.min(volume, getVolumeMax()));
1434            if (mVolume != volume) {
1435                mVolume = volume;
1436                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
1437                dispatchRouteVolumeChanged(this);
1438                if (mGroup != null) {
1439                    mGroup.memberVolumeChanged(this);
1440                }
1441            }
1442        }
1443
1444        @Override
1445        public void requestSetVolume(int volume) {
1446            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1447                if (mVcb == null) {
1448                    Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
1449                    return;
1450                }
1451                mVcb.vcb.onVolumeSetRequest(this, volume);
1452            }
1453        }
1454
1455        @Override
1456        public void requestUpdateVolume(int direction) {
1457            if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
1458                if (mVcb == null) {
1459                    Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
1460                    return;
1461                }
1462                mVcb.vcb.onVolumeUpdateRequest(this, direction);
1463            }
1464        }
1465
1466        /**
1467         * Defines the maximum volume at which the playback associated with this route is performed
1468         * (for user feedback purposes). This information is only used when the playback is not
1469         * local.
1470         * @param volumeMax
1471         */
1472        public void setVolumeMax(int volumeMax) {
1473            if (mVolumeMax != volumeMax) {
1474                mVolumeMax = volumeMax;
1475                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax);
1476            }
1477        }
1478
1479        /**
1480         * Defines over what stream type the media is presented.
1481         * @param stream
1482         */
1483        public void setPlaybackStream(int stream) {
1484            if (mPlaybackStream != stream) {
1485                mPlaybackStream = stream;
1486                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
1487            }
1488        }
1489
1490        private void updatePlaybackInfoOnRcc() {
1491            if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) {
1492                mRcc.setPlaybackInformation(
1493                        RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax);
1494                mRcc.setPlaybackInformation(
1495                        RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume);
1496                mRcc.setPlaybackInformation(
1497                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling);
1498                mRcc.setPlaybackInformation(
1499                        RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream);
1500                mRcc.setPlaybackInformation(
1501                        RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType);
1502                // let AudioService know whom to call when remote volume needs to be updated
1503                try {
1504                    sStatic.mAudioService.registerRemoteVolumeObserverForRcc(
1505                            mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */);
1506                } catch (RemoteException e) {
1507                    Log.e(TAG, "Error registering remote volume observer", e);
1508                }
1509            }
1510        }
1511
1512        private void setPlaybackInfoOnRcc(int what, int value) {
1513            if (mRcc != null) {
1514                mRcc.setPlaybackInformation(what, value);
1515            }
1516        }
1517    }
1518
1519    /**
1520     * Information about a route that consists of multiple other routes in a group.
1521     */
1522    public static class RouteGroup extends RouteInfo {
1523        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
1524        private boolean mUpdateName;
1525
1526        RouteGroup(RouteCategory category) {
1527            super(category);
1528            mGroup = this;
1529            mVolumeHandling = PLAYBACK_VOLUME_FIXED;
1530        }
1531
1532        CharSequence getName(Resources res) {
1533            if (mUpdateName) updateName();
1534            return super.getName(res);
1535        }
1536
1537        /**
1538         * Add a route to this group. The route must not currently belong to another group.
1539         *
1540         * @param route route to add to this group
1541         */
1542        public void addRoute(RouteInfo route) {
1543            if (route.getGroup() != null) {
1544                throw new IllegalStateException("Route " + route + " is already part of a group.");
1545            }
1546            if (route.getCategory() != mCategory) {
1547                throw new IllegalArgumentException(
1548                        "Route cannot be added to a group with a different category. " +
1549                            "(Route category=" + route.getCategory() +
1550                            " group category=" + mCategory + ")");
1551            }
1552            final int at = mRoutes.size();
1553            mRoutes.add(route);
1554            route.mGroup = this;
1555            mUpdateName = true;
1556            updateVolume();
1557            routeUpdated();
1558            dispatchRouteGrouped(route, this, at);
1559        }
1560
1561        /**
1562         * Add a route to this group before the specified index.
1563         *
1564         * @param route route to add
1565         * @param insertAt insert the new route before this index
1566         */
1567        public void addRoute(RouteInfo route, int insertAt) {
1568            if (route.getGroup() != null) {
1569                throw new IllegalStateException("Route " + route + " is already part of a group.");
1570            }
1571            if (route.getCategory() != mCategory) {
1572                throw new IllegalArgumentException(
1573                        "Route cannot be added to a group with a different category. " +
1574                            "(Route category=" + route.getCategory() +
1575                            " group category=" + mCategory + ")");
1576            }
1577            mRoutes.add(insertAt, route);
1578            route.mGroup = this;
1579            mUpdateName = true;
1580            updateVolume();
1581            routeUpdated();
1582            dispatchRouteGrouped(route, this, insertAt);
1583        }
1584
1585        /**
1586         * Remove a route from this group.
1587         *
1588         * @param route route to remove
1589         */
1590        public void removeRoute(RouteInfo route) {
1591            if (route.getGroup() != this) {
1592                throw new IllegalArgumentException("Route " + route +
1593                        " is not a member of this group.");
1594            }
1595            mRoutes.remove(route);
1596            route.mGroup = null;
1597            mUpdateName = true;
1598            updateVolume();
1599            dispatchRouteUngrouped(route, this);
1600            routeUpdated();
1601        }
1602
1603        /**
1604         * Remove the route at the specified index from this group.
1605         *
1606         * @param index index of the route to remove
1607         */
1608        public void removeRoute(int index) {
1609            RouteInfo route = mRoutes.remove(index);
1610            route.mGroup = null;
1611            mUpdateName = true;
1612            updateVolume();
1613            dispatchRouteUngrouped(route, this);
1614            routeUpdated();
1615        }
1616
1617        /**
1618         * @return The number of routes in this group
1619         */
1620        public int getRouteCount() {
1621            return mRoutes.size();
1622        }
1623
1624        /**
1625         * Return the route in this group at the specified index
1626         *
1627         * @param index Index to fetch
1628         * @return The route at index
1629         */
1630        public RouteInfo getRouteAt(int index) {
1631            return mRoutes.get(index);
1632        }
1633
1634        /**
1635         * Set an icon that will be used to represent this group.
1636         * The system may use this icon in picker UIs or similar.
1637         *
1638         * @param icon icon drawable to use to represent this group
1639         */
1640        public void setIconDrawable(Drawable icon) {
1641            mIcon = icon;
1642        }
1643
1644        /**
1645         * Set an icon that will be used to represent this group.
1646         * The system may use this icon in picker UIs or similar.
1647         *
1648         * @param resId Resource ID of an icon drawable to use to represent this group
1649         */
1650        public void setIconResource(int resId) {
1651            setIconDrawable(sStatic.mResources.getDrawable(resId));
1652        }
1653
1654        @Override
1655        public void requestSetVolume(int volume) {
1656            final int maxVol = getVolumeMax();
1657            if (maxVol == 0) {
1658                return;
1659            }
1660
1661            final float scaledVolume = (float) volume / maxVol;
1662            final int routeCount = getRouteCount();
1663            for (int i = 0; i < routeCount; i++) {
1664                final RouteInfo route = getRouteAt(i);
1665                final int routeVol = (int) (scaledVolume * route.getVolumeMax());
1666                route.requestSetVolume(routeVol);
1667            }
1668            if (volume != mVolume) {
1669                mVolume = volume;
1670                dispatchRouteVolumeChanged(this);
1671            }
1672        }
1673
1674        @Override
1675        public void requestUpdateVolume(int direction) {
1676            final int maxVol = getVolumeMax();
1677            if (maxVol == 0) {
1678                return;
1679            }
1680
1681            final int routeCount = getRouteCount();
1682            int volume = 0;
1683            for (int i = 0; i < routeCount; i++) {
1684                final RouteInfo route = getRouteAt(i);
1685                route.requestUpdateVolume(direction);
1686                final int routeVol = route.getVolume();
1687                if (routeVol > volume) {
1688                    volume = routeVol;
1689                }
1690            }
1691            if (volume != mVolume) {
1692                mVolume = volume;
1693                dispatchRouteVolumeChanged(this);
1694            }
1695        }
1696
1697        void memberNameChanged(RouteInfo info, CharSequence name) {
1698            mUpdateName = true;
1699            routeUpdated();
1700        }
1701
1702        void memberStatusChanged(RouteInfo info, CharSequence status) {
1703            setStatusInt(status);
1704        }
1705
1706        void memberVolumeChanged(RouteInfo info) {
1707            updateVolume();
1708        }
1709
1710        void updateVolume() {
1711            // A group always represents the highest component volume value.
1712            final int routeCount = getRouteCount();
1713            int volume = 0;
1714            for (int i = 0; i < routeCount; i++) {
1715                final int routeVol = getRouteAt(i).getVolume();
1716                if (routeVol > volume) {
1717                    volume = routeVol;
1718                }
1719            }
1720            if (volume != mVolume) {
1721                mVolume = volume;
1722                dispatchRouteVolumeChanged(this);
1723            }
1724        }
1725
1726        @Override
1727        void routeUpdated() {
1728            int types = 0;
1729            final int count = mRoutes.size();
1730            if (count == 0) {
1731                // Don't keep empty groups in the router.
1732                MediaRouter.removeRoute(this);
1733                return;
1734            }
1735
1736            int maxVolume = 0;
1737            boolean isLocal = true;
1738            boolean isFixedVolume = true;
1739            for (int i = 0; i < count; i++) {
1740                final RouteInfo route = mRoutes.get(i);
1741                types |= route.mSupportedTypes;
1742                final int routeMaxVolume = route.getVolumeMax();
1743                if (routeMaxVolume > maxVolume) {
1744                    maxVolume = routeMaxVolume;
1745                }
1746                isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
1747                isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
1748            }
1749            mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
1750            mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
1751            mSupportedTypes = types;
1752            mVolumeMax = maxVolume;
1753            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
1754            super.routeUpdated();
1755        }
1756
1757        void updateName() {
1758            final StringBuilder sb = new StringBuilder();
1759            final int count = mRoutes.size();
1760            for (int i = 0; i < count; i++) {
1761                final RouteInfo info = mRoutes.get(i);
1762                // TODO: There's probably a much more correct way to localize this.
1763                if (i > 0) sb.append(", ");
1764                sb.append(info.mName);
1765            }
1766            mName = sb.toString();
1767            mUpdateName = false;
1768        }
1769
1770        @Override
1771        public String toString() {
1772            StringBuilder sb = new StringBuilder(super.toString());
1773            sb.append('[');
1774            final int count = mRoutes.size();
1775            for (int i = 0; i < count; i++) {
1776                if (i > 0) sb.append(", ");
1777                sb.append(mRoutes.get(i));
1778            }
1779            sb.append(']');
1780            return sb.toString();
1781        }
1782    }
1783
1784    /**
1785     * Definition of a category of routes. All routes belong to a category.
1786     */
1787    public static class RouteCategory {
1788        CharSequence mName;
1789        int mNameResId;
1790        int mTypes;
1791        final boolean mGroupable;
1792        boolean mIsSystem;
1793
1794        RouteCategory(CharSequence name, int types, boolean groupable) {
1795            mName = name;
1796            mTypes = types;
1797            mGroupable = groupable;
1798        }
1799
1800        RouteCategory(int nameResId, int types, boolean groupable) {
1801            mNameResId = nameResId;
1802            mTypes = types;
1803            mGroupable = groupable;
1804        }
1805
1806        /**
1807         * @return the name of this route category
1808         */
1809        public CharSequence getName() {
1810            return getName(sStatic.mResources);
1811        }
1812
1813        /**
1814         * Return the properly localized/configuration dependent name of this RouteCategory.
1815         *
1816         * @param context Context to resolve name resources
1817         * @return the name of this route category
1818         */
1819        public CharSequence getName(Context context) {
1820            return getName(context.getResources());
1821        }
1822
1823        CharSequence getName(Resources res) {
1824            if (mNameResId != 0) {
1825                return res.getText(mNameResId);
1826            }
1827            return mName;
1828        }
1829
1830        /**
1831         * Return the current list of routes in this category that have been added
1832         * to the MediaRouter.
1833         *
1834         * <p>This list will not include routes that are nested within RouteGroups.
1835         * A RouteGroup is treated as a single route within its category.</p>
1836         *
1837         * @param out a List to fill with the routes in this category. If this parameter is
1838         *            non-null, it will be cleared, filled with the current routes with this
1839         *            category, and returned. If this parameter is null, a new List will be
1840         *            allocated to report the category's current routes.
1841         * @return A list with the routes in this category that have been added to the MediaRouter.
1842         */
1843        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
1844            if (out == null) {
1845                out = new ArrayList<RouteInfo>();
1846            } else {
1847                out.clear();
1848            }
1849
1850            final int count = getRouteCountStatic();
1851            for (int i = 0; i < count; i++) {
1852                final RouteInfo route = getRouteAtStatic(i);
1853                if (route.mCategory == this) {
1854                    out.add(route);
1855                }
1856            }
1857            return out;
1858        }
1859
1860        /**
1861         * @return Flag set describing the route types supported by this category
1862         */
1863        public int getSupportedTypes() {
1864            return mTypes;
1865        }
1866
1867        /**
1868         * Return whether or not this category supports grouping.
1869         *
1870         * <p>If this method returns true, all routes obtained from this category
1871         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
1872         *
1873         * @return true if this category supports
1874         */
1875        public boolean isGroupable() {
1876            return mGroupable;
1877        }
1878
1879        /**
1880         * @return true if this is the category reserved for system routes.
1881         * @hide
1882         */
1883        public boolean isSystem() {
1884            return mIsSystem;
1885        }
1886
1887        public String toString() {
1888            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
1889                    " groupable=" + mGroupable + " }";
1890        }
1891    }
1892
1893    static class CallbackInfo {
1894        public int type;
1895        public final Callback cb;
1896        public final MediaRouter router;
1897
1898        public CallbackInfo(Callback cb, int type, MediaRouter router) {
1899            this.cb = cb;
1900            this.type = type;
1901            this.router = router;
1902        }
1903    }
1904
1905    /**
1906     * Interface for receiving events about media routing changes.
1907     * All methods of this interface will be called from the application's main thread.
1908     *
1909     * <p>A Callback will only receive events relevant to routes that the callback
1910     * was registered for.</p>
1911     *
1912     * @see MediaRouter#addCallback(int, Callback)
1913     * @see MediaRouter#removeCallback(Callback)
1914     */
1915    public static abstract class Callback {
1916        /**
1917         * Called when the supplied route becomes selected as the active route
1918         * for the given route type.
1919         *
1920         * @param router the MediaRouter reporting the event
1921         * @param type Type flag set indicating the routes that have been selected
1922         * @param info Route that has been selected for the given route types
1923         */
1924        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
1925
1926        /**
1927         * Called when the supplied route becomes unselected as the active route
1928         * for the given route type.
1929         *
1930         * @param router the MediaRouter reporting the event
1931         * @param type Type flag set indicating the routes that have been unselected
1932         * @param info Route that has been unselected for the given route types
1933         */
1934        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
1935
1936        /**
1937         * Called when a route for the specified type was added.
1938         *
1939         * @param router the MediaRouter reporting the event
1940         * @param info Route that has become available for use
1941         */
1942        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
1943
1944        /**
1945         * Called when a route for the specified type was removed.
1946         *
1947         * @param router the MediaRouter reporting the event
1948         * @param info Route that has been removed from availability
1949         */
1950        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
1951
1952        /**
1953         * Called when an aspect of the indicated route has changed.
1954         *
1955         * <p>This will not indicate that the types supported by this route have
1956         * changed, only that cosmetic info such as name or status have been updated.</p>
1957         *
1958         * @param router the MediaRouter reporting the event
1959         * @param info The route that was changed
1960         */
1961        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
1962
1963        /**
1964         * Called when a route is added to a group.
1965         *
1966         * @param router the MediaRouter reporting the event
1967         * @param info The route that was added
1968         * @param group The group the route was added to
1969         * @param index The route index within group that info was added at
1970         */
1971        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1972                int index);
1973
1974        /**
1975         * Called when a route is removed from a group.
1976         *
1977         * @param router the MediaRouter reporting the event
1978         * @param info The route that was removed
1979         * @param group The group the route was removed from
1980         */
1981        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
1982
1983        /**
1984         * Called when a route's volume changes.
1985         *
1986         * @param router the MediaRouter reporting the event
1987         * @param info The route with altered volume
1988         */
1989        public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
1990
1991        /**
1992         * Called when a route's presentation display changes.
1993         * <p>
1994         * This method is called whenever the route's presentation display becomes
1995         * available, is removes or has changes to some of its properties (such as its size).
1996         * </p>
1997         *
1998         * @param router the MediaRouter reporting the event
1999         * @param info The route whose presentation display changed
2000         *
2001         * @see RouteInfo#getPresentationDisplay()
2002         */
2003        public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
2004        }
2005    }
2006
2007    /**
2008     * Stub implementation of {@link MediaRouter.Callback}.
2009     * Each abstract method is defined as a no-op. Override just the ones
2010     * you need.
2011     */
2012    public static class SimpleCallback extends Callback {
2013
2014        @Override
2015        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
2016        }
2017
2018        @Override
2019        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
2020        }
2021
2022        @Override
2023        public void onRouteAdded(MediaRouter router, RouteInfo info) {
2024        }
2025
2026        @Override
2027        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
2028        }
2029
2030        @Override
2031        public void onRouteChanged(MediaRouter router, RouteInfo info) {
2032        }
2033
2034        @Override
2035        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
2036                int index) {
2037        }
2038
2039        @Override
2040        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
2041        }
2042
2043        @Override
2044        public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
2045        }
2046    }
2047
2048    static class VolumeCallbackInfo {
2049        public final VolumeCallback vcb;
2050        public final RouteInfo route;
2051
2052        public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
2053            this.vcb = vcb;
2054            this.route = route;
2055        }
2056    }
2057
2058    /**
2059     * Interface for receiving events about volume changes.
2060     * All methods of this interface will be called from the application's main thread.
2061     *
2062     * <p>A VolumeCallback will only receive events relevant to routes that the callback
2063     * was registered for.</p>
2064     *
2065     * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
2066     */
2067    public static abstract class VolumeCallback {
2068        /**
2069         * Called when the volume for the route should be increased or decreased.
2070         * @param info the route affected by this event
2071         * @param direction an integer indicating whether the volume is to be increased
2072         *     (positive value) or decreased (negative value).
2073         *     For bundled changes, the absolute value indicates the number of changes
2074         *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
2075         */
2076        public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
2077        /**
2078         * Called when the volume for the route should be set to the given value
2079         * @param info the route affected by this event
2080         * @param volume an integer indicating the new volume value that should be used, always
2081         *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
2082         */
2083        public abstract void onVolumeSetRequest(RouteInfo info, int volume);
2084    }
2085
2086    static class VolumeChangeReceiver extends BroadcastReceiver {
2087        @Override
2088        public void onReceive(Context context, Intent intent) {
2089            if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
2090                final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
2091                        -1);
2092                if (streamType != AudioManager.STREAM_MUSIC) {
2093                    return;
2094                }
2095
2096                final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
2097                final int oldVolume = intent.getIntExtra(
2098                        AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
2099                if (newVolume != oldVolume) {
2100                    systemVolumeChanged(newVolume);
2101                }
2102            }
2103        }
2104    }
2105
2106    static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
2107        @Override
2108        public void onReceive(Context context, Intent intent) {
2109            if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
2110                updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
2111                        DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
2112            }
2113        }
2114    }
2115}
2116