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