MediaRouter.java revision 4599696591f745b3a546197d2ba7e5cfc5562484
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.Context;
20import android.content.res.Resources;
21import android.graphics.drawable.Drawable;
22import android.os.Handler;
23import android.os.IBinder;
24import android.os.RemoteException;
25import android.os.ServiceManager;
26import android.text.TextUtils;
27import android.util.Log;
28
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.List;
32import java.util.concurrent.CopyOnWriteArrayList;
33
34/**
35 * MediaRouter allows applications to control the routing of media channels
36 * and streams from the current device to external speakers and destination devices.
37 *
38 * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
39 * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
40 * Context.MEDIA_ROUTER_SERVICE}.
41 *
42 * <p>The media router API is not thread-safe; all interactions with it must be
43 * done from the main thread of the process.</p>
44 */
45public class MediaRouter {
46    private static final String TAG = "MediaRouter";
47
48    static class Static {
49        final Resources mResources;
50        final IAudioService mAudioService;
51        final Handler mHandler;
52        final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
53                new CopyOnWriteArrayList<CallbackInfo>();
54
55        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
56        final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
57
58        final RouteCategory mSystemCategory;
59
60        final AudioRoutesInfo mCurRoutesInfo = new AudioRoutesInfo();
61
62        RouteInfo mDefaultAudio;
63        RouteInfo mBluetoothA2dpRoute;
64
65        RouteInfo mSelectedRoute;
66
67        final IAudioRoutesObserver.Stub mRoutesObserver = new IAudioRoutesObserver.Stub() {
68            public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
69                mHandler.post(new Runnable() {
70                    @Override public void run() {
71                        updateRoutes(newRoutes);
72                    }
73                });
74            }
75        };
76
77        Static(Context appContext) {
78            mResources = Resources.getSystem();
79            mHandler = new Handler(appContext.getMainLooper());
80
81            IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
82            mAudioService = IAudioService.Stub.asInterface(b);
83
84            // XXX this doesn't deal with locale changes!
85            mSystemCategory = new RouteCategory(mResources.getText(
86                    com.android.internal.R.string.default_audio_route_category_name),
87                    ROUTE_TYPE_LIVE_AUDIO, false);
88        }
89
90        // Called after sStatic is initialized
91        void startMonitoringRoutes() {
92            mDefaultAudio = new RouteInfo(mSystemCategory);
93            mDefaultAudio.mNameResId = com.android.internal.R.string.default_audio_route_name;
94            mDefaultAudio.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
95            addRoute(mDefaultAudio);
96
97            AudioRoutesInfo newRoutes = null;
98            try {
99                newRoutes = mAudioService.startWatchingRoutes(mRoutesObserver);
100            } catch (RemoteException e) {
101            }
102            if (newRoutes != null) {
103                updateRoutes(newRoutes);
104            }
105        }
106
107        void updateRoutes(AudioRoutesInfo newRoutes) {
108            if (newRoutes.mMainType != mCurRoutesInfo.mMainType) {
109                mCurRoutesInfo.mMainType = newRoutes.mMainType;
110                int name;
111                if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADPHONES) != 0
112                        || (newRoutes.mMainType&AudioRoutesInfo.MAIN_HEADSET) != 0) {
113                    name = com.android.internal.R.string.default_audio_route_name_headphones;
114                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
115                    name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
116                } else if ((newRoutes.mMainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
117                    name = com.android.internal.R.string.default_audio_route_name_hdmi;
118                } else {
119                    name = com.android.internal.R.string.default_audio_route_name;
120                }
121                sStatic.mDefaultAudio.mNameResId = name;
122                dispatchRouteChanged(sStatic.mDefaultAudio);
123            }
124            if (!TextUtils.equals(newRoutes.mBluetoothName, mCurRoutesInfo.mBluetoothName)) {
125                mCurRoutesInfo.mBluetoothName = newRoutes.mBluetoothName;
126                if (mCurRoutesInfo.mBluetoothName != null) {
127                    if (sStatic.mBluetoothA2dpRoute == null) {
128                        final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
129                        info.mName = mCurRoutesInfo.mBluetoothName;
130                        info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
131                        sStatic.mBluetoothA2dpRoute = info;
132                        addRoute(sStatic.mBluetoothA2dpRoute);
133                    } else {
134                        sStatic.mBluetoothA2dpRoute.mName = mCurRoutesInfo.mBluetoothName;
135                        dispatchRouteChanged(sStatic.mBluetoothA2dpRoute);
136                    }
137                } else if (sStatic.mBluetoothA2dpRoute != null) {
138                    removeRoute(sStatic.mBluetoothA2dpRoute);
139                    sStatic.mBluetoothA2dpRoute = null;
140                }
141            }
142        }
143    }
144
145    static Static sStatic;
146
147    /**
148     * Route type flag for live audio.
149     *
150     * <p>A device that supports live audio routing will allow the media audio stream
151     * to be routed to supported destinations. This can include internal speakers or
152     * audio jacks on the device itself, A2DP devices, and more.</p>
153     *
154     * <p>Once initiated this routing is transparent to the application. All audio
155     * played on the media stream will be routed to the selected destination.</p>
156     */
157    public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
158
159    /**
160     * Route type flag for application-specific usage.
161     *
162     * <p>Unlike other media route types, user routes are managed by the application.
163     * The MediaRouter will manage and dispatch events for user routes, but the application
164     * is expected to interpret the meaning of these events and perform the requested
165     * routing tasks.</p>
166     */
167    public static final int ROUTE_TYPE_USER = 0x00800000;
168
169    // Maps application contexts
170    static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
171
172    static String typesToString(int types) {
173        final StringBuilder result = new StringBuilder();
174        if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
175            result.append("ROUTE_TYPE_LIVE_AUDIO ");
176        }
177        if ((types & ROUTE_TYPE_USER) != 0) {
178            result.append("ROUTE_TYPE_USER ");
179        }
180        return result.toString();
181    }
182
183    /** @hide */
184    public MediaRouter(Context context) {
185        synchronized (Static.class) {
186            if (sStatic == null) {
187                sStatic = new Static(context.getApplicationContext());
188                sStatic.startMonitoringRoutes();
189            }
190        }
191    }
192
193    /**
194     * @hide for use by framework routing UI
195     */
196    public RouteInfo getSystemAudioRoute() {
197        return sStatic.mDefaultAudio;
198    }
199
200    /**
201     * @hide for use by framework routing UI
202     */
203    public RouteCategory getSystemAudioCategory() {
204        return sStatic.mSystemCategory;
205    }
206
207    /**
208     * Return the currently selected route for the given types
209     *
210     * @param type route types
211     * @return the selected route
212     */
213    public RouteInfo getSelectedRoute(int type) {
214        return sStatic.mSelectedRoute;
215    }
216
217    /**
218     * Add a callback to listen to events about specific kinds of media routes.
219     * If the specified callback is already registered, its registration will be updated for any
220     * additional route types specified.
221     *
222     * @param types Types of routes this callback is interested in
223     * @param cb Callback to add
224     */
225    public void addCallback(int types, Callback cb) {
226        final int count = sStatic.mCallbacks.size();
227        for (int i = 0; i < count; i++) {
228            final CallbackInfo info = sStatic.mCallbacks.get(i);
229            if (info.cb == cb) {
230                info.type &= types;
231                return;
232            }
233        }
234        sStatic.mCallbacks.add(new CallbackInfo(cb, types, this));
235    }
236
237    /**
238     * Remove the specified callback. It will no longer receive events about media routing.
239     *
240     * @param cb Callback to remove
241     */
242    public void removeCallback(Callback cb) {
243        final int count = sStatic.mCallbacks.size();
244        for (int i = 0; i < count; i++) {
245            if (sStatic.mCallbacks.get(i).cb == cb) {
246                sStatic.mCallbacks.remove(i);
247                return;
248            }
249        }
250        Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
251    }
252
253    /**
254     * Select the specified route to use for output of the given media types.
255     *
256     * @param types type flags indicating which types this route should be used for.
257     *              The route must support at least a subset.
258     * @param route Route to select
259     */
260    public void selectRoute(int types, RouteInfo route) {
261        // Applications shouldn't programmatically change anything but user routes.
262        types &= ROUTE_TYPE_USER;
263        selectRouteStatic(types, route);
264    }
265
266    /**
267     * @hide internal use
268     */
269    public void selectRouteInt(int types, RouteInfo route) {
270        selectRouteStatic(types, route);
271    }
272
273    static void selectRouteStatic(int types, RouteInfo route) {
274        if (sStatic.mSelectedRoute == route) return;
275        if ((route.getSupportedTypes() & types) == 0) {
276            Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
277                    typesToString(route.getSupportedTypes()) + " into route types " +
278                    typesToString(types));
279        }
280
281        if (sStatic.mSelectedRoute != null) {
282            // TODO filter types properly
283            dispatchRouteUnselected(types & sStatic.mSelectedRoute.getSupportedTypes(),
284                    sStatic.mSelectedRoute);
285        }
286        sStatic.mSelectedRoute = route;
287        if (route != null) {
288            // TODO filter types properly
289            dispatchRouteSelected(types & route.getSupportedTypes(), route);
290        }
291    }
292
293    /**
294     * Add an app-specified route for media to the MediaRouter.
295     * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
296     *
297     * @param info Definition of the route to add
298     * @see #createUserRoute()
299     * @see #removeUserRoute(UserRouteInfo)
300     */
301    public void addUserRoute(UserRouteInfo info) {
302        addRoute(info);
303    }
304
305    /**
306     * @hide Framework use only
307     */
308    public void addRouteInt(RouteInfo info) {
309        addRoute(info);
310    }
311
312    static void addRoute(RouteInfo info) {
313        final RouteCategory cat = info.getCategory();
314        if (!sStatic.mCategories.contains(cat)) {
315            sStatic.mCategories.add(cat);
316        }
317        final boolean onlyRoute = sStatic.mRoutes.isEmpty();
318        if (cat.isGroupable() && !(info instanceof RouteGroup)) {
319            // Enforce that any added route in a groupable category must be in a group.
320            final RouteGroup group = new RouteGroup(info.getCategory());
321            sStatic.mRoutes.add(group);
322            dispatchRouteAdded(group);
323            group.addRoute(info);
324
325            info = group;
326        } else {
327            sStatic.mRoutes.add(info);
328            dispatchRouteAdded(info);
329        }
330
331        if (onlyRoute) {
332            selectRouteStatic(info.getSupportedTypes(), info);
333        }
334    }
335
336    /**
337     * Remove an app-specified route for media from the MediaRouter.
338     *
339     * @param info Definition of the route to remove
340     * @see #addUserRoute(UserRouteInfo)
341     */
342    public void removeUserRoute(UserRouteInfo info) {
343        removeRoute(info);
344    }
345
346    /**
347     * Remove all app-specified routes from the MediaRouter.
348     *
349     * @see #removeUserRoute(UserRouteInfo)
350     */
351    public void clearUserRoutes() {
352        for (int i = 0; i < sStatic.mRoutes.size(); i++) {
353            final RouteInfo info = sStatic.mRoutes.get(i);
354            // TODO Right now, RouteGroups only ever contain user routes.
355            // The code below will need to change if this assumption does.
356            if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
357                removeRouteAt(i);
358                i--;
359            }
360        }
361    }
362
363    /**
364     * @hide internal use only
365     */
366    public void removeRouteInt(RouteInfo info) {
367        removeRoute(info);
368    }
369
370    static void removeRoute(RouteInfo info) {
371        if (sStatic.mRoutes.remove(info)) {
372            final RouteCategory removingCat = info.getCategory();
373            final int count = sStatic.mRoutes.size();
374            boolean found = false;
375            for (int i = 0; i < count; i++) {
376                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
377                if (removingCat == cat) {
378                    found = true;
379                    break;
380                }
381            }
382            if (info == sStatic.mSelectedRoute) {
383                // Removing the currently selected route? Select the default before we remove it.
384                // TODO: Be smarter about the route types here; this selects for all valid.
385                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
386            }
387            if (!found) {
388                sStatic.mCategories.remove(removingCat);
389            }
390            dispatchRouteRemoved(info);
391        }
392    }
393
394    void removeRouteAt(int routeIndex) {
395        if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) {
396            final RouteInfo info = sStatic.mRoutes.remove(routeIndex);
397            final RouteCategory removingCat = info.getCategory();
398            final int count = sStatic.mRoutes.size();
399            boolean found = false;
400            for (int i = 0; i < count; i++) {
401                final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
402                if (removingCat == cat) {
403                    found = true;
404                    break;
405                }
406            }
407            if (info == sStatic.mSelectedRoute) {
408                // Removing the currently selected route? Select the default before we remove it.
409                // TODO: Be smarter about the route types here; this selects for all valid.
410                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
411            }
412            if (!found) {
413                sStatic.mCategories.remove(removingCat);
414            }
415            dispatchRouteRemoved(info);
416        }
417    }
418
419    /**
420     * Return the number of {@link MediaRouter.RouteCategory categories} currently
421     * represented by routes known to this MediaRouter.
422     *
423     * @return the number of unique categories represented by this MediaRouter's known routes
424     */
425    public int getCategoryCount() {
426        return sStatic.mCategories.size();
427    }
428
429    /**
430     * Return the {@link MediaRouter.RouteCategory category} at the given index.
431     * Valid indices are in the range [0-getCategoryCount).
432     *
433     * @param index which category to return
434     * @return the category at index
435     */
436    public RouteCategory getCategoryAt(int index) {
437        return sStatic.mCategories.get(index);
438    }
439
440    /**
441     * Return the number of {@link MediaRouter.RouteInfo routes} currently known
442     * to this MediaRouter.
443     *
444     * @return the number of routes tracked by this router
445     */
446    public int getRouteCount() {
447        return sStatic.mRoutes.size();
448    }
449
450    /**
451     * Return the route at the specified index.
452     *
453     * @param index index of the route to return
454     * @return the route at index
455     */
456    public RouteInfo getRouteAt(int index) {
457        return sStatic.mRoutes.get(index);
458    }
459
460    static int getRouteCountStatic() {
461        return sStatic.mRoutes.size();
462    }
463
464    static RouteInfo getRouteAtStatic(int index) {
465        return sStatic.mRoutes.get(index);
466    }
467
468    /**
469     * Create a new user route that may be modified and registered for use by the application.
470     *
471     * @param category The category the new route will belong to
472     * @return A new UserRouteInfo for use by the application
473     *
474     * @see #addUserRoute(UserRouteInfo)
475     * @see #removeUserRoute(UserRouteInfo)
476     * @see #createRouteCategory(CharSequence)
477     */
478    public UserRouteInfo createUserRoute(RouteCategory category) {
479        return new UserRouteInfo(category);
480    }
481
482    /**
483     * Create a new route category. Each route must belong to a category.
484     *
485     * @param name Name of the new category
486     * @param isGroupable true if routes in this category may be grouped with one another
487     * @return the new RouteCategory
488     */
489    public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
490        return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
491    }
492
493    /**
494     * Create a new route category. Each route must belong to a category.
495     *
496     * @param nameResId Resource ID of the name of the new category
497     * @param isGroupable true if routes in this category may be grouped with one another
498     * @return the new RouteCategory
499     */
500    public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
501        return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
502    }
503
504    static void updateRoute(final RouteInfo info) {
505        dispatchRouteChanged(info);
506    }
507
508    static void dispatchRouteSelected(int type, RouteInfo info) {
509        for (CallbackInfo cbi : sStatic.mCallbacks) {
510            if ((cbi.type & type) != 0) {
511                cbi.cb.onRouteSelected(cbi.router, type, info);
512            }
513        }
514    }
515
516    static void dispatchRouteUnselected(int type, RouteInfo info) {
517        for (CallbackInfo cbi : sStatic.mCallbacks) {
518            if ((cbi.type & type) != 0) {
519                cbi.cb.onRouteUnselected(cbi.router, type, info);
520            }
521        }
522    }
523
524    static void dispatchRouteChanged(RouteInfo info) {
525        for (CallbackInfo cbi : sStatic.mCallbacks) {
526            if ((cbi.type & info.mSupportedTypes) != 0) {
527                cbi.cb.onRouteChanged(cbi.router, info);
528            }
529        }
530    }
531
532    static void dispatchRouteAdded(RouteInfo info) {
533        for (CallbackInfo cbi : sStatic.mCallbacks) {
534            if ((cbi.type & info.mSupportedTypes) != 0) {
535                cbi.cb.onRouteAdded(cbi.router, info);
536            }
537        }
538    }
539
540    static void dispatchRouteRemoved(RouteInfo info) {
541        for (CallbackInfo cbi : sStatic.mCallbacks) {
542            if ((cbi.type & info.mSupportedTypes) != 0) {
543                cbi.cb.onRouteRemoved(cbi.router, info);
544            }
545        }
546    }
547
548    static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
549        for (CallbackInfo cbi : sStatic.mCallbacks) {
550            if ((cbi.type & group.mSupportedTypes) != 0) {
551                cbi.cb.onRouteGrouped(cbi.router, info, group, index);
552            }
553        }
554    }
555
556    static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
557        for (CallbackInfo cbi : sStatic.mCallbacks) {
558            if ((cbi.type & group.mSupportedTypes) != 0) {
559                cbi.cb.onRouteUngrouped(cbi.router, info, group);
560            }
561        }
562    }
563
564    /**
565     * Information about a media route.
566     */
567    public static class RouteInfo {
568        CharSequence mName;
569        int mNameResId;
570        private CharSequence mStatus;
571        int mSupportedTypes;
572        RouteGroup mGroup;
573        final RouteCategory mCategory;
574        Drawable mIcon;
575
576        private Object mTag;
577
578        RouteInfo(RouteCategory category) {
579            mCategory = category;
580        }
581
582        /**
583         * @return The user-friendly name of a media route. This is the string presented
584         * to users who may select this as the active route.
585         */
586        public CharSequence getName() {
587            return getName(sStatic.mResources);
588        }
589
590        /**
591         * Return the properly localized/resource selected name of this route.
592         *
593         * @param context Context used to resolve the correct configuration to load
594         * @return The user-friendly name of the media route. This is the string presented
595         * to users who may select this as the active route.
596         */
597        public CharSequence getName(Context context) {
598            return getName(context.getResources());
599        }
600
601        CharSequence getName(Resources res) {
602            if (mNameResId != 0) {
603                return mName = res.getText(mNameResId);
604            }
605            return mName;
606        }
607
608        /**
609         * @return The user-friendly status for a media route. This may include a description
610         * of the currently playing media, if available.
611         */
612        public CharSequence getStatus() {
613            return mStatus;
614        }
615
616        /**
617         * @return A media type flag set describing which types this route supports.
618         */
619        public int getSupportedTypes() {
620            return mSupportedTypes;
621        }
622
623        /**
624         * @return The group that this route belongs to.
625         */
626        public RouteGroup getGroup() {
627            return mGroup;
628        }
629
630        /**
631         * @return the category this route belongs to.
632         */
633        public RouteCategory getCategory() {
634            return mCategory;
635        }
636
637        /**
638         * Get the icon representing this route.
639         * This icon will be used in picker UIs if available.
640         *
641         * @return the icon representing this route or null if no icon is available
642         */
643        public Drawable getIconDrawable() {
644            return mIcon;
645        }
646
647        /**
648         * Set an application-specific tag object for this route.
649         * The application may use this to store arbitrary data associated with the
650         * route for internal tracking.
651         *
652         * <p>Note that the lifespan of a route may be well past the lifespan of
653         * an Activity or other Context; take care that objects you store here
654         * will not keep more data in memory alive than you intend.</p>
655         *
656         * @param tag Arbitrary, app-specific data for this route to hold for later use
657         */
658        public void setTag(Object tag) {
659            mTag = tag;
660            routeUpdated();
661        }
662
663        /**
664         * @return The tag object previously set by the application
665         * @see #setTag(Object)
666         */
667        public Object getTag() {
668            return mTag;
669        }
670
671        void setStatusInt(CharSequence status) {
672            if (!status.equals(mStatus)) {
673                mStatus = status;
674                if (mGroup != null) {
675                    mGroup.memberStatusChanged(this, status);
676                }
677                routeUpdated();
678            }
679        }
680
681        void routeUpdated() {
682            updateRoute(this);
683        }
684
685        @Override
686        public String toString() {
687            String supportedTypes = typesToString(getSupportedTypes());
688            return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() +
689                    " category=" + getCategory() +
690                    " supportedTypes=" + supportedTypes + "}";
691        }
692    }
693
694    /**
695     * Information about a route that the application may define and modify.
696     *
697     * @see MediaRouter.RouteInfo
698     */
699    public static class UserRouteInfo extends RouteInfo {
700        RemoteControlClient mRcc;
701
702        UserRouteInfo(RouteCategory category) {
703            super(category);
704            mSupportedTypes = ROUTE_TYPE_USER;
705        }
706
707        /**
708         * Set the user-visible name of this route.
709         * @param name Name to display to the user to describe this route
710         */
711        public void setName(CharSequence name) {
712            mName = name;
713            routeUpdated();
714        }
715
716        /**
717         * Set the user-visible name of this route.
718         * @param resId Resource ID of the name to display to the user to describe this route
719         */
720        public void setName(int resId) {
721            mNameResId = resId;
722            mName = null;
723            routeUpdated();
724        }
725
726        /**
727         * Set the current user-visible status for this route.
728         * @param status Status to display to the user to describe what the endpoint
729         * of this route is currently doing
730         */
731        public void setStatus(CharSequence status) {
732            setStatusInt(status);
733        }
734
735        /**
736         * Set the RemoteControlClient responsible for reporting playback info for this
737         * user route.
738         *
739         * <p>If this route manages remote playback, the data exposed by this
740         * RemoteControlClient will be used to reflect and update information
741         * such as route volume info in related UIs.</p>
742         *
743         * @param rcc RemoteControlClient associated with this route
744         */
745        public void setRemoteControlClient(RemoteControlClient rcc) {
746            mRcc = rcc;
747        }
748
749        /**
750         * Retrieve the RemoteControlClient associated with this route, if one has been set.
751         *
752         * @return the RemoteControlClient associated with this route
753         * @see #setRemoteControlClient(RemoteControlClient)
754         */
755        public RemoteControlClient getRemoteControlClient() {
756            return mRcc;
757        }
758
759        /**
760         * Set an icon that will be used to represent this route.
761         * The system may use this icon in picker UIs or similar.
762         *
763         * @param icon icon drawable to use to represent this route
764         */
765        public void setIconDrawable(Drawable icon) {
766            mIcon = icon;
767        }
768
769        /**
770         * Set an icon that will be used to represent this route.
771         * The system may use this icon in picker UIs or similar.
772         *
773         * @param resId Resource ID of an icon drawable to use to represent this route
774         */
775        public void setIconResource(int resId) {
776            setIconDrawable(sStatic.mResources.getDrawable(resId));
777        }
778    }
779
780    /**
781     * Information about a route that consists of multiple other routes in a group.
782     */
783    public static class RouteGroup extends RouteInfo {
784        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
785        private boolean mUpdateName;
786
787        RouteGroup(RouteCategory category) {
788            super(category);
789            mGroup = this;
790        }
791
792        CharSequence getName(Resources res) {
793            if (mUpdateName) updateName();
794            return super.getName(res);
795        }
796
797        /**
798         * Add a route to this group. The route must not currently belong to another group.
799         *
800         * @param route route to add to this group
801         */
802        public void addRoute(RouteInfo route) {
803            if (route.getGroup() != null) {
804                throw new IllegalStateException("Route " + route + " is already part of a group.");
805            }
806            if (route.getCategory() != mCategory) {
807                throw new IllegalArgumentException(
808                        "Route cannot be added to a group with a different category. " +
809                            "(Route category=" + route.getCategory() +
810                            " group category=" + mCategory + ")");
811            }
812            final int at = mRoutes.size();
813            mRoutes.add(route);
814            route.mGroup = this;
815            mUpdateName = true;
816            dispatchRouteGrouped(route, this, at);
817            routeUpdated();
818        }
819
820        /**
821         * Add a route to this group before the specified index.
822         *
823         * @param route route to add
824         * @param insertAt insert the new route before this index
825         */
826        public void addRoute(RouteInfo route, int insertAt) {
827            if (route.getGroup() != null) {
828                throw new IllegalStateException("Route " + route + " is already part of a group.");
829            }
830            if (route.getCategory() != mCategory) {
831                throw new IllegalArgumentException(
832                        "Route cannot be added to a group with a different category. " +
833                            "(Route category=" + route.getCategory() +
834                            " group category=" + mCategory + ")");
835            }
836            mRoutes.add(insertAt, route);
837            route.mGroup = this;
838            mUpdateName = true;
839            dispatchRouteGrouped(route, this, insertAt);
840            routeUpdated();
841        }
842
843        /**
844         * Remove a route from this group.
845         *
846         * @param route route to remove
847         */
848        public void removeRoute(RouteInfo route) {
849            if (route.getGroup() != this) {
850                throw new IllegalArgumentException("Route " + route +
851                        " is not a member of this group.");
852            }
853            mRoutes.remove(route);
854            route.mGroup = null;
855            mUpdateName = true;
856            dispatchRouteUngrouped(route, this);
857            routeUpdated();
858        }
859
860        /**
861         * Remove the route at the specified index from this group.
862         *
863         * @param index index of the route to remove
864         */
865        public void removeRoute(int index) {
866            RouteInfo route = mRoutes.remove(index);
867            route.mGroup = null;
868            mUpdateName = true;
869            dispatchRouteUngrouped(route, this);
870            routeUpdated();
871        }
872
873        /**
874         * @return The number of routes in this group
875         */
876        public int getRouteCount() {
877            return mRoutes.size();
878        }
879
880        /**
881         * Return the route in this group at the specified index
882         *
883         * @param index Index to fetch
884         * @return The route at index
885         */
886        public RouteInfo getRouteAt(int index) {
887            return mRoutes.get(index);
888        }
889
890        /**
891         * Set an icon that will be used to represent this group.
892         * The system may use this icon in picker UIs or similar.
893         *
894         * @param icon icon drawable to use to represent this group
895         */
896        public void setIconDrawable(Drawable icon) {
897            mIcon = icon;
898        }
899
900        /**
901         * Set an icon that will be used to represent this group.
902         * The system may use this icon in picker UIs or similar.
903         *
904         * @param resId Resource ID of an icon drawable to use to represent this group
905         */
906        public void setIconResource(int resId) {
907            setIconDrawable(sStatic.mResources.getDrawable(resId));
908        }
909
910        void memberNameChanged(RouteInfo info, CharSequence name) {
911            mUpdateName = true;
912            routeUpdated();
913        }
914
915        void memberStatusChanged(RouteInfo info, CharSequence status) {
916            setStatusInt(status);
917        }
918
919        @Override
920        void routeUpdated() {
921            int types = 0;
922            final int count = mRoutes.size();
923            if (count == 0) {
924                // Don't keep empty groups in the router.
925                MediaRouter.removeRoute(this);
926                return;
927            }
928
929            for (int i = 0; i < count; i++) {
930                types |= mRoutes.get(i).mSupportedTypes;
931            }
932            mSupportedTypes = types;
933            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
934            super.routeUpdated();
935        }
936
937        void updateName() {
938            final StringBuilder sb = new StringBuilder();
939            final int count = mRoutes.size();
940            for (int i = 0; i < count; i++) {
941                final RouteInfo info = mRoutes.get(i);
942                // TODO: There's probably a much more correct way to localize this.
943                if (i > 0) sb.append(", ");
944                sb.append(info.mName);
945            }
946            mName = sb.toString();
947            mUpdateName = false;
948        }
949
950        @Override
951        public String toString() {
952            StringBuilder sb = new StringBuilder(super.toString());
953            sb.append('[');
954            final int count = mRoutes.size();
955            for (int i = 0; i < count; i++) {
956                if (i > 0) sb.append(", ");
957                sb.append(mRoutes.get(i));
958            }
959            sb.append(']');
960            return sb.toString();
961        }
962    }
963
964    /**
965     * Definition of a category of routes. All routes belong to a category.
966     */
967    public static class RouteCategory {
968        CharSequence mName;
969        int mNameResId;
970        int mTypes;
971        final boolean mGroupable;
972
973        RouteCategory(CharSequence name, int types, boolean groupable) {
974            mName = name;
975            mTypes = types;
976            mGroupable = groupable;
977        }
978
979        RouteCategory(int nameResId, int types, boolean groupable) {
980            mNameResId = nameResId;
981            mTypes = types;
982            mGroupable = groupable;
983        }
984
985        /**
986         * @return the name of this route category
987         */
988        public CharSequence getName() {
989            return getName(sStatic.mResources);
990        }
991
992        /**
993         * Return the properly localized/configuration dependent name of this RouteCategory.
994         *
995         * @param context Context to resolve name resources
996         * @return the name of this route category
997         */
998        public CharSequence getName(Context context) {
999            return getName(context.getResources());
1000        }
1001
1002        CharSequence getName(Resources res) {
1003            if (mNameResId != 0) {
1004                return res.getText(mNameResId);
1005            }
1006            return mName;
1007        }
1008
1009        /**
1010         * Return the current list of routes in this category that have been added
1011         * to the MediaRouter.
1012         *
1013         * <p>This list will not include routes that are nested within RouteGroups.
1014         * A RouteGroup is treated as a single route within its category.</p>
1015         *
1016         * @param out a List to fill with the routes in this category. If this parameter is
1017         *            non-null, it will be cleared, filled with the current routes with this
1018         *            category, and returned. If this parameter is null, a new List will be
1019         *            allocated to report the category's current routes.
1020         * @return A list with the routes in this category that have been added to the MediaRouter.
1021         */
1022        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
1023            if (out == null) {
1024                out = new ArrayList<RouteInfo>();
1025            } else {
1026                out.clear();
1027            }
1028
1029            final int count = getRouteCountStatic();
1030            for (int i = 0; i < count; i++) {
1031                final RouteInfo route = getRouteAtStatic(i);
1032                if (route.mCategory == this) {
1033                    out.add(route);
1034                }
1035            }
1036            return out;
1037        }
1038
1039        /**
1040         * @return Flag set describing the route types supported by this category
1041         */
1042        public int getSupportedTypes() {
1043            return mTypes;
1044        }
1045
1046        /**
1047         * Return whether or not this category supports grouping.
1048         *
1049         * <p>If this method returns true, all routes obtained from this category
1050         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
1051         *
1052         * @return true if this category supports
1053         */
1054        public boolean isGroupable() {
1055            return mGroupable;
1056        }
1057
1058        public String toString() {
1059            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
1060                    " groupable=" + mGroupable + " }";
1061        }
1062    }
1063
1064    static class CallbackInfo {
1065        public int type;
1066        public final Callback cb;
1067        public final MediaRouter router;
1068
1069        public CallbackInfo(Callback cb, int type, MediaRouter router) {
1070            this.cb = cb;
1071            this.type = type;
1072            this.router = router;
1073        }
1074    }
1075
1076    /**
1077     * Interface for receiving events about media routing changes.
1078     * All methods of this interface will be called from the application's main thread.
1079     *
1080     * <p>A Callback will only receive events relevant to routes that the callback
1081     * was registered for.</p>
1082     *
1083     * @see MediaRouter#addCallback(int, Callback)
1084     * @see MediaRouter#removeCallback(Callback)
1085     */
1086    public static abstract class Callback {
1087        /**
1088         * Called when the supplied route becomes selected as the active route
1089         * for the given route type.
1090         *
1091         * @param router the MediaRouter reporting the event
1092         * @param type Type flag set indicating the routes that have been selected
1093         * @param info Route that has been selected for the given route types
1094         */
1095        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
1096
1097        /**
1098         * Called when the supplied route becomes unselected as the active route
1099         * for the given route type.
1100         *
1101         * @param router the MediaRouter reporting the event
1102         * @param type Type flag set indicating the routes that have been unselected
1103         * @param info Route that has been unselected for the given route types
1104         */
1105        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
1106
1107        /**
1108         * Called when a route for the specified type was added.
1109         *
1110         * @param router the MediaRouter reporting the event
1111         * @param info Route that has become available for use
1112         */
1113        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
1114
1115        /**
1116         * Called when a route for the specified type was removed.
1117         *
1118         * @param router the MediaRouter reporting the event
1119         * @param info Route that has been removed from availability
1120         */
1121        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
1122
1123        /**
1124         * Called when an aspect of the indicated route has changed.
1125         *
1126         * <p>This will not indicate that the types supported by this route have
1127         * changed, only that cosmetic info such as name or status have been updated.</p>
1128         *
1129         * @param router the MediaRouter reporting the event
1130         * @param info The route that was changed
1131         */
1132        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
1133
1134        /**
1135         * Called when a route is added to a group.
1136         *
1137         * @param router the MediaRouter reporting the event
1138         * @param info The route that was added
1139         * @param group The group the route was added to
1140         * @param index The route index within group that info was added at
1141         */
1142        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1143                int index);
1144
1145        /**
1146         * Called when a route is removed from a group.
1147         *
1148         * @param router the MediaRouter reporting the event
1149         * @param info The route that was removed
1150         * @param group The group the route was removed from
1151         */
1152        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
1153    }
1154
1155    /**
1156     * Stub implementation of {@link MediaRouter.Callback}.
1157     * Each abstract method is defined as a no-op. Override just the ones
1158     * you need.
1159     */
1160    public static class SimpleCallback extends Callback {
1161
1162        @Override
1163        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
1164        }
1165
1166        @Override
1167        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
1168        }
1169
1170        @Override
1171        public void onRouteAdded(MediaRouter router, RouteInfo info) {
1172        }
1173
1174        @Override
1175        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
1176        }
1177
1178        @Override
1179        public void onRouteChanged(MediaRouter router, RouteInfo info) {
1180        }
1181
1182        @Override
1183        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1184                int index) {
1185        }
1186
1187        @Override
1188        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
1189        }
1190
1191    }
1192}
1193