MediaRouter.java revision b507e525a61ed761eecfc2eaaf19af7e8db5dca5
1/*
2 * Copyright (C) 2013 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.support.v7.media;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.res.Resources;
25import android.graphics.drawable.Drawable;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.Message;
30import android.support.v4.hardware.display.DisplayManagerCompat;
31import android.support.v7.media.MediaRouteProvider.RouteDescriptor;
32import android.support.v7.media.MediaRouteProvider.ProviderDescriptor;
33import android.support.v7.media.MediaRouteProvider.ProviderMetadata;
34import android.util.Log;
35import android.view.Display;
36
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.List;
40import java.util.WeakHashMap;
41import java.util.concurrent.CopyOnWriteArrayList;
42
43/**
44 * MediaRouter allows applications to control the routing of media channels
45 * and streams from the current device to external speakers and destination devices.
46 * <p>
47 * A MediaRouter instance is retrieved through {@link #getInstance}.  Applications
48 * can query the media router about the currently selected route and its capabilities
49 * to determine how to send content to the route's destination.  Applications can
50 * also {@link RouteInfo#sendControlRequest send control requests} to the route
51 * to ask the route's destination to perform certain remote control functions
52 * such as playing media.
53 * </p><p>
54 * See also {@link MediaRouteProvider} for information on how an application
55 * can publish new media routes to the media router.
56 * </p><p>
57 * The media router API is not thread-safe; all interactions with it must be
58 * done from the main thread of the process.
59 * </p>
60 */
61public final class MediaRouter {
62    private static final String TAG = "MediaRouter";
63
64    // Maintains global media router state for the process.
65    // This field is initialized in MediaRouter.getInstance() before any
66    // MediaRouter objects are instantiated so it is guaranteed to be
67    // valid whenever any instance method is invoked.
68    static GlobalMediaRouter sGlobal;
69
70    // Context-bound state of the media router.
71    final Context mContext;
72    final CopyOnWriteArrayList<Callback> mCallbacks = new CopyOnWriteArrayList<Callback>();
73
74    MediaRouter(Context context) {
75        mContext = context;
76    }
77
78    /**
79     * Gets an instance of the media router service from the context.
80     */
81    public static MediaRouter getInstance(Context context) {
82        if (context == null) {
83            throw new IllegalArgumentException("context must not be null");
84        }
85        checkCallingThread();
86
87        if (sGlobal == null) {
88            sGlobal = new GlobalMediaRouter(context.getApplicationContext());
89            sGlobal.start();
90        }
91        return sGlobal.getRouter(context);
92    }
93
94    /**
95     * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to
96     * this media router.
97     */
98    public List<RouteInfo> getRoutes() {
99        checkCallingThread();
100        return sGlobal.getRoutes();
101    }
102
103    /**
104     * Gets information about the {@link MediaRouter.ProviderInfo route providers}
105     * currently known to this media router.
106     */
107    public List<ProviderInfo> getProviders() {
108        checkCallingThread();
109        return sGlobal.getProviders();
110    }
111
112    /**
113     * Gets the default route for playing media content on the system.
114     * <p>
115     * The system always provides a default route.
116     * </p>
117     *
118     * @return The default route, which is guaranteed to never be null.
119     */
120    public RouteInfo getDefaultRoute() {
121        checkCallingThread();
122        return sGlobal.getDefaultRoute();
123    }
124
125    /**
126     * Gets the currently selected route.
127     * <p>
128     * The application should examine the route's
129     * {@link RouteInfo#getControlFilters media control intent filters} to assess the
130     * capabilities of the route before attempting to use it.
131     * </p>
132     *
133     * <h3>Example</h3>
134     * <pre>
135     * public boolean playMovie() {
136     *     MediaRouter mediaRouter = MediaRouter.getInstance(context);
137     *     MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
138     *
139     *     // First try using the remote playback interface, if supported.
140     *     if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
141     *         // The route supports remote playback.
142     *         // Try to send it the Uri of the movie to play.
143     *         Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
144     *         intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
145     *         intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
146     *         if (route.supportsControlRequest(intent)) {
147     *             route.sendControlRequest(intent, null);
148     *             return true; // sent the request to play the movie
149     *         }
150     *     }
151     *
152     *     // If remote playback was not possible, then play locally.
153     *     if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
154     *         // The route supports live video streaming.
155     *         // Prepare to play content locally in a window or in a presentation.
156     *         return playMovieInWindow();
157     *     }
158     *
159     *     // Neither interface is supported, so we can't play the movie to this route.
160     *     return false;
161     * }
162     * </pre>
163     *
164     * @return The selected route, which is guaranteed to never be null.
165     *
166     * @see RouteInfo#getControlFilters
167     * @see RouteInfo#supportsControlCategory
168     * @see RouteInfo#supportsControlRequest
169     */
170    public RouteInfo getSelectedRoute() {
171        checkCallingThread();
172        return sGlobal.getSelectedRoute();
173    }
174
175    /**
176     * Selects the specified route.
177     *
178     * @param route The route to select.
179     */
180    public void selectRoute(RouteInfo route) {
181        if (route == null) {
182            throw new IllegalArgumentException("route must not be null");
183        }
184        checkCallingThread();
185
186        sGlobal.selectRoute(route);
187    }
188
189    /**
190     * Adds a callback to listen to changes to media routes.
191     *
192     * @param callback The callback to add.
193     */
194    public void addCallback(Callback callback) {
195        if (callback == null) {
196            throw new IllegalArgumentException("callback must not be null");
197        }
198        checkCallingThread();
199
200        if (!mCallbacks.contains(callback)) {
201            mCallbacks.add(callback);
202        }
203    }
204
205    /**
206     * Removes the specified callback.  It will no longer receive information about
207     * changes to media routes.
208     *
209     * @param callback The callback to remove.
210     */
211    public void removeCallback(Callback callback) {
212        if (callback == null) {
213            throw new IllegalArgumentException("callback must not be null");
214        }
215        checkCallingThread();
216
217        mCallbacks.remove(callback);
218    }
219
220    /**
221     * Registers a media route provider globally for this application process.
222     *
223     * @param providerInstance The media route provider instance to add.
224     *
225     * @see MediaRouteProvider
226     */
227    public void addProvider(MediaRouteProvider providerInstance) {
228        if (providerInstance == null) {
229            throw new IllegalArgumentException("providerInstance must not be null");
230        }
231        checkCallingThread();
232
233        sGlobal.addProvider(providerInstance);
234    }
235
236    /**
237     * Unregisters a media route provider globally for this application process.
238     *
239     * @param providerInstance The media route provider instance to remove.
240     *
241     * @see MediaRouteProvider
242     */
243    public void removeProvider(MediaRouteProvider providerInstance) {
244        if (providerInstance == null) {
245            throw new IllegalArgumentException("providerInstance must not be null");
246        }
247        checkCallingThread();
248
249        sGlobal.removeProvider(providerInstance);
250    }
251
252    /**
253     * Ensures that calls into the media router are on the correct thread.
254     * It pays to be a little paranoid when global state invariants are at risk.
255     */
256    static void checkCallingThread() {
257        if (Looper.myLooper() != Looper.getMainLooper()) {
258            throw new IllegalStateException("The media router service must only be "
259                    + "accessed on the application's main thread.");
260        }
261    }
262
263    static <T> boolean equal(T a, T b) {
264        return a == b || (a != null && b != null && a.equals(b));
265    }
266
267    /**
268     * Provides information about a media route.
269     * <p>
270     * Each media route has a list of {@link MediaControlIntent media control}
271     * {@link #getControlFilters intent filters} that describe the capabilities of the
272     * route and the manner in which it is used and controlled.
273     * </p>
274     */
275    public static final class RouteInfo {
276        private final ProviderInfo mProvider;
277        private final String mDescriptorId;
278        private String mName;
279        private String mStatus;
280        private Drawable mIconDrawable;
281        private int mIconResource;
282        private boolean mEnabled;
283        private final ArrayList<IntentFilter> mControlFilters = new ArrayList<IntentFilter>();
284        private int mPlaybackType;
285        private int mPlaybackStream;
286        private int mVolumeHandling;
287        private int mVolume;
288        private int mVolumeMax;
289        private Display mPresentationDisplay;
290        private int mPresentationDisplayId = -1;
291        private Bundle mExtras;
292        private RouteDescriptor mDescriptor;
293
294        /**
295         * The default playback type, "local", indicating the presentation of the media
296         * is happening on the same device (e.g. a phone, a tablet) as where it is
297         * controlled from.
298         *
299         * @see #getPlaybackType
300         */
301        public static final int PLAYBACK_TYPE_LOCAL = 0;
302
303        /**
304         * A playback type indicating the presentation of the media is happening on
305         * a different device (i.e. the remote device) than where it is controlled from.
306         *
307         * @see #getPlaybackType
308         */
309        public static final int PLAYBACK_TYPE_REMOTE = 1;
310
311        /**
312         * Playback information indicating the playback volume is fixed, i.e. it cannot be
313         * controlled from this object. An example of fixed playback volume is a remote player,
314         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
315         * than attenuate at the source.
316         *
317         * @see #getVolumeHandling
318         */
319        public static final int PLAYBACK_VOLUME_FIXED = 0;
320
321        /**
322         * Playback information indicating the playback volume is variable and can be controlled
323         * from this object.
324         *
325         * @see #getVolumeHandling
326         */
327        public static final int PLAYBACK_VOLUME_VARIABLE = 1;
328
329        static final int CHANGE_GENERAL = 1 << 0;
330        static final int CHANGE_VOLUME = 1 << 1;
331        static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2;
332
333        RouteInfo(ProviderInfo provider, String descriptorId) {
334            mProvider = provider;
335            mDescriptorId = descriptorId;
336        }
337
338        /**
339         * Gets information about the provider of this media route.
340         */
341        public ProviderInfo getProvider() {
342            return mProvider;
343        }
344
345        /**
346         * Gets the name of this route.
347         *
348         * @return The user-friendly name of a media route. This is the string presented
349         * to users who may select this as the active route.
350         */
351        public String getName() {
352            return mName;
353        }
354
355        /**
356         * Gets the status of this route.
357         *
358         * @return The user-friendly status for a media route. This may include a description
359         * of the currently playing media, if available.
360         */
361        public String getStatus() {
362            return mStatus;
363        }
364
365        /**
366         * Get the icon representing this route.
367         * This icon will be used in picker UIs if available.
368         *
369         * @return The icon representing this route or null if no icon is available.
370         */
371        public Drawable getIconDrawable() {
372            checkCallingThread();
373            if (mIconDrawable == null) {
374                if (mIconResource != 0) {
375                    Resources resources = mProvider.getResources();
376                    if (resources != null) {
377                        try {
378                            mIconDrawable = resources.getDrawable(mIconResource);
379                        } catch (Resources.NotFoundException ex) {
380                            Log.w(TAG, "Unable to load media route icon drawable resource "
381                                    + "from provider.", ex);
382                        }
383                    }
384                }
385            }
386            return mIconDrawable;
387        }
388
389        /**
390         * Returns true if this route is enabled and may be selected.
391         *
392         * @return true if this route is enabled and may be selected.
393         */
394        public boolean isEnabled() {
395            return mEnabled;
396        }
397
398        /**
399         * Returns true if this route is currently selected.
400         *
401         * @return true if this route is currently selected.
402         *
403         * @see MediaRouter#getSelectedRoute
404         */
405        public boolean isSelected() {
406            checkCallingThread();
407            return sGlobal.getSelectedRoute() == this;
408        }
409
410        /**
411         * Returns true if this route is the default route.
412         *
413         * @return true if this route is the default route.
414         *
415         * @see MediaRouter#getDefaultRoute
416         */
417        public boolean isDefault() {
418            checkCallingThread();
419            return sGlobal.getDefaultRoute() == this;
420        }
421
422        /**
423         * Gets a list of {@link MediaControlIntent media control intent} filters that
424         * describe the capabilities of this route and the media control actions that
425         * it supports.
426         *
427         * @return A list of intent filters that specifies the media control intents that
428         * this route supports.
429         *
430         * @see MediaControlIntent
431         * @see #supportsControlCategory
432         * @see #supportsControlRequest
433         */
434        public List<IntentFilter> getControlFilters() {
435            return mControlFilters;
436        }
437
438        /**
439         * Returns true if the route supports the specified
440         * {@link MediaControlIntent media control} category.
441         * <p>
442         * Media control categories describe the capabilities of this route
443         * such as whether it supports live audio streaming or remote playback.
444         * </p>
445         *
446         * @param category A {@link MediaControlIntent media control} category
447         * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
448         * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
449         * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
450         * media control category.
451         *
452         * @see MediaControlIntent
453         * @see #getControlFilters
454         */
455        public boolean supportsControlCategory(String category) {
456            if (category == null) {
457                throw new IllegalArgumentException("category must not be null");
458            }
459            checkCallingThread();
460
461            int count = mControlFilters.size();
462            for (int i = 0; i < count; i++) {
463                if (mControlFilters.get(i).hasCategory(category)) {
464                    return true;
465                }
466            }
467            return false;
468        }
469
470        /**
471         * Returns true if the route supports the specified
472         * {@link MediaControlIntent media control} request.
473         * <p>
474         * Media control requests are used to request the route to perform
475         * actions such as starting remote playback of a content stream.
476         * </p>
477         *
478         * @param intent A {@link MediaControlIntent media control intent}.
479         * @return True if the route can handle the specified intent.
480         *
481         * @see MediaControlIntent
482         * @see #getControlFilters
483         */
484        public boolean supportsControlRequest(Intent intent) {
485            if (intent == null) {
486                throw new IllegalArgumentException("intent must not be null");
487            }
488            checkCallingThread();
489
490            ContentResolver contentResolver = sGlobal.getContentResolver();
491            int count = mControlFilters.size();
492            for (int i = 0; i < count; i++) {
493                if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) {
494                    return true;
495                }
496            }
497            return false;
498        }
499
500        /**
501         * Sends a {@link MediaControlIntent media control} request to be performed
502         * asynchronously by the route's destination.
503         * <p>
504         * Media control requests are used to request the route to perform
505         * actions such as starting remote playback of a content stream.
506         * </p><p>
507         * This function may only be called on a selected route.  Control requests
508         * sent to unselected routes will fail.
509         * </p>
510         *
511         * @param intent A {@link MediaControlIntent media control intent}.
512         * @param callback A {@link ControlRequestCallback} to invoke with the result
513         * of the request, or null if no result is required.
514         *
515         * @see MediaControlIntent
516         */
517        public void sendControlRequest(Intent intent, ControlRequestCallback callback) {
518            if (intent == null) {
519                throw new IllegalArgumentException("intent must not be null");
520            }
521            checkCallingThread();
522
523            sGlobal.sendControlRequest(this, intent, callback);
524        }
525
526        /**
527         * Gets the type of playback associated with this route.
528         *
529         * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL}
530         * or {@link #PLAYBACK_TYPE_REMOTE}.
531         */
532        public int getPlaybackType() {
533            return mPlaybackType;
534        }
535
536        /**
537         * Gets the the stream over which the playback associated with this route is performed.
538         *
539         * @return The stream over which the playback associated with this route is performed.
540         */
541        public int getPlaybackStream() {
542            return mPlaybackStream;
543        }
544
545        /**
546         * Gets information about how volume is handled on the route.
547         *
548         * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED}
549         * or {@link #PLAYBACK_VOLUME_VARIABLE}.
550         */
551        public int getVolumeHandling() {
552            return mVolumeHandling;
553        }
554
555        /**
556         * Gets the current volume for this route. Depending on the route, this may only
557         * be valid if the route is currently selected.
558         *
559         * @return The volume at which the playback associated with this route is performed.
560         */
561        public int getVolume() {
562            return mVolume;
563        }
564
565        /**
566         * Gets the maximum volume at which the playback associated with this route is performed.
567         *
568         * @return The maximum volume at which the playback associated with
569         * this route is performed.
570         */
571        public int getVolumeMax() {
572            return mVolumeMax;
573        }
574
575        /**
576         * Requests a volume change for this route asynchronously.
577         * <p>
578         * This function may only be called on a selected route.  It will have
579         * no effect if the route is currently unselected.
580         * </p>
581         *
582         * @param volume The new volume value between 0 and {@link #getVolumeMax}.
583         */
584        public void requestSetVolume(int volume) {
585            checkCallingThread();
586            sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume)));
587        }
588
589        /**
590         * Requests an incremental volume update for this route asynchronously.
591         * <p>
592         * This function may only be called on a selected route.  It will have
593         * no effect if the route is currently unselected.
594         * </p>
595         *
596         * @param delta The delta to add to the current volume.
597         */
598        public void requestUpdateVolume(int delta) {
599            checkCallingThread();
600            if (delta != 0) {
601                sGlobal.requestUpdateVolume(this, delta);
602            }
603        }
604
605        /**
606         * Gets the {@link Display} that should be used by the application to show
607         * a {@link android.app.Presentation} on an external display when this route is selected.
608         * Depending on the route, this may only be valid if the route is currently
609         * selected.
610         * <p>
611         * The preferred presentation display may change independently of the route
612         * being selected or unselected.  For example, the presentation display
613         * of the default system route may change when an external HDMI display is connected
614         * or disconnected even though the route itself has not changed.
615         * </p><p>
616         * This method may return null if there is no external display associated with
617         * the route or if the display is not ready to show UI yet.
618         * </p><p>
619         * The application should listen for changes to the presentation display
620         * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
621         * show or dismiss its {@link android.app.Presentation} accordingly when the display
622         * becomes available or is removed.
623         * </p><p>
624         * This method only makes sense for
625         * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes.
626         * </p>
627         *
628         * @return The preferred presentation display to use when this route is
629         * selected or null if none.
630         *
631         * @see MediaControlIntent#CATEGORY_LIVE_VIDEO
632         * @see android.app.Presentation
633         */
634        public Display getPresentationDisplay() {
635            checkCallingThread();
636            if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
637                mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId);
638            }
639            return mPresentationDisplay;
640        }
641
642        /**
643         * Gets a collection of extra properties about this route that were supplied
644         * by its media route provider, or null if none.
645         */
646        public Bundle getExtras() {
647            return mExtras;
648        }
649
650        /**
651         * Selects this media route.
652         */
653        public void select() {
654            checkCallingThread();
655            sGlobal.selectRoute(this);
656        }
657
658        @Override
659        public String toString() {
660            return "MediaRouter.RouteInfo{ name=" + mName
661                    + ", status=" + mStatus
662                    + ", enabled=" + mEnabled
663                    + ", playbackType=" + mPlaybackType
664                    + ", playbackStream=" + mPlaybackStream
665                    + ", volumeHandling=" + mVolumeHandling
666                    + ", volume=" + mVolume
667                    + ", volumeMax=" + mVolumeMax
668                    + ", presentationDisplayId=" + mPresentationDisplayId
669                    + ", extras=" + mExtras
670                    + ", providerPackageName=" + mProvider.getPackageName()
671                    + " }";
672        }
673
674        int updateDescriptor(RouteDescriptor descriptor) {
675            int changes = 0;
676            if (mDescriptor != descriptor) {
677                mDescriptor = descriptor;
678                if (descriptor != null) {
679                    if (!equal(mName, descriptor.getName())) {
680                        mName = descriptor.getName();
681                        changes |= CHANGE_GENERAL;
682                    }
683                    if (!equal(mStatus, descriptor.getStatus())) {
684                        mStatus = descriptor.getStatus();
685                        changes |= CHANGE_GENERAL;
686                    }
687                    if (mIconResource != descriptor.getIconResource()) {
688                        mIconResource = descriptor.getIconResource();
689                        mIconDrawable = null;
690                        changes |= CHANGE_GENERAL;
691                    }
692                    if (mIconResource == 0
693                            && mIconDrawable != descriptor.getIconDrawable()) {
694                        mIconDrawable = descriptor.getIconDrawable();
695                        changes |= CHANGE_GENERAL;
696                    }
697                    if (mEnabled != descriptor.isEnabled()) {
698                        mEnabled = descriptor.isEnabled();
699                        changes |= CHANGE_GENERAL;
700                    }
701                    IntentFilter[] descriptorControlFilters = descriptor.getControlFilters();
702                    if (!hasSameControlFilters(descriptorControlFilters)) {
703                        mControlFilters.clear();
704                        for (IntentFilter f : descriptorControlFilters) {
705                            mControlFilters.add(f);
706                        }
707                        changes |= CHANGE_GENERAL;
708                    }
709                    if (mPlaybackType != descriptor.getPlaybackType()) {
710                        mPlaybackType = descriptor.getPlaybackType();
711                        changes |= CHANGE_GENERAL;
712                    }
713                    if (mPlaybackStream != descriptor.getPlaybackStream()) {
714                        mPlaybackStream = descriptor.getPlaybackStream();
715                        changes |= CHANGE_GENERAL;
716                    }
717                    if (mVolumeHandling != descriptor.getVolumeHandling()) {
718                        mVolumeHandling = descriptor.getVolumeHandling();
719                        changes |= CHANGE_GENERAL | CHANGE_VOLUME;
720                    }
721                    if (mVolume != descriptor.getVolume()) {
722                        mVolume = descriptor.getVolume();
723                        changes |= CHANGE_GENERAL | CHANGE_VOLUME;
724                    }
725                    if (mVolumeMax != descriptor.getVolumeMax()) {
726                        mVolumeMax = descriptor.getVolumeMax();
727                        changes |= CHANGE_GENERAL | CHANGE_VOLUME;
728                    }
729                    if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) {
730                        mPresentationDisplayId = descriptor.getPresentationDisplayId();
731                        mPresentationDisplay = null;
732                        changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
733                    }
734                    if (!equal(mExtras, descriptor.getExtras())) {
735                        mExtras = descriptor.getExtras();
736                        changes |= CHANGE_GENERAL;
737                    }
738                }
739            }
740            return changes;
741        }
742
743        boolean hasSameControlFilters(IntentFilter[] controlFilters) {
744            final int count = mControlFilters.size();
745            if (count != controlFilters.length) {
746                return false;
747            }
748            for (int i = 0; i < count; i++) {
749                if (!mControlFilters.get(i).equals(controlFilters[i])) {
750                    return false;
751                }
752            }
753            return true;
754        }
755
756        String getDescriptorId() {
757            return mDescriptorId;
758        }
759
760        MediaRouteProvider getProviderInstance() {
761            return mProvider.getProviderInstance();
762        }
763    }
764
765    /**
766     * Provides information about a media route provider.
767     * <p>
768     * This object may be used to determine which media route provider has
769     * published a particular route.
770     * </p>
771     */
772    public static final class ProviderInfo {
773        private final MediaRouteProvider mProviderInstance;
774        private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
775
776        private final ProviderMetadata mMetadata;
777        private ProviderDescriptor mDescriptor;
778        private Resources mResources;
779        private boolean mResourcesNotAvailable;
780
781        ProviderInfo(MediaRouteProvider provider) {
782            mProviderInstance = provider;
783            mMetadata = provider.getMetadata();
784        }
785
786        /**
787         * Gets the provider's underlying {@link MediaRouteProvider} instance.
788         */
789        public MediaRouteProvider getProviderInstance() {
790            checkCallingThread();
791            return mProviderInstance;
792        }
793
794        /**
795         * Gets the package name of the media route provider service.
796         */
797        public String getPackageName() {
798            return mMetadata.getPackageName();
799        }
800
801        /**
802         * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider.
803         */
804        public List<RouteInfo> getRoutes() {
805            checkCallingThread();
806            return mRoutes;
807        }
808
809        Resources getResources() {
810            if (mResources == null && !mResourcesNotAvailable) {
811                String packageName = getPackageName();
812                Context context = sGlobal.getProviderContext(packageName);
813                if (context != null) {
814                    mResources = context.getResources();
815                } else {
816                    Log.w(TAG, "Unable to obtain resources for route provider package: "
817                            + packageName);
818                    mResourcesNotAvailable = true;
819                }
820            }
821            return mResources;
822        }
823
824        boolean updateDescriptor(ProviderDescriptor descriptor) {
825            if (mDescriptor != descriptor) {
826                mDescriptor = descriptor;
827                return true;
828            }
829            return false;
830        }
831
832        int findRouteByDescriptorId(String id) {
833            final int count = mRoutes.size();
834            for (int i = 0; i < count; i++) {
835                if (mRoutes.get(i).mDescriptorId.equals(id)) {
836                    return i;
837                }
838            }
839            return -1;
840        }
841
842        @Override
843        public String toString() {
844            return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
845                    + " }";
846        }
847    }
848
849    /**
850     * Interface for receiving events about media routing changes.
851     * All methods of this interface will be called from the application's main thread.
852     * <p>
853     * A Callback will only receive events relevant to routes that the callback
854     * was registered for.
855     * </p>
856     *
857     * @see MediaRouter#addCallback(Callback)
858     * @see MediaRouter#removeCallback(Callback)
859     */
860    public static abstract class Callback {
861        /**
862         * Called when the supplied media route becomes selected as the active route.
863         *
864         * @param router The media router reporting the event.
865         * @param route The route that has been selected.
866         */
867        public void onRouteSelected(MediaRouter router, RouteInfo route) {
868        }
869
870        /**
871         * Called when the supplied media route becomes unselected as the active route.
872         *
873         * @param router The media router reporting the event.
874         * @param route The route that has been unselected.
875         */
876        public void onRouteUnselected(MediaRouter router, RouteInfo route) {
877        }
878
879        /**
880         * Called when a media route has been added.
881         *
882         * @param router The media router reporting the event.
883         * @param route The route that has become available for use.
884         */
885        public void onRouteAdded(MediaRouter router, RouteInfo route) {
886        }
887
888        /**
889         * Called when a media route has been removed.
890         *
891         * @param router The media router reporting the event.
892         * @param route The route that has been removed from availability.
893         */
894        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
895        }
896
897        /**
898         * Called when a property of the indicated media route has changed.
899         *
900         * @param router The media router reporting the event.
901         * @param route The route that was changed.
902         */
903        public void onRouteChanged(MediaRouter router, RouteInfo route) {
904        }
905
906        /**
907         * Called when a media route's volume changes.
908         *
909         * @param router The media router reporting the event.
910         * @param route The route whose volume changed.
911         */
912        public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
913        }
914
915        /**
916         * Called when a media route's presentation display changes.
917         * <p>
918         * This method is called whenever the route's presentation display becomes
919         * available, is removed or has changes to some of its properties (such as its size).
920         * </p>
921         *
922         * @param router The media router reporting the event.
923         * @param route The route whose presentation display changed.
924         *
925         * @see RouteInfo#getPresentationDisplay()
926         */
927        public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
928        }
929
930        /**
931         * Called when a media route provider has been added.
932         *
933         * @param router The media router reporting the event.
934         * @param provider The provider that has become available for use.
935         */
936        public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
937        }
938
939        /**
940         * Called when a media route provider has been removed.
941         *
942         * @param router The media router reporting the event.
943         * @param provider The provider that has been removed from availability.
944         */
945        public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
946        }
947    }
948
949    /**
950     * Callback which is invoked with the result of a media control request.
951     *
952     * @see RouteInfo#sendControlRequest
953     */
954    public static abstract class ControlRequestCallback {
955        /**
956         * Result code: The media control action succeeded.
957         */
958        public static final int REQUEST_SUCCEEDED = 0;
959
960        /**
961         * Result code: The media control action failed.
962         */
963        public static final int REQUEST_FAILED = -1;
964
965        /**
966         * Called with the result of the media control request.
967         *
968         * @param result The result code: {@link #REQUEST_SUCCEEDED}, or {@link #REQUEST_FAILED}.
969         * @param data Additional result data.  Contents depend on the media control action.
970         */
971        public void onResult(int result, Bundle data) {
972        }
973    }
974
975    /**
976     * Global state for the media router.
977     * <p>
978     * Media routes and media route providers are global to the process; their
979     * state and the bulk of the media router implementation lives here.
980     * </p>
981     */
982    private static final class GlobalMediaRouter implements SystemMediaRouteProvider.SyncCallback {
983        private final Context mApplicationContext;
984        private final MediaRouter mApplicationRouter;
985        private final WeakHashMap<Context, MediaRouter> mRouters =
986                new WeakHashMap<Context, MediaRouter>();
987        private final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
988        private final ArrayList<ProviderInfo> mProviders =
989                new ArrayList<ProviderInfo>();
990        private final ProviderCallback mProviderCallback = new ProviderCallback();
991        private final CallbackHandler mCallbackHandler = new CallbackHandler();
992        private final DisplayManagerCompat mDisplayManager;
993        private final SystemMediaRouteProvider mSystemProvider;
994
995        private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
996        private RouteInfo mDefaultRoute;
997        private RouteInfo mSelectedRoute;
998        private MediaRouteProvider.RouteController mSelectedRouteController;
999
1000        GlobalMediaRouter(Context applicationContext) {
1001            mApplicationContext = applicationContext;
1002            mDisplayManager = DisplayManagerCompat.getInstance(applicationContext);
1003            mApplicationRouter = getRouter(applicationContext);
1004
1005            // Add the system media route provider for interoperating with
1006            // the framework media router.  This one is special and receives
1007            // synchronization messages from the media router.
1008            mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
1009            addProvider(mSystemProvider);
1010        }
1011
1012        public void start() {
1013            // Start watching for routes published by registered media route
1014            // provider services.
1015            mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher(
1016                    mApplicationContext, mApplicationRouter);
1017            mRegisteredProviderWatcher.start();
1018        }
1019
1020        public MediaRouter getRouter(Context context) {
1021            MediaRouter router = mRouters.get(context);
1022            if (router == null) {
1023                router = new MediaRouter(context);
1024                mRouters.put(context, router);
1025            }
1026            return router;
1027        }
1028
1029        public ContentResolver getContentResolver() {
1030            return mApplicationContext.getContentResolver();
1031        }
1032
1033        public Context getProviderContext(String packageName) {
1034            if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) {
1035                return mApplicationContext;
1036            }
1037            try {
1038                return mApplicationContext.createPackageContext(
1039                        packageName, Context.CONTEXT_RESTRICTED);
1040            } catch (NameNotFoundException ex) {
1041                return null;
1042            }
1043        }
1044
1045        public Display getDisplay(int displayId) {
1046            return mDisplayManager.getDisplay(displayId);
1047        }
1048
1049        public void sendControlRequest(RouteInfo route,
1050                Intent intent, ControlRequestCallback callback) {
1051            if (route == mSelectedRoute && mSelectedRouteController != null) {
1052                if (mSelectedRouteController.sendControlRequest(intent, callback)) {
1053                    return;
1054                }
1055            }
1056            if (callback != null) {
1057                callback.onResult(ControlRequestCallback.REQUEST_FAILED, null);
1058            }
1059        }
1060
1061        public void requestSetVolume(RouteInfo route, int volume) {
1062            if (route == mSelectedRoute && mSelectedRouteController != null) {
1063                mSelectedRouteController.setVolume(volume);
1064            }
1065        }
1066
1067        public void requestUpdateVolume(RouteInfo route, int delta) {
1068            if (route == mSelectedRoute && mSelectedRouteController != null) {
1069                mSelectedRouteController.updateVolume(delta);
1070            }
1071        }
1072
1073        public List<RouteInfo> getRoutes() {
1074            return mRoutes;
1075        }
1076
1077        public List<ProviderInfo> getProviders() {
1078            return mProviders;
1079        }
1080
1081        public RouteInfo getDefaultRoute() {
1082            if (mDefaultRoute == null) {
1083                // This should never happen once the media router has been fully
1084                // initialized but it is good to check for the error in case there
1085                // is a bug in provider initialization.
1086                throw new IllegalStateException("There is no default route.  "
1087                        + "The media router has not yet been fully initialized.");
1088            }
1089            return mDefaultRoute;
1090        }
1091
1092        public RouteInfo getSelectedRoute() {
1093            if (mSelectedRoute == null) {
1094                // This should never happen once the media router has been fully
1095                // initialized but it is good to check for the error in case there
1096                // is a bug in provider initialization.
1097                throw new IllegalStateException("There is no currently selected route.  "
1098                        + "The media router has not yet been fully initialized.");
1099            }
1100            return mSelectedRoute;
1101        }
1102
1103        public void selectRoute(RouteInfo route) {
1104            if (!mRoutes.contains(route)) {
1105                Log.w(TAG, "Ignoring attempt to select removed route: " + route);
1106                return;
1107            }
1108            if (!route.mEnabled) {
1109                Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
1110                return;
1111            }
1112
1113            setSelectedRouteInternal(route);
1114        }
1115
1116        public void addProvider(MediaRouteProvider providerInstance) {
1117            int index = findProviderInfo(providerInstance);
1118            if (index < 0) {
1119                // 1. Add the provider to the list.
1120                ProviderInfo provider = new ProviderInfo(providerInstance);
1121                mProviders.add(provider);
1122                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
1123                // 2. Create the provider's contents.
1124                updateProviderContents(provider, providerInstance.getDescriptor());
1125                // 3. Register the provider callback.
1126                providerInstance.addCallback(mProviderCallback);
1127            }
1128        }
1129
1130        public void removeProvider(MediaRouteProvider providerInstance) {
1131            int index = findProviderInfo(providerInstance);
1132            if (index >= 0) {
1133                // 1. Unregister the provider callback.
1134                providerInstance.removeCallback(mProviderCallback);
1135                // 2. Delete the provider's contents.
1136                ProviderInfo provider = mProviders.get(index);
1137                updateProviderContents(provider, null);
1138                // 3. Remove the provider from the list.
1139                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
1140                mProviders.remove(index);
1141            }
1142        }
1143
1144        private void updateProviderDescriptor(MediaRouteProvider providerInstance,
1145                ProviderDescriptor descriptor) {
1146            int index = findProviderInfo(providerInstance);
1147            if (index >= 0) {
1148                // Update the provider's contents.
1149                ProviderInfo provider = mProviders.get(index);
1150                updateProviderContents(provider, descriptor);
1151            }
1152        }
1153
1154        private int findProviderInfo(MediaRouteProvider providerInstance) {
1155            final int count = mProviders.size();
1156            for (int i = 0; i < count; i++) {
1157                if (mProviders.get(i).mProviderInstance == providerInstance) {
1158                    return i;
1159                }
1160            }
1161            return -1;
1162        }
1163
1164        private void updateProviderContents(ProviderInfo provider,
1165                ProviderDescriptor providerDescriptor) {
1166            if (provider.updateDescriptor(providerDescriptor)) {
1167                // Update all existing routes and reorder them to match
1168                // the order of their descriptors.
1169                int targetIndex = 0;
1170                if (providerDescriptor != null) {
1171                    if (providerDescriptor.isValid()) {
1172                        final RouteDescriptor[] routeDescriptors = providerDescriptor.getRoutes();
1173                        for (int i = 0; i < routeDescriptors.length; i++) {
1174                            final RouteDescriptor routeDescriptor = routeDescriptors[i];
1175                            final String id = routeDescriptor.getId();
1176                            final int sourceIndex = provider.findRouteByDescriptorId(id);
1177                            if (sourceIndex < 0) {
1178                                // 1. Add the route to the list.
1179                                RouteInfo route = new RouteInfo(provider, id);
1180                                provider.mRoutes.add(targetIndex++, route);
1181                                mRoutes.add(route);
1182                                // 2. Create the route's contents.
1183                                route.updateDescriptor(routeDescriptor);
1184                                // 3. Notify clients about addition.
1185                                mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
1186                            } else if (sourceIndex < targetIndex) {
1187                                Log.w(TAG, "Ignoring route descriptor with duplicate id: "
1188                                        + routeDescriptor);
1189                            } else {
1190                                // 1. Reorder the route within the list.
1191                                RouteInfo route = provider.mRoutes.get(sourceIndex);
1192                                Collections.swap(provider.mRoutes,
1193                                        sourceIndex, targetIndex++);
1194                                // 2. Update the route's contents.
1195                                int changes = route.updateDescriptor(routeDescriptor);
1196                                // 3. Unselect route if needed before notifying about changes.
1197                                unselectRouteIfNeeded(route);
1198                                // 4. Notify clients about changes.
1199                                if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
1200                                    mCallbackHandler.post(
1201                                            CallbackHandler.MSG_ROUTE_CHANGED, route);
1202                                }
1203                                if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
1204                                    mCallbackHandler.post(
1205                                            CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
1206                                }
1207                                if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
1208                                    mCallbackHandler.post(CallbackHandler.
1209                                            MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
1210                                }
1211                            }
1212                        }
1213                    } else {
1214                        Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor);
1215                    }
1216                }
1217
1218                // Dispose all remaining routes that do not have matching descriptors.
1219                for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
1220                    // 1. Delete the route's contents.
1221                    RouteInfo route = provider.mRoutes.get(i);
1222                    route.updateDescriptor(null);
1223                    // 2. Remove the route from the list.
1224                    mRoutes.remove(provider);
1225                    provider.mRoutes.remove(i);
1226                    // 3. Unselect route if needed before notifying about removal.
1227                    unselectRouteIfNeeded(route);
1228                    // 4. Notify clients about removal.
1229                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
1230                }
1231
1232                // Choose a new selected route if needed.
1233                selectRouteIfNeeded();
1234            }
1235        }
1236
1237        private void unselectRouteIfNeeded(RouteInfo route) {
1238            if (mDefaultRoute == route && !isRouteSelectable(route)) {
1239                Log.i(TAG, "Choosing a new default route because the current one "
1240                        + "is no longer selectable: " + route);
1241                mDefaultRoute = null;
1242            }
1243            if (mSelectedRoute == route && !isRouteSelectable(route)) {
1244                Log.i(TAG, "Choosing a new selected route because the current one "
1245                        + "is no longer selectable: " + route);
1246                setSelectedRouteInternal(null);
1247            }
1248        }
1249
1250        private void selectRouteIfNeeded() {
1251            if (mDefaultRoute == null && !mRoutes.isEmpty()) {
1252                for (RouteInfo route : mRoutes) {
1253                    if (isSystemDefaultRoute(route) && isRouteSelectable(route)) {
1254                        mDefaultRoute = route;
1255                        break;
1256                    }
1257                }
1258            }
1259            if (mSelectedRoute == null) {
1260                setSelectedRouteInternal(mDefaultRoute);
1261            }
1262        }
1263
1264        private boolean isRouteSelectable(RouteInfo route) {
1265            // This tests whether the route is still valid and enabled.
1266            // The route descriptor field is set to null when the route is removed.
1267            return route.mDescriptor != null && route.mEnabled;
1268        }
1269
1270        private boolean isSystemDefaultRoute(RouteInfo route) {
1271            return route.getProviderInstance() == mSystemProvider
1272                    && route.mDescriptorId.equals(
1273                            SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
1274        }
1275
1276        private void setSelectedRouteInternal(RouteInfo route) {
1277            if (mSelectedRoute != route) {
1278                if (mSelectedRoute != null) {
1279                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute);
1280                    if (mSelectedRouteController != null) {
1281                        mSelectedRouteController.unselect();
1282                        mSelectedRouteController.release();
1283                        mSelectedRouteController = null;
1284                    }
1285                }
1286
1287                mSelectedRoute = route;
1288
1289                if (mSelectedRoute != null) {
1290                    mSelectedRouteController = route.getProviderInstance().onCreateRouteController(
1291                            route.mDescriptorId);
1292                    if (mSelectedRouteController != null) {
1293                        mSelectedRouteController.select();
1294                    }
1295                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
1296                }
1297            }
1298        }
1299
1300        @Override
1301        public RouteInfo getSystemRouteByDescriptorId(String id) {
1302            int providerIndex = findProviderInfo(mSystemProvider);
1303            if (providerIndex >= 0) {
1304                ProviderInfo provider = mProviders.get(providerIndex);
1305                int routeIndex = provider.findRouteByDescriptorId(id);
1306                if (routeIndex >= 0) {
1307                    return provider.mRoutes.get(routeIndex);
1308                }
1309            }
1310            return null;
1311        }
1312
1313        private final class ProviderCallback extends MediaRouteProvider.Callback {
1314            @Override
1315            public void onDescriptorChanged(MediaRouteProvider provider,
1316                    ProviderDescriptor descriptor) {
1317                updateProviderDescriptor(provider, descriptor);
1318            }
1319        }
1320
1321        private final class CallbackHandler extends Handler {
1322            private final ArrayList<MediaRouter> mTempMediaRouters =
1323                    new ArrayList<MediaRouter>();
1324
1325            public static final int MSG_ROUTE_ADDED = 1;
1326            public static final int MSG_ROUTE_REMOVED = 2;
1327            public static final int MSG_ROUTE_CHANGED = 3;
1328            public static final int MSG_ROUTE_VOLUME_CHANGED = 4;
1329            public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = 5;
1330            public static final int MSG_ROUTE_SELECTED = 6;
1331            public static final int MSG_ROUTE_UNSELECTED = 7;
1332            public static final int MSG_PROVIDER_ADDED = 8;
1333            public static final int MSG_PROVIDER_REMOVED = 9;
1334
1335            public void post(int msg, Object obj) {
1336                obtainMessage(msg, obj).sendToTarget();
1337            }
1338
1339            @Override
1340            public void handleMessage(Message msg) {
1341                final int what = msg.what;
1342                final Object obj = msg.obj;
1343
1344                // Synchronize state with the system media router.
1345                syncWithSystemProvider(what, obj);
1346
1347                // Invoke all registered callbacks.
1348                mTempMediaRouters.addAll(mRouters.values());
1349                try {
1350                    final int routerCount = mTempMediaRouters.size();
1351                    for (int i = 0; i < routerCount; i++) {
1352                        final MediaRouter router = mTempMediaRouters.get(i);
1353                        if (!router.mCallbacks.isEmpty()) {
1354                            for (MediaRouter.Callback callback : router.mCallbacks) {
1355                                invokeCallback(router, callback, what, obj);
1356                            }
1357                        }
1358                    }
1359                } finally {
1360                    mTempMediaRouters.clear();
1361                }
1362            }
1363
1364            private void syncWithSystemProvider(int what, Object obj) {
1365                switch (what) {
1366                    case MSG_ROUTE_ADDED:
1367                        mSystemProvider.onSyncRouteAdded((RouteInfo)obj);
1368                        break;
1369                    case MSG_ROUTE_REMOVED:
1370                        mSystemProvider.onSyncRouteRemoved((RouteInfo)obj);
1371                        break;
1372                    case MSG_ROUTE_CHANGED:
1373                        mSystemProvider.onSyncRouteChanged((RouteInfo)obj);
1374                        break;
1375                    case MSG_ROUTE_SELECTED:
1376                        mSystemProvider.onSyncRouteSelected((RouteInfo)obj);
1377                        break;
1378                }
1379            }
1380
1381            private void invokeCallback(MediaRouter router, MediaRouter.Callback callback,
1382                    int what, Object obj) {
1383                switch (what) {
1384                    case MSG_ROUTE_ADDED:
1385                        callback.onRouteAdded(router, (RouteInfo)obj);
1386                        break;
1387                    case MSG_ROUTE_REMOVED:
1388                        callback.onRouteRemoved(router, (RouteInfo)obj);
1389                        break;
1390                    case MSG_ROUTE_CHANGED:
1391                        callback.onRouteChanged(router, (RouteInfo)obj);
1392                        break;
1393                    case MSG_ROUTE_VOLUME_CHANGED:
1394                        callback.onRouteVolumeChanged(router, (RouteInfo)obj);
1395                        break;
1396                    case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
1397                        callback.onRoutePresentationDisplayChanged(router, (RouteInfo)obj);
1398                        break;
1399                    case MSG_ROUTE_SELECTED:
1400                        callback.onRouteSelected(router, (RouteInfo)obj);
1401                        break;
1402                    case MSG_ROUTE_UNSELECTED:
1403                        callback.onRouteUnselected(router, (RouteInfo)obj);
1404                        break;
1405                    case MSG_PROVIDER_ADDED:
1406                        callback.onProviderAdded(router, (ProviderInfo)obj);
1407                        break;
1408                    case MSG_PROVIDER_REMOVED:
1409                        callback.onProviderRemoved(router, (ProviderInfo)obj);
1410                        break;
1411                }
1412            }
1413        }
1414    }
1415}
1416