MediaRouter.java revision 690ffb4e1f735148a15f2036d9a3c1962fba188c
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.bluetooth.BluetoothA2dp;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.os.Handler;
25import android.util.Log;
26
27import java.util.ArrayList;
28import java.util.HashMap;
29
30/**
31 * MediaRouter allows applications to control the routing of media channels
32 * and streams from the current device to external speakers and destination devices.
33 *
34 * <p>Media routes should only be modified on your application's main thread.</p>
35 */
36public class MediaRouter {
37    private static final String TAG = "MediaRouter";
38
39    private Context mAppContext;
40    private AudioManager mAudioManager;
41    private Handler mHandler;
42    private final ArrayList<CallbackInfo> mCallbacks = new ArrayList<CallbackInfo>();
43
44    private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
45    private final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
46
47    private final RouteCategory mSystemCategory;
48    private RouteInfo mDefaultAudio;
49    private RouteInfo mBluetoothA2dpRoute;
50
51    private RouteInfo mSelectedRoute;
52
53    // These get removed when an activity dies
54    final ArrayList<BroadcastReceiver> mRegisteredReceivers = new ArrayList<BroadcastReceiver>();
55
56    /**
57     * Route type flag for live audio.
58     *
59     * <p>A device that supports live audio routing will allow the media audio stream
60     * to be routed to supported destinations. This can include internal speakers or
61     * audio jacks on the device itself, A2DP devices, and more.</p>
62     *
63     * <p>Once initiated this routing is transparent to the application. All audio
64     * played on the media stream will be routed to the selected destination.</p>
65     */
66    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
67
68    /**
69     * Route type flag for application-specific usage.
70     *
71     * <p>Unlike other media route types, user routes are managed by the application.
72     * The MediaRouter will manage and dispatch events for user routes, but the application
73     * is expected to interpret the meaning of these events and perform the requested
74     * routing tasks.</p>
75     */
76    public static final int ROUTE_TYPE_USER = 0x00800000;
77
78    // Maps application contexts
79    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
80
81    /**
82     * Return a MediaRouter for the application that the specified Context belongs to.
83     * The behavior or availability of media routing may depend on
84     * various parameters of the context.
85     *
86     * @param context Context for the desired router
87     * @return Router for the supplied Context
88     */
89    public static MediaRouter forApplication(Context context) {
90        final Context appContext = context.getApplicationContext();
91        if (!sRouters.containsKey(appContext)) {
92            final MediaRouter r = new MediaRouter(appContext);
93            sRouters.put(appContext, r);
94            return r;
95        } else {
96            return sRouters.get(appContext);
97        }
98    }
99
100    static String typesToString(int types) {
101        final StringBuilder result = new StringBuilder();
102        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
103            result.append("ROUTE_TYPE_LIVE_AUDIO ");
104        }
105        if ((types & ROUTE_TYPE_USER) != 0) {
106            result.append("ROUTE_TYPE_USER ");
107        }
108        return result.toString();
109    }
110
111    private MediaRouter(Context context) {
112        mAppContext = context;
113        mHandler = new Handler(mAppContext.getMainLooper());
114
115        mAudioManager = (AudioManager) mAppContext.getSystemService(Context.AUDIO_SERVICE);
116
117        mSystemCategory = new RouteCategory(mAppContext.getText(
118                com.android.internal.R.string.default_audio_route_category_name),
119                ROUTE_TYPE_LIVE_AUDIO, false);
120
121        registerReceivers();
122
123        createDefaultRoutes();
124    }
125
126    private void registerReceivers() {
127        final BroadcastReceiver volumeReceiver = new VolumeChangedBroadcastReceiver();
128        mAppContext.registerReceiver(volumeReceiver,
129                new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
130        mRegisteredReceivers.add(volumeReceiver);
131
132        final IntentFilter speakerFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
133        speakerFilter.addAction(Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG);
134        speakerFilter.addAction(Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG);
135        speakerFilter.addAction(Intent.ACTION_HDMI_AUDIO_PLUG);
136        final BroadcastReceiver plugReceiver = new HeadphoneChangedBroadcastReceiver();
137        mAppContext.registerReceiver(plugReceiver, speakerFilter);
138        mRegisteredReceivers.add(plugReceiver);
139    }
140
141    void unregisterReceivers() {
142        final int count = mRegisteredReceivers.size();
143        for (int i = 0; i < count; i++) {
144            final BroadcastReceiver r = mRegisteredReceivers.get(i);
145            mAppContext.unregisterReceiver(r);
146        }
147        mRegisteredReceivers.clear();
148    }
149
150    private void createDefaultRoutes() {
151        mDefaultAudio = new RouteInfo(mSystemCategory);
152        mDefaultAudio.mName = mAppContext.getText(
153                com.android.internal.R.string.default_audio_route_name);
154        mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
155        final int maxMusicVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
156        if (maxMusicVolume > 0) {
157            mDefaultAudio.mVolume =
158                    mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / maxMusicVolume;
159        }
160        addRoute(mDefaultAudio);
161    }
162
163    /**
164     * @hide for use by framework routing UI
165     */
166    public RouteInfo getSystemAudioRoute() {
167        return mDefaultAudio;
168    }
169
170    /**
171     * Return the currently selected route for the given types
172     *
173     * @param type route types
174     * @return the selected route
175     */
176    public RouteInfo getSelectedRoute(int type) {
177        return mSelectedRoute;
178    }
179
180    void onHeadphonesPlugged(boolean headphonesPresent, String headphonesName) {
181        mDefaultAudio.mName = headphonesPresent ? headphonesName : mAppContext.getText(
182                com.android.internal.R.string.default_audio_route_name);
183        dispatchRouteChanged(mDefaultAudio);
184    }
185
186    /**
187     * Set volume for the specified selected route types.
188     *
189     * @param types Volume will be set for these route types
190     * @param volume Volume to set in the range 0.f (inaudible) to 1.f (full volume).
191     */
192    public void setSelectedRouteVolume(int types, float volume) {
193        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0 && mSelectedRoute == mDefaultAudio) {
194            final int index = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
195            mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, index, 0);
196        }
197        if ((types & ROUTE_TYPE_USER) != 0 && mSelectedRoute instanceof UserRouteInfo) {
198            mSelectedRoute.mVolume = volume;
199            dispatchVolumeChanged(ROUTE_TYPE_USER, volume);
200        }
201    }
202
203    /**
204     * Add a callback to listen to events about specific kinds of media routes.
205     * If the specified callback is already registered, its registration will be updated for any
206     * additional route types specified.
207     *
208     * @param types Types of routes this callback is interested in
209     * @param cb Callback to add
210     */
211    public void addCallback(int types, Callback cb) {
212        final int count = mCallbacks.size();
213        for (int i = 0; i < count; i++) {
214            final CallbackInfo info = mCallbacks.get(i);
215            if (info.cb == cb) {
216                info.type &= types;
217                return;
218            }
219        }
220        mCallbacks.add(new CallbackInfo(cb, types));
221    }
222
223    /**
224     * Remove the specified callback. It will no longer receive events about media routing.
225     *
226     * @param cb Callback to remove
227     */
228    public void removeCallback(Callback cb) {
229        final int count = mCallbacks.size();
230        for (int i = 0; i < count; i++) {
231            if (mCallbacks.get(i).cb == cb) {
232                mCallbacks.remove(i);
233                return;
234            }
235        }
236        Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
237    }
238
239    public void selectRoute(int types, RouteInfo route) {
240        if (mSelectedRoute == route) return;
241
242        if (mSelectedRoute != null) {
243            // TODO filter types properly
244            dispatchRouteUnselected(types & mSelectedRoute.getSupportedTypes(), mSelectedRoute);
245        }
246        mSelectedRoute = route;
247        if (route != null) {
248            // TODO filter types properly
249            dispatchRouteSelected(types & route.getSupportedTypes(), route);
250        }
251    }
252
253    /**
254     * Add an app-specified route for media to the MediaRouter.
255     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
256     *
257     * @param info Definition of the route to add
258     * @see #createUserRoute()
259     * @see #removeUserRoute(UserRouteInfo)
260     */
261    public void addUserRoute(UserRouteInfo info) {
262        addRoute(info);
263    }
264
265    void addRoute(RouteInfo info) {
266        final RouteCategory cat = info.getCategory();
267        if (!mCategories.contains(cat)) {
268            mCategories.add(cat);
269        }
270        if (info.getCategory().isGroupable() && !(info instanceof RouteGroup)) {
271            // Enforce that any added route in a groupable category must be in a group.
272            final RouteGroup group = new RouteGroup(info.getCategory());
273            group.addRoute(info);
274            info = group;
275        }
276        final boolean onlyRoute = mRoutes.isEmpty();
277        mRoutes.add(info);
278        dispatchRouteAdded(info);
279        if (onlyRoute) {
280            selectRoute(info.getSupportedTypes(), info);
281        }
282    }
283
284    /**
285     * Remove an app-specified route for media from the MediaRouter.
286     *
287     * @param info Definition of the route to remove
288     * @see #addUserRoute(UserRouteInfo)
289     */
290    public void removeUserRoute(UserRouteInfo info) {
291        removeRoute(info);
292    }
293
294    /**
295     * Remove all app-specified routes from the MediaRouter.
296     *
297     * @see #removeUserRoute(UserRouteInfo)
298     */
299    public void clearUserRoutes() {
300        for (int i = 0; i < mRoutes.size(); i++) {
301            final RouteInfo info = mRoutes.get(i);
302            if (info instanceof UserRouteInfo) {
303                removeRouteAt(i);
304                i--;
305            }
306        }
307    }
308
309    void removeRoute(RouteInfo info) {
310        if (mRoutes.remove(info)) {
311            final RouteCategory removingCat = info.getCategory();
312            final int count = mRoutes.size();
313            boolean found = false;
314            for (int i = 0; i < count; i++) {
315                final RouteCategory cat = mRoutes.get(i).getCategory();
316                if (removingCat == cat) {
317                    found = true;
318                    break;
319                }
320            }
321            if (!found) {
322                mCategories.remove(removingCat);
323            }
324            dispatchRouteRemoved(info);
325        }
326    }
327
328    void removeRouteAt(int routeIndex) {
329        if (routeIndex >= 0 && routeIndex < mRoutes.size()) {
330            final RouteInfo info = mRoutes.remove(routeIndex);
331            final RouteCategory removingCat = info.getCategory();
332            final int count = mRoutes.size();
333            boolean found = false;
334            for (int i = 0; i < count; i++) {
335                final RouteCategory cat = mRoutes.get(i).getCategory();
336                if (removingCat == cat) {
337                    found = true;
338                    break;
339                }
340            }
341            if (!found) {
342                mCategories.remove(removingCat);
343            }
344            dispatchRouteRemoved(info);
345        }
346    }
347
348    /**
349     * Return the number of {@link MediaRouter.RouteCategory categories} currently
350     * represented by routes known to this MediaRouter.
351     *
352     * @return the number of unique categories represented by this MediaRouter's known routes
353     */
354    public int getCategoryCount() {
355        return mCategories.size();
356    }
357
358    /**
359     * Return the {@link MediaRouter.RouteCategory category} at the given index.
360     * Valid indices are in the range [0-getCategoryCount).
361     *
362     * @param index which category to return
363     * @return the category at index
364     */
365    public RouteCategory getCategoryAt(int index) {
366        return mCategories.get(index);
367    }
368
369    /**
370     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
371     * to this MediaRouter.
372     *
373     * @return the number of routes tracked by this router
374     */
375    public int getRouteCount() {
376        return mRoutes.size();
377    }
378
379    /**
380     * Return the route at the specified index.
381     *
382     * @param index index of the route to return
383     * @return the route at index
384     */
385    public RouteInfo getRouteAt(int index) {
386        return mRoutes.get(index);
387    }
388
389    /**
390     * Create a new user route that may be modified and registered for use by the application.
391     *
392     * @param category The category the new route will belong to
393     * @return A new UserRouteInfo for use by the application
394     *
395     * @see #addUserRoute(UserRouteInfo)
396     * @see #removeUserRoute(UserRouteInfo)
397     * @see #createRouteCategory(CharSequence)
398     */
399    public UserRouteInfo createUserRoute(RouteCategory category) {
400        return new UserRouteInfo(category);
401    }
402
403    /**
404     * Create a new route category. Each route must belong to a category.
405     *
406     * @param name Name of the new category
407     * @param isGroupable true if routes in this category may be grouped with one another
408     * @return the new RouteCategory
409     */
410    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
411        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
412    }
413
414    void updateRoute(final RouteInfo info) {
415        dispatchRouteChanged(info);
416    }
417
418    void dispatchRouteSelected(int type, RouteInfo info) {
419        final int count = mCallbacks.size();
420        for (int i = 0; i < count; i++) {
421            final CallbackInfo cbi = mCallbacks.get(i);
422            if ((cbi.type & type) != 0) {
423                cbi.cb.onRouteSelected(type, info);
424            }
425        }
426    }
427
428    void dispatchRouteUnselected(int type, RouteInfo info) {
429        final int count = mCallbacks.size();
430        for (int i = 0; i < count; i++) {
431            final CallbackInfo cbi = mCallbacks.get(i);
432            if ((cbi.type & type) != 0) {
433                cbi.cb.onRouteUnselected(type, info);
434            }
435        }
436    }
437
438    void dispatchRouteChanged(RouteInfo info) {
439        final int count = mCallbacks.size();
440        for (int i = 0; i < count; i++) {
441            final CallbackInfo cbi = mCallbacks.get(i);
442            if ((cbi.type & info.mSupportedTypes) != 0) {
443                cbi.cb.onRouteChanged(info);
444            }
445        }
446    }
447
448    void dispatchRouteAdded(RouteInfo info) {
449        final int count = mCallbacks.size();
450        for (int i = 0; i < count; i++) {
451            final CallbackInfo cbi = mCallbacks.get(i);
452            if ((cbi.type & info.mSupportedTypes) != 0) {
453                cbi.cb.onRouteAdded(info.mSupportedTypes, info);
454            }
455        }
456    }
457
458    void dispatchRouteRemoved(RouteInfo info) {
459        final int count = mCallbacks.size();
460        for (int i = 0; i < count; i++) {
461            final CallbackInfo cbi = mCallbacks.get(i);
462            if ((cbi.type & info.mSupportedTypes) != 0) {
463                cbi.cb.onRouteRemoved(info.mSupportedTypes, info);
464            }
465        }
466    }
467
468    void dispatchVolumeChanged(int type, float volume) {
469        final int count = mCallbacks.size();
470        for (int i = 0; i < count; i++) {
471            final CallbackInfo cbi = mCallbacks.get(i);
472            if ((cbi.type & type) != 0) {
473                cbi.cb.onVolumeChanged(type, volume);
474            }
475        }
476    }
477
478    void onA2dpDeviceConnected() {
479        final RouteInfo info = new RouteInfo(mSystemCategory);
480        info.mName = "Bluetooth"; // TODO Fetch the real name of the device
481        mBluetoothA2dpRoute = info;
482        addRoute(mBluetoothA2dpRoute);
483    }
484
485    void onA2dpDeviceDisconnected() {
486        removeRoute(mBluetoothA2dpRoute);
487        mBluetoothA2dpRoute = null;
488    }
489
490    /**
491     * Information about a media route.
492     */
493    public class RouteInfo {
494        CharSequence mName;
495        private CharSequence mStatus;
496        int mSupportedTypes;
497        RouteGroup mGroup;
498        final RouteCategory mCategory;
499        float mVolume;
500
501        RouteInfo(RouteCategory category) {
502            mCategory = category;
503            category.mRoutes.add(this);
504        }
505
506        /**
507         * @return The user-friendly name of a media route. This is the string presented
508         * to users who may select this as the active route.
509         */
510        public CharSequence getName() {
511            return mName;
512        }
513
514        /**
515         * @return The user-friendly status for a media route. This may include a description
516         * of the currently playing media, if available.
517         */
518        public CharSequence getStatus() {
519            return mStatus;
520        }
521
522        /**
523         * @return A media type flag set describing which types this route supports.
524         */
525        public int getSupportedTypes() {
526            return mSupportedTypes;
527        }
528
529        /**
530         * @return The group that this route belongs to.
531         */
532        public RouteGroup getGroup() {
533            return mGroup;
534        }
535
536        /**
537         * @return the category this route belongs to.
538         */
539        public RouteCategory getCategory() {
540            return mCategory;
541        }
542
543        /**
544         * @return This route's current volume setting.
545         */
546        public float getVolume() {
547            return mVolume;
548        }
549
550        void setStatusInt(CharSequence status) {
551            if (!status.equals(mStatus)) {
552                mStatus = status;
553                routeUpdated();
554                if (mGroup != null) {
555                    mGroup.memberStatusChanged(this, status);
556                }
557                routeUpdated();
558            }
559        }
560
561        void routeUpdated() {
562            updateRoute(this);
563        }
564
565        @Override
566        public String toString() {
567            String supportedTypes = typesToString(mSupportedTypes);
568            return "RouteInfo{ name=" + mName + ", status=" + mStatus +
569                    " category=" + mCategory +
570                    " supportedTypes=" + supportedTypes + "}";
571        }
572    }
573
574    /**
575     * Information about a route that the application may define and modify.
576     *
577     * @see MediaRouter.RouteInfo
578     */
579    public class UserRouteInfo extends RouteInfo {
580
581        UserRouteInfo(RouteCategory category) {
582            super(category);
583            mSupportedTypes = ROUTE_TYPE_USER;
584        }
585
586        /**
587         * Set the user-visible name of this route.
588         * @param name Name to display to the user to describe this route
589         */
590        public void setName(CharSequence name) {
591            mName = name;
592            routeUpdated();
593        }
594
595        /**
596         * Set the current user-visible status for this route.
597         * @param status Status to display to the user to describe what the endpoint
598         * of this route is currently doing
599         */
600        public void setStatus(CharSequence status) {
601            setStatusInt(status);
602        }
603    }
604
605    /**
606     * Information about a route that consists of multiple other routes in a group.
607     */
608    public class RouteGroup extends RouteInfo {
609        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
610        private boolean mUpdateName;
611
612        RouteGroup(RouteCategory category) {
613            super(category);
614            mGroup = this;
615        }
616
617        public CharSequence getName() {
618            if (mUpdateName) updateName();
619            return super.getName();
620        }
621
622        /**
623         * Add a route to this group. The route must not currently belong to another group.
624         *
625         * @param route route to add to this group
626         */
627        public void addRoute(RouteInfo route) {
628            if (route.getGroup() != null) {
629                throw new IllegalStateException("Route " + route + " is already part of a group.");
630            }
631            if (route.getCategory() != mCategory) {
632                throw new IllegalArgumentException(
633                        "Route cannot be added to a group with a different category. " +
634                            "(Route category=" + route.getCategory() +
635                            " group category=" + mCategory + ")");
636            }
637            mRoutes.add(route);
638            mUpdateName = true;
639            routeUpdated();
640        }
641
642        /**
643         * Add a route to this group before the specified index.
644         *
645         * @param route route to add
646         * @param insertAt insert the new route before this index
647         */
648        public void addRoute(RouteInfo route, int insertAt) {
649            if (route.getGroup() != null) {
650                throw new IllegalStateException("Route " + route + " is already part of a group.");
651            }
652            if (route.getCategory() != mCategory) {
653                throw new IllegalArgumentException(
654                        "Route cannot be added to a group with a different category. " +
655                            "(Route category=" + route.getCategory() +
656                            " group category=" + mCategory + ")");
657            }
658            mRoutes.add(insertAt, route);
659            mUpdateName = true;
660            routeUpdated();
661        }
662
663        /**
664         * Remove a route from this group.
665         *
666         * @param route route to remove
667         */
668        public void removeRoute(RouteInfo route) {
669            if (route.getGroup() != this) {
670                throw new IllegalArgumentException("Route " + route +
671                        " is not a member of this group.");
672            }
673            mRoutes.remove(route);
674            mUpdateName = true;
675            routeUpdated();
676        }
677
678        /**
679         * Remove the route at the specified index from this group.
680         *
681         * @param index index of the route to remove
682         */
683        public void removeRoute(int index) {
684            mRoutes.remove(index);
685            mUpdateName = true;
686            routeUpdated();
687        }
688
689        void memberNameChanged(RouteInfo info, CharSequence name) {
690            mUpdateName = true;
691            routeUpdated();
692        }
693
694        void memberStatusChanged(RouteInfo info, CharSequence status) {
695            setStatusInt(status);
696        }
697
698        void updateName() {
699            final StringBuilder sb = new StringBuilder();
700            final int count = mRoutes.size();
701            for (int i = 0; i < count; i++) {
702                final RouteInfo info = mRoutes.get(i);
703                if (i > 0) sb.append(", ");
704                sb.append(info.mName);
705            }
706            mName = sb.toString();
707            mUpdateName = false;
708        }
709    }
710
711    /**
712     * Definition of a category of routes. All routes belong to a category.
713     */
714    public class RouteCategory {
715        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
716        CharSequence mName;
717        int mTypes;
718        final boolean mGroupable;
719
720        RouteCategory(CharSequence name, int types, boolean groupable) {
721            mName = name;
722            mTypes = types;
723            mGroupable = groupable;
724        }
725
726        /**
727         * @return the name of this route category
728         */
729        public CharSequence getName() {
730            return mName;
731        }
732
733        /**
734         * @return the number of routes in this category
735         */
736        public int getRouteCount() {
737            return mRoutes.size();
738        }
739
740        /**
741         * Return a route from this category
742         *
743         * @param index Index from [0-getRouteCount)
744         * @return the route at the given index
745         */
746        public RouteInfo getRouteAt(int index) {
747            return mRoutes.get(index);
748        }
749
750        /**
751         * @return Flag set describing the route types supported by this category
752         */
753        public int getSupportedTypes() {
754            return mTypes;
755        }
756
757        /**
758         * Return whether or not this category supports grouping.
759         *
760         * <p>If this method returns true, all routes obtained from this category
761         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.
762         *
763         * @return true if this category supports
764         */
765        public boolean isGroupable() {
766            return mGroupable;
767        }
768
769        public String toString() {
770            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
771                    " groupable=" + mGroupable + " routes=" + mRoutes.size() + " }";
772        }
773    }
774
775    static class CallbackInfo {
776        public int type;
777        public Callback cb;
778
779        public CallbackInfo(Callback cb, int type) {
780            this.cb = cb;
781            this.type = type;
782        }
783    }
784
785    /**
786     * Interface for receiving events about media routing changes.
787     * All methods of this interface will be called from the application's main thread.
788     *
789     * <p>A Callback will only receive events relevant to routes that the callback
790     * was registered for.</p>
791     *
792     * @see MediaRouter#addCallback(int, Callback)
793     * @see MediaRouter#removeCallback(Callback)
794     */
795    public interface Callback {
796        /**
797         * Called when the supplied route becomes selected as the active route
798         * for the given route type.
799         *
800         * @param type Type flag set indicating the routes that have been selected
801         * @param info Route that has been selected for the given route types
802         */
803        public void onRouteSelected(int type, RouteInfo info);
804
805        /**
806         * Called when the supplied route becomes unselected as the active route
807         * for the given route type.
808         *
809         * @param type Type flag set indicating the routes that have been unselected
810         * @param info Route that has been unselected for the given route types
811         */
812        public void onRouteUnselected(int type, RouteInfo info);
813
814        /**
815         * Called when the volume is changed for the specified route types.
816         *
817         * @param type Type flags indicating which volume type was changed
818         * @param volume New volume value in the range 0 (inaudible) to 1 (full)
819         */
820        public void onVolumeChanged(int type, float volume);
821
822        /**
823         * Called when a route for the specified type was added.
824         *
825         * @param type Type flags indicating which types the added route supports
826         * @param info Route that has become available for use
827         */
828        public void onRouteAdded(int type, RouteInfo info);
829
830        /**
831         * Called when a route for the specified type was removed.
832         *
833         * @param type Type flags indicating which types the removed route supported
834         * @param info Route that has been removed from availability
835         */
836        public void onRouteRemoved(int type, RouteInfo info);
837
838        /**
839         * Called when an aspect of the indicated route has changed.
840         *
841         * <p>This will not indicate that the types supported by this route have
842         * changed, only that cosmetic info such as name or status have been updated.</p>
843         *
844         * @param info The route that was changed
845         */
846        public void onRouteChanged(RouteInfo info);
847    }
848
849    /**
850     * Stub implementation of the {@link MediaRouter.Callback} interface.
851     * Each interface method is defined as a no-op. Override just the ones
852     * you need.
853     */
854    public static class SimpleCallback implements Callback {
855
856        @Override
857        public void onRouteSelected(int type, RouteInfo info) {
858
859        }
860
861        @Override
862        public void onRouteUnselected(int type, RouteInfo info) {
863
864        }
865
866        @Override
867        public void onVolumeChanged(int type, float volume) {
868
869        }
870
871        @Override
872        public void onRouteAdded(int type, RouteInfo info) {
873
874        }
875
876        @Override
877        public void onRouteRemoved(int type, RouteInfo info) {
878
879        }
880
881        @Override
882        public void onRouteChanged(RouteInfo info) {
883
884        }
885
886    }
887
888    class VolumeChangedBroadcastReceiver extends BroadcastReceiver {
889        @Override
890        public void onReceive(Context context, Intent intent) {
891            final String action = intent.getAction();
892            if (AudioManager.VOLUME_CHANGED_ACTION.equals(action) &&
893                    AudioManager.STREAM_MUSIC == intent.getIntExtra(
894                            AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1)) {
895                final int maxVol = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
896                final int volExtra = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
897                final float volume = (float) volExtra / maxVol;
898                mDefaultAudio.mVolume = volume;
899                dispatchVolumeChanged(ROUTE_TYPE_LIVE_AUDIO, volume);
900            }
901        }
902    }
903
904    class BtChangedBroadcastReceiver extends BroadcastReceiver {
905        @Override
906        public void onReceive(Context context, Intent intent) {
907            final String action = intent.getAction();
908            if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
909                final int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1);
910                if (state == BluetoothA2dp.STATE_CONNECTED) {
911                    onA2dpDeviceConnected();
912                } else if (state == BluetoothA2dp.STATE_DISCONNECTING ||
913                        state == BluetoothA2dp.STATE_DISCONNECTED) {
914                    onA2dpDeviceDisconnected();
915                }
916            }
917        }
918    }
919
920    class HeadphoneChangedBroadcastReceiver extends BroadcastReceiver {
921        @Override
922        public void onReceive(Context context, Intent intent) {
923            final String action = intent.getAction();
924            if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
925                final boolean plugged = intent.getIntExtra("state", 0) != 0;
926                final String name = mAppContext.getString(
927                        com.android.internal.R.string.default_audio_route_name_headphones);
928                onHeadphonesPlugged(plugged, name);
929            } else if (Intent.ACTION_ANALOG_AUDIO_DOCK_PLUG.equals(action) ||
930                    Intent.ACTION_DIGITAL_AUDIO_DOCK_PLUG.equals(action)) {
931                final boolean plugged = intent.getIntExtra("state", 0) != 0;
932                final String name = mAppContext.getString(
933                        com.android.internal.R.string.default_audio_route_name_dock_speakers);
934                onHeadphonesPlugged(plugged, name);
935            } else if (Intent.ACTION_HDMI_AUDIO_PLUG.equals(action)) {
936                final boolean plugged = intent.getIntExtra("state", 0) != 0;
937                final String name = mAppContext.getString(
938                        com.android.internal.R.string.default_audio_route_name_hdmi);
939                onHeadphonesPlugged(plugged, name);
940            }
941        }
942    }
943}
944