MediaRouter.java revision 2cd5d253786b115470adc04a3609358f5eb7eb0a
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        // playback information
593        int mPlaybackType = PLAYBACK_TYPE_LOCAL;
594        int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
595        int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
596        int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
597        int mPlaybackStream = AudioManager.STREAM_MUSIC;
598        VolumeCallbackInfo mVcb;
599
600        private Object mTag;
601
602        /**
603         * The default playback type, "local", indicating the presentation of the media is happening
604         * on the same device (e.g. a phone, a tablet) as where it is controlled from.
605         * @see #setPlaybackType(int)
606         */
607        public final static int PLAYBACK_TYPE_LOCAL = 0;
608        /**
609         * A playback type indicating the presentation of the media is happening on
610         * a different device (i.e. the remote device) than where it is controlled from.
611         * @see #setPlaybackType(int)
612         */
613        public final static int PLAYBACK_TYPE_REMOTE = 1;
614        /**
615         * Playback information indicating the playback volume is fixed, i.e. it cannot be
616         * controlled from this object. An example of fixed playback volume is a remote player,
617         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
618         * than attenuate at the source.
619         * @see #setVolumeHandling(int)
620         */
621        public final static int PLAYBACK_VOLUME_FIXED = 0;
622        /**
623         * Playback information indicating the playback volume is variable and can be controlled
624         * from this object.
625         */
626        public final static int PLAYBACK_VOLUME_VARIABLE = 1;
627
628        RouteInfo(RouteCategory category) {
629            mCategory = category;
630        }
631
632        /**
633         * @return The user-friendly name of a media route. This is the string presented
634         * to users who may select this as the active route.
635         */
636        public CharSequence getName() {
637            return getName(sStatic.mResources);
638        }
639
640        /**
641         * Return the properly localized/resource selected name of this route.
642         *
643         * @param context Context used to resolve the correct configuration to load
644         * @return The user-friendly name of the media route. This is the string presented
645         * to users who may select this as the active route.
646         */
647        public CharSequence getName(Context context) {
648            return getName(context.getResources());
649        }
650
651        CharSequence getName(Resources res) {
652            if (mNameResId != 0) {
653                return mName = res.getText(mNameResId);
654            }
655            return mName;
656        }
657
658        /**
659         * @return The user-friendly status for a media route. This may include a description
660         * of the currently playing media, if available.
661         */
662        public CharSequence getStatus() {
663            return mStatus;
664        }
665
666        /**
667         * @return A media type flag set describing which types this route supports.
668         */
669        public int getSupportedTypes() {
670            return mSupportedTypes;
671        }
672
673        /**
674         * @return The group that this route belongs to.
675         */
676        public RouteGroup getGroup() {
677            return mGroup;
678        }
679
680        /**
681         * @return the category this route belongs to.
682         */
683        public RouteCategory getCategory() {
684            return mCategory;
685        }
686
687        /**
688         * Get the icon representing this route.
689         * This icon will be used in picker UIs if available.
690         *
691         * @return the icon representing this route or null if no icon is available
692         */
693        public Drawable getIconDrawable() {
694            return mIcon;
695        }
696
697        /**
698         * Set an application-specific tag object for this route.
699         * The application may use this to store arbitrary data associated with the
700         * route for internal tracking.
701         *
702         * <p>Note that the lifespan of a route may be well past the lifespan of
703         * an Activity or other Context; take care that objects you store here
704         * will not keep more data in memory alive than you intend.</p>
705         *
706         * @param tag Arbitrary, app-specific data for this route to hold for later use
707         */
708        public void setTag(Object tag) {
709            mTag = tag;
710            routeUpdated();
711        }
712
713        /**
714         * @return The tag object previously set by the application
715         * @see #setTag(Object)
716         */
717        public Object getTag() {
718            return mTag;
719        }
720
721        /**
722         * @return the type of playback associated with this route
723         * @see UserRouteInfo#setPlaybackType(int)
724         */
725        public int getPlaybackType() {
726            return mPlaybackType;
727        }
728
729        /**
730         * @return the stream over which the playback associated with this route is performed
731         * @see UserRouteInfo#setPlaybackStream(int)
732         */
733        public int getPlaybackStream() {
734            return mPlaybackStream;
735        }
736
737        /**
738         * @return the volume at which the playback associated with this route is performed
739         * @see UserRouteInfo#setVolume(int)
740         */
741        public int getVolume() {
742            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
743                int vol = 0;
744                try {
745                    vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
746                } catch (RemoteException e) {
747                    Log.e(TAG, "Error getting local stream volume", e);
748                }
749                return vol;
750            } else {
751                return mVolume;
752            }
753        }
754
755        /**
756         * @return the maximum volume at which the playback associated with this route is performed
757         * @see UserRouteInfo#setVolumeMax(int)
758         */
759        public int getVolumeMax() {
760            if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
761                int volMax = 0;
762                try {
763                    volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
764                } catch (RemoteException e) {
765                    Log.e(TAG, "Error getting local stream volume", e);
766                }
767                return volMax;
768            } else {
769                return mVolumeMax;
770            }
771        }
772
773        /**
774         * @return how volume is handling on the route
775         * @see UserRouteInfo#setVolumeHandling(int)
776         */
777        public int getVolumeHandling() {
778            return mVolumeHandling;
779        }
780
781        void setStatusInt(CharSequence status) {
782            if (!status.equals(mStatus)) {
783                mStatus = status;
784                if (mGroup != null) {
785                    mGroup.memberStatusChanged(this, status);
786                }
787                routeUpdated();
788            }
789        }
790
791        final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
792            public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
793                sStatic.mHandler.post(new Runnable() {
794                    @Override
795                    public void run() {
796                      //Log.d(TAG, "dispatchRemoteVolumeUpdate dir=" + direction + " val=" + value);
797                        if (mVcb != null) {
798                            if (direction != 0) {
799                                mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
800                            } else {
801                                mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
802                            }
803                        }
804                    }
805                });
806            }
807        };
808
809        void routeUpdated() {
810            updateRoute(this);
811        }
812
813        @Override
814        public String toString() {
815            String supportedTypes = typesToString(getSupportedTypes());
816            return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() +
817                    " category=" + getCategory() +
818                    " supportedTypes=" + supportedTypes + "}";
819        }
820    }
821
822    /**
823     * Information about a route that the application may define and modify.
824     *
825     * @see MediaRouter.RouteInfo
826     */
827    public static class UserRouteInfo extends RouteInfo {
828        RemoteControlClient mRcc;
829
830        UserRouteInfo(RouteCategory category) {
831            super(category);
832            mSupportedTypes = ROUTE_TYPE_USER;
833        }
834
835        /**
836         * Set the user-visible name of this route.
837         * @param name Name to display to the user to describe this route
838         */
839        public void setName(CharSequence name) {
840            mName = name;
841            routeUpdated();
842        }
843
844        /**
845         * Set the user-visible name of this route.
846         * @param resId Resource ID of the name to display to the user to describe this route
847         */
848        public void setName(int resId) {
849            mNameResId = resId;
850            mName = null;
851            routeUpdated();
852        }
853
854        /**
855         * Set the current user-visible status for this route.
856         * @param status Status to display to the user to describe what the endpoint
857         * of this route is currently doing
858         */
859        public void setStatus(CharSequence status) {
860            setStatusInt(status);
861        }
862
863        /**
864         * Set the RemoteControlClient responsible for reporting playback info for this
865         * user route.
866         *
867         * <p>If this route manages remote playback, the data exposed by this
868         * RemoteControlClient will be used to reflect and update information
869         * such as route volume info in related UIs.</p>
870         *
871         * <p>The RemoteControlClient must have been previously registered with
872         * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
873         *
874         * @param rcc RemoteControlClient associated with this route
875         */
876        public void setRemoteControlClient(RemoteControlClient rcc) {
877            mRcc = rcc;
878            updatePlaybackInfoOnRcc();
879        }
880
881        /**
882         * Retrieve the RemoteControlClient associated with this route, if one has been set.
883         *
884         * @return the RemoteControlClient associated with this route
885         * @see #setRemoteControlClient(RemoteControlClient)
886         */
887        public RemoteControlClient getRemoteControlClient() {
888            return mRcc;
889        }
890
891        /**
892         * Set an icon that will be used to represent this route.
893         * The system may use this icon in picker UIs or similar.
894         *
895         * @param icon icon drawable to use to represent this route
896         */
897        public void setIconDrawable(Drawable icon) {
898            mIcon = icon;
899        }
900
901        /**
902         * Set an icon that will be used to represent this route.
903         * The system may use this icon in picker UIs or similar.
904         *
905         * @param resId Resource ID of an icon drawable to use to represent this route
906         */
907        public void setIconResource(int resId) {
908            setIconDrawable(sStatic.mResources.getDrawable(resId));
909        }
910
911        /**
912         * Set a callback to be notified of volume update requests
913         * @param vcb
914         */
915        public void setVolumeCallback(VolumeCallback vcb) {
916            mVcb = new VolumeCallbackInfo(vcb, this);
917        }
918
919        /**
920         * Defines whether playback associated with this route is "local"
921         *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
922         *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
923         * @param type
924         */
925        public void setPlaybackType(int type) {
926            if (mPlaybackType != type) {
927                mPlaybackType = type;
928                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, type);
929            }
930        }
931
932        /**
933         * Defines whether volume for the playback associated with this route is fixed
934         * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
935         * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
936         * @param volumeHandling
937         */
938        public void setVolumeHandling(int volumeHandling) {
939            if (mVolumeHandling != volumeHandling) {
940                mVolumeHandling = volumeHandling;
941                setPlaybackInfoOnRcc(
942                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, volumeHandling);
943            }
944        }
945
946        /**
947         * Defines at what volume the playback associated with this route is performed (for user
948         * feedback purposes). This information is only used when the playback is not local.
949         * @param volume
950         */
951        public void setVolume(int volume) {
952            if (mVolume != volume) {
953                mVolume = volume;
954                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME, volume);
955            }
956        }
957
958        /**
959         * Defines the maximum volume at which the playback associated with this route is performed
960         * (for user feedback purposes). This information is only used when the playback is not
961         * local.
962         * @param volumeMax
963         */
964        public void setVolumeMax(int volumeMax) {
965            if (mVolumeMax != volumeMax) {
966                mVolumeMax = volumeMax;
967                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, volumeMax);
968            }
969        }
970
971        /**
972         * Defines over what stream type the media is presented.
973         * @param stream
974         */
975        public void setPlaybackStream(int stream) {
976            if (mPlaybackStream != stream) {
977                mPlaybackStream = stream;
978                setPlaybackInfoOnRcc(RemoteControlClient.PLAYBACKINFO_USES_STREAM, stream);
979            }
980        }
981
982        private void updatePlaybackInfoOnRcc() {
983            if ((mRcc != null) && (mRcc.getRcseId() != RemoteControlClient.RCSE_ID_UNREGISTERED)) {
984                mRcc.setPlaybackInformation(
985                        RemoteControlClient.PLAYBACKINFO_VOLUME_MAX, mVolumeMax);
986                mRcc.setPlaybackInformation(
987                        RemoteControlClient.PLAYBACKINFO_VOLUME, mVolume);
988                mRcc.setPlaybackInformation(
989                        RemoteControlClient.PLAYBACKINFO_VOLUME_HANDLING, mVolumeHandling);
990                mRcc.setPlaybackInformation(
991                        RemoteControlClient.PLAYBACKINFO_USES_STREAM, mPlaybackStream);
992                mRcc.setPlaybackInformation(
993                        RemoteControlClient.PLAYBACKINFO_PLAYBACK_TYPE, mPlaybackType);
994                // let AudioService know whom to call when remote volume needs to be updated
995                try {
996                    sStatic.mAudioService.registerRemoteVolumeObserverForRcc(
997                            mRcc.getRcseId() /* rccId */, mRemoteVolObserver /* rvo */);
998                } catch (RemoteException e) {
999                    Log.e(TAG, "Error registering remote volume observer", e);
1000                }
1001            }
1002        }
1003
1004        private void setPlaybackInfoOnRcc(int what, int value) {
1005            if (mRcc != null) {
1006                mRcc.setPlaybackInformation(what, value);
1007            }
1008        }
1009    }
1010
1011    /**
1012     * Information about a route that consists of multiple other routes in a group.
1013     */
1014    public static class RouteGroup extends RouteInfo {
1015        final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
1016        private boolean mUpdateName;
1017
1018        RouteGroup(RouteCategory category) {
1019            super(category);
1020            mGroup = this;
1021        }
1022
1023        CharSequence getName(Resources res) {
1024            if (mUpdateName) updateName();
1025            return super.getName(res);
1026        }
1027
1028        /**
1029         * Add a route to this group. The route must not currently belong to another group.
1030         *
1031         * @param route route to add to this group
1032         */
1033        public void addRoute(RouteInfo route) {
1034            if (route.getGroup() != null) {
1035                throw new IllegalStateException("Route " + route + " is already part of a group.");
1036            }
1037            if (route.getCategory() != mCategory) {
1038                throw new IllegalArgumentException(
1039                        "Route cannot be added to a group with a different category. " +
1040                            "(Route category=" + route.getCategory() +
1041                            " group category=" + mCategory + ")");
1042            }
1043            final int at = mRoutes.size();
1044            mRoutes.add(route);
1045            route.mGroup = this;
1046            mUpdateName = true;
1047            dispatchRouteGrouped(route, this, at);
1048            routeUpdated();
1049        }
1050
1051        /**
1052         * Add a route to this group before the specified index.
1053         *
1054         * @param route route to add
1055         * @param insertAt insert the new route before this index
1056         */
1057        public void addRoute(RouteInfo route, int insertAt) {
1058            if (route.getGroup() != null) {
1059                throw new IllegalStateException("Route " + route + " is already part of a group.");
1060            }
1061            if (route.getCategory() != mCategory) {
1062                throw new IllegalArgumentException(
1063                        "Route cannot be added to a group with a different category. " +
1064                            "(Route category=" + route.getCategory() +
1065                            " group category=" + mCategory + ")");
1066            }
1067            mRoutes.add(insertAt, route);
1068            route.mGroup = this;
1069            mUpdateName = true;
1070            dispatchRouteGrouped(route, this, insertAt);
1071            routeUpdated();
1072        }
1073
1074        /**
1075         * Remove a route from this group.
1076         *
1077         * @param route route to remove
1078         */
1079        public void removeRoute(RouteInfo route) {
1080            if (route.getGroup() != this) {
1081                throw new IllegalArgumentException("Route " + route +
1082                        " is not a member of this group.");
1083            }
1084            mRoutes.remove(route);
1085            route.mGroup = null;
1086            mUpdateName = true;
1087            dispatchRouteUngrouped(route, this);
1088            routeUpdated();
1089        }
1090
1091        /**
1092         * Remove the route at the specified index from this group.
1093         *
1094         * @param index index of the route to remove
1095         */
1096        public void removeRoute(int index) {
1097            RouteInfo route = mRoutes.remove(index);
1098            route.mGroup = null;
1099            mUpdateName = true;
1100            dispatchRouteUngrouped(route, this);
1101            routeUpdated();
1102        }
1103
1104        /**
1105         * @return The number of routes in this group
1106         */
1107        public int getRouteCount() {
1108            return mRoutes.size();
1109        }
1110
1111        /**
1112         * Return the route in this group at the specified index
1113         *
1114         * @param index Index to fetch
1115         * @return The route at index
1116         */
1117        public RouteInfo getRouteAt(int index) {
1118            return mRoutes.get(index);
1119        }
1120
1121        /**
1122         * Set an icon that will be used to represent this group.
1123         * The system may use this icon in picker UIs or similar.
1124         *
1125         * @param icon icon drawable to use to represent this group
1126         */
1127        public void setIconDrawable(Drawable icon) {
1128            mIcon = icon;
1129        }
1130
1131        /**
1132         * Set an icon that will be used to represent this group.
1133         * The system may use this icon in picker UIs or similar.
1134         *
1135         * @param resId Resource ID of an icon drawable to use to represent this group
1136         */
1137        public void setIconResource(int resId) {
1138            setIconDrawable(sStatic.mResources.getDrawable(resId));
1139        }
1140
1141        void memberNameChanged(RouteInfo info, CharSequence name) {
1142            mUpdateName = true;
1143            routeUpdated();
1144        }
1145
1146        void memberStatusChanged(RouteInfo info, CharSequence status) {
1147            setStatusInt(status);
1148        }
1149
1150        @Override
1151        void routeUpdated() {
1152            int types = 0;
1153            final int count = mRoutes.size();
1154            if (count == 0) {
1155                // Don't keep empty groups in the router.
1156                MediaRouter.removeRoute(this);
1157                return;
1158            }
1159
1160            for (int i = 0; i < count; i++) {
1161                types |= mRoutes.get(i).mSupportedTypes;
1162            }
1163            mSupportedTypes = types;
1164            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
1165            super.routeUpdated();
1166        }
1167
1168        void updateName() {
1169            final StringBuilder sb = new StringBuilder();
1170            final int count = mRoutes.size();
1171            for (int i = 0; i < count; i++) {
1172                final RouteInfo info = mRoutes.get(i);
1173                // TODO: There's probably a much more correct way to localize this.
1174                if (i > 0) sb.append(", ");
1175                sb.append(info.mName);
1176            }
1177            mName = sb.toString();
1178            mUpdateName = false;
1179        }
1180
1181        @Override
1182        public String toString() {
1183            StringBuilder sb = new StringBuilder(super.toString());
1184            sb.append('[');
1185            final int count = mRoutes.size();
1186            for (int i = 0; i < count; i++) {
1187                if (i > 0) sb.append(", ");
1188                sb.append(mRoutes.get(i));
1189            }
1190            sb.append(']');
1191            return sb.toString();
1192        }
1193    }
1194
1195    /**
1196     * Definition of a category of routes. All routes belong to a category.
1197     */
1198    public static class RouteCategory {
1199        CharSequence mName;
1200        int mNameResId;
1201        int mTypes;
1202        final boolean mGroupable;
1203
1204        RouteCategory(CharSequence name, int types, boolean groupable) {
1205            mName = name;
1206            mTypes = types;
1207            mGroupable = groupable;
1208        }
1209
1210        RouteCategory(int nameResId, int types, boolean groupable) {
1211            mNameResId = nameResId;
1212            mTypes = types;
1213            mGroupable = groupable;
1214        }
1215
1216        /**
1217         * @return the name of this route category
1218         */
1219        public CharSequence getName() {
1220            return getName(sStatic.mResources);
1221        }
1222
1223        /**
1224         * Return the properly localized/configuration dependent name of this RouteCategory.
1225         *
1226         * @param context Context to resolve name resources
1227         * @return the name of this route category
1228         */
1229        public CharSequence getName(Context context) {
1230            return getName(context.getResources());
1231        }
1232
1233        CharSequence getName(Resources res) {
1234            if (mNameResId != 0) {
1235                return res.getText(mNameResId);
1236            }
1237            return mName;
1238        }
1239
1240        /**
1241         * Return the current list of routes in this category that have been added
1242         * to the MediaRouter.
1243         *
1244         * <p>This list will not include routes that are nested within RouteGroups.
1245         * A RouteGroup is treated as a single route within its category.</p>
1246         *
1247         * @param out a List to fill with the routes in this category. If this parameter is
1248         *            non-null, it will be cleared, filled with the current routes with this
1249         *            category, and returned. If this parameter is null, a new List will be
1250         *            allocated to report the category's current routes.
1251         * @return A list with the routes in this category that have been added to the MediaRouter.
1252         */
1253        public List<RouteInfo> getRoutes(List<RouteInfo> out) {
1254            if (out == null) {
1255                out = new ArrayList<RouteInfo>();
1256            } else {
1257                out.clear();
1258            }
1259
1260            final int count = getRouteCountStatic();
1261            for (int i = 0; i < count; i++) {
1262                final RouteInfo route = getRouteAtStatic(i);
1263                if (route.mCategory == this) {
1264                    out.add(route);
1265                }
1266            }
1267            return out;
1268        }
1269
1270        /**
1271         * @return Flag set describing the route types supported by this category
1272         */
1273        public int getSupportedTypes() {
1274            return mTypes;
1275        }
1276
1277        /**
1278         * Return whether or not this category supports grouping.
1279         *
1280         * <p>If this method returns true, all routes obtained from this category
1281         * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
1282         *
1283         * @return true if this category supports
1284         */
1285        public boolean isGroupable() {
1286            return mGroupable;
1287        }
1288
1289        public String toString() {
1290            return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
1291                    " groupable=" + mGroupable + " }";
1292        }
1293    }
1294
1295    static class CallbackInfo {
1296        public int type;
1297        public final Callback cb;
1298        public final MediaRouter router;
1299
1300        public CallbackInfo(Callback cb, int type, MediaRouter router) {
1301            this.cb = cb;
1302            this.type = type;
1303            this.router = router;
1304        }
1305    }
1306
1307    /**
1308     * Interface for receiving events about media routing changes.
1309     * All methods of this interface will be called from the application's main thread.
1310     *
1311     * <p>A Callback will only receive events relevant to routes that the callback
1312     * was registered for.</p>
1313     *
1314     * @see MediaRouter#addCallback(int, Callback)
1315     * @see MediaRouter#removeCallback(Callback)
1316     */
1317    public static abstract class Callback {
1318        /**
1319         * Called when the supplied route becomes selected as the active route
1320         * for the given route type.
1321         *
1322         * @param router the MediaRouter reporting the event
1323         * @param type Type flag set indicating the routes that have been selected
1324         * @param info Route that has been selected for the given route types
1325         */
1326        public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
1327
1328        /**
1329         * Called when the supplied route becomes unselected as the active route
1330         * for the given route type.
1331         *
1332         * @param router the MediaRouter reporting the event
1333         * @param type Type flag set indicating the routes that have been unselected
1334         * @param info Route that has been unselected for the given route types
1335         */
1336        public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
1337
1338        /**
1339         * Called when a route for the specified type was added.
1340         *
1341         * @param router the MediaRouter reporting the event
1342         * @param info Route that has become available for use
1343         */
1344        public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
1345
1346        /**
1347         * Called when a route for the specified type was removed.
1348         *
1349         * @param router the MediaRouter reporting the event
1350         * @param info Route that has been removed from availability
1351         */
1352        public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
1353
1354        /**
1355         * Called when an aspect of the indicated route has changed.
1356         *
1357         * <p>This will not indicate that the types supported by this route have
1358         * changed, only that cosmetic info such as name or status have been updated.</p>
1359         *
1360         * @param router the MediaRouter reporting the event
1361         * @param info The route that was changed
1362         */
1363        public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
1364
1365        /**
1366         * Called when a route is added to a group.
1367         *
1368         * @param router the MediaRouter reporting the event
1369         * @param info The route that was added
1370         * @param group The group the route was added to
1371         * @param index The route index within group that info was added at
1372         */
1373        public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1374                int index);
1375
1376        /**
1377         * Called when a route is removed from a group.
1378         *
1379         * @param router the MediaRouter reporting the event
1380         * @param info The route that was removed
1381         * @param group The group the route was removed from
1382         */
1383        public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
1384    }
1385
1386    /**
1387     * Stub implementation of {@link MediaRouter.Callback}.
1388     * Each abstract method is defined as a no-op. Override just the ones
1389     * you need.
1390     */
1391    public static class SimpleCallback extends Callback {
1392
1393        @Override
1394        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
1395        }
1396
1397        @Override
1398        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
1399        }
1400
1401        @Override
1402        public void onRouteAdded(MediaRouter router, RouteInfo info) {
1403        }
1404
1405        @Override
1406        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
1407        }
1408
1409        @Override
1410        public void onRouteChanged(MediaRouter router, RouteInfo info) {
1411        }
1412
1413        @Override
1414        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
1415                int index) {
1416        }
1417
1418        @Override
1419        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
1420        }
1421
1422    }
1423
1424    static class VolumeCallbackInfo {
1425        public final VolumeCallback vcb;
1426        public final RouteInfo route;
1427
1428        public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
1429            this.vcb = vcb;
1430            this.route = route;
1431        }
1432    }
1433
1434    /**
1435     * Interface for receiving events about volume changes.
1436     * All methods of this interface will be called from the application's main thread.
1437     *
1438     * <p>A VolumeCallback will only receive events relevant to routes that the callback
1439     * was registered for.</p>
1440     *
1441     * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
1442     */
1443    public static abstract class VolumeCallback {
1444        /**
1445         * Called when the volume for the route should be increased or decreased.
1446         * @param info the route affected by this event
1447         * @param direction an integer indicating whether the volume is to be increased
1448         *     (positive value) or decreased (negative value).
1449         *     For bundled changes, the absolute value indicates the number of changes
1450         *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
1451         */
1452        public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
1453        /**
1454         * Called when the volume for the route should be set to the given value
1455         * @param info the route affected by this event
1456         * @param volume an integer indicating the new volume value that should be used, always
1457         *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
1458         */
1459        public abstract void onVolumeSetRequest(RouteInfo info, int volume);
1460    }
1461
1462}
1463