MediaRouter.java revision 3cb412fab95f51525d1e777e5ae6d12021bb2c93
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.app.ActivityManager;
20import android.content.ComponentName;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.IntentSender;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.res.Resources;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.Message;
33import android.support.annotation.IntDef;
34import android.support.annotation.NonNull;
35import android.support.annotation.Nullable;
36import android.support.v4.app.ActivityManagerCompat;
37import android.support.v4.hardware.display.DisplayManagerCompat;
38import android.support.v4.media.VolumeProviderCompat;
39import android.support.v4.media.session.MediaSessionCompat;
40import android.support.v4.util.Pair;
41import android.support.v7.media.MediaRouteProvider.ProviderMetadata;
42import android.support.v7.media.MediaRouteProvider.RouteController;
43import android.text.TextUtils;
44import android.util.Log;
45import android.view.Display;
46
47import java.lang.annotation.Retention;
48import java.lang.annotation.RetentionPolicy;
49import java.lang.ref.WeakReference;
50import java.util.ArrayList;
51import java.util.Collections;
52import java.util.HashMap;
53import java.util.List;
54import java.util.Locale;
55import java.util.Map;
56
57/**
58 * MediaRouter allows applications to control the routing of media channels
59 * and streams from the current device to external speakers and destination devices.
60 * <p>
61 * A MediaRouter instance is retrieved through {@link #getInstance}.  Applications
62 * can query the media router about the currently selected route and its capabilities
63 * to determine how to send content to the route's destination.  Applications can
64 * also {@link RouteInfo#sendControlRequest send control requests} to the route
65 * to ask the route's destination to perform certain remote control functions
66 * such as playing media.
67 * </p><p>
68 * See also {@link MediaRouteProvider} for information on how an application
69 * can publish new media routes to the media router.
70 * </p><p>
71 * The media router API is not thread-safe; all interactions with it must be
72 * done from the main thread of the process.
73 * </p>
74 */
75public final class MediaRouter {
76    private static final String TAG = "MediaRouter";
77    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
78
79    /**
80     * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
81     * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the reason the route
82     * was unselected is unknown.
83     */
84    public static final int UNSELECT_REASON_UNKNOWN = 0;
85    /**
86     * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
87     * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
88     * the disconnect button to disconnect and keep playing.
89     * <p>
90     *
91     * @see {@link MediaRouteDescriptor#canDisconnectAndKeepPlaying()}.
92     */
93    public static final int UNSELECT_REASON_DISCONNECTED = 1;
94    /**
95     * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
96     * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
97     * the stop casting button.
98     */
99    public static final int UNSELECT_REASON_STOPPED = 2;
100    /**
101     * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
102     * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user selected
103     * a different route.
104     */
105    public static final int UNSELECT_REASON_ROUTE_CHANGED = 3;
106
107    // Maintains global media router state for the process.
108    // This field is initialized in MediaRouter.getInstance() before any
109    // MediaRouter objects are instantiated so it is guaranteed to be
110    // valid whenever any instance method is invoked.
111    static GlobalMediaRouter sGlobal;
112
113    // Context-bound state of the media router.
114    final Context mContext;
115    final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<CallbackRecord>();
116
117    /** @hide */
118    @IntDef(flag = true,
119            value = {
120                    CALLBACK_FLAG_PERFORM_ACTIVE_SCAN,
121                    CALLBACK_FLAG_REQUEST_DISCOVERY,
122                    CALLBACK_FLAG_UNFILTERED_EVENTS
123            }
124    )
125    @Retention(RetentionPolicy.SOURCE)
126    private @interface CallbackFlags {}
127
128    /**
129     * Flag for {@link #addCallback}: Actively scan for routes while this callback
130     * is registered.
131     * <p>
132     * When this flag is specified, the media router will actively scan for new
133     * routes.  Certain routes, such as wifi display routes, may not be discoverable
134     * except when actively scanning.  This flag is typically used when the route picker
135     * dialog has been opened by the user to ensure that the route information is
136     * up to date.
137     * </p><p>
138     * Active scanning may consume a significant amount of power and may have intrusive
139     * effects on wireless connectivity.  Therefore it is important that active scanning
140     * only be requested when it is actually needed to satisfy a user request to
141     * discover and select a new route.
142     * </p><p>
143     * This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing
144     * active scans is much more expensive than a normal discovery request.
145     * </p>
146     *
147     * @see #CALLBACK_FLAG_REQUEST_DISCOVERY
148     */
149    public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
150
151    /**
152     * Flag for {@link #addCallback}: Do not filter route events.
153     * <p>
154     * When this flag is specified, the callback will be invoked for events that affect any
155     * route even if they do not match the callback's filter.
156     * </p>
157     */
158    public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
159
160    /**
161     * Flag for {@link #addCallback}: Request passive route discovery while this
162     * callback is registered, except on {@link ActivityManager#isLowRamDevice low-RAM devices}.
163     * <p>
164     * When this flag is specified, the media router will try to discover routes.
165     * Although route discovery is intended to be efficient, checking for new routes may
166     * result in some network activity and could slowly drain the battery.  Therefore
167     * applications should only specify {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} when
168     * they are running in the foreground and would like to provide the user with the
169     * option of connecting to new routes.
170     * </p><p>
171     * Applications should typically add a callback using this flag in the
172     * {@link android.app.Activity activity's} {@link android.app.Activity#onStart onStart}
173     * method and remove it in the {@link android.app.Activity#onStop onStop} method.
174     * The {@link android.support.v7.app.MediaRouteDiscoveryFragment} fragment may
175     * also be used for this purpose.
176     * </p><p class="note">
177     * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag
178     * will be ignored.  Refer to
179     * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
180     * </p>
181     *
182     * @see android.support.v7.app.MediaRouteDiscoveryFragment
183     */
184    public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
185
186    /**
187     * Flag for {@link #addCallback}: Request passive route discovery while this
188     * callback is registered, even on {@link ActivityManager#isLowRamDevice low-RAM devices}.
189     * <p class="note">
190     * This flag has a significant performance impact on low-RAM devices
191     * since it may cause many media route providers to be started simultaneously.
192     * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
193     * performing passive discovery on these devices altogether.  Refer to
194     * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
195     * </p>
196     *
197     * @see android.support.v7.app.MediaRouteDiscoveryFragment
198     */
199    public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3;
200
201    /**
202     * Flag for {@link #isRouteAvailable}: Ignore the default route.
203     * <p>
204     * This flag is used to determine whether a matching non-default route is available.
205     * This constraint may be used to decide whether to offer the route chooser dialog
206     * to the user.  There is no point offering the chooser if there are no
207     * non-default choices.
208     * </p>
209     */
210    public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
211
212    /**
213     * Flag for {@link #isRouteAvailable}: Require an actual route to be matched.
214     * <p>
215     * If this flag is not set, then {@link #isRouteAvailable} will return true
216     * if it is possible to discover a matching route even if discovery is not in
217     * progress or if no matching route has yet been found.  This feature is used to
218     * save resources by removing the need to perform passive route discovery on
219     * {@link ActivityManager#isLowRamDevice low-RAM devices}.
220     * </p><p>
221     * If this flag is set, then {@link #isRouteAvailable} will only return true if
222     * a matching route has actually been discovered.
223     * </p>
224     */
225    public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1;
226
227    MediaRouter(Context context) {
228        mContext = context;
229    }
230
231    /**
232     * Gets an instance of the media router service associated with the context.
233     * <p>
234     * The application is responsible for holding a strong reference to the returned
235     * {@link MediaRouter} instance, such as by storing the instance in a field of
236     * the {@link android.app.Activity}, to ensure that the media router remains alive
237     * as long as the application is using its features.
238     * </p><p>
239     * In other words, the support library only holds a {@link WeakReference weak reference}
240     * to each media router instance.  When there are no remaining strong references to the
241     * media router instance, all of its callbacks will be removed and route discovery
242     * will no longer be performed on its behalf.
243     * </p>
244     *
245     * @return The media router instance for the context.  The application must hold
246     * a strong reference to this object as long as it is in use.
247     */
248    public static MediaRouter getInstance(@NonNull Context context) {
249        if (context == null) {
250            throw new IllegalArgumentException("context must not be null");
251        }
252        checkCallingThread();
253
254        if (sGlobal == null) {
255            sGlobal = new GlobalMediaRouter(context.getApplicationContext());
256            sGlobal.start();
257        }
258        return sGlobal.getRouter(context);
259    }
260
261    /**
262     * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to
263     * this media router.
264     */
265    public List<RouteInfo> getRoutes() {
266        checkCallingThread();
267        return sGlobal.getRoutes();
268    }
269
270    /**
271     * Gets information about the {@link MediaRouter.ProviderInfo route providers}
272     * currently known to this media router.
273     */
274    public List<ProviderInfo> getProviders() {
275        checkCallingThread();
276        return sGlobal.getProviders();
277    }
278
279    /**
280     * Gets the default route for playing media content on the system.
281     * <p>
282     * The system always provides a default route.
283     * </p>
284     *
285     * @return The default route, which is guaranteed to never be null.
286     */
287    @NonNull
288    public RouteInfo getDefaultRoute() {
289        checkCallingThread();
290        return sGlobal.getDefaultRoute();
291    }
292
293    /**
294     * Gets the currently selected route.
295     * <p>
296     * The application should examine the route's
297     * {@link RouteInfo#getControlFilters media control intent filters} to assess the
298     * capabilities of the route before attempting to use it.
299     * </p>
300     *
301     * <h3>Example</h3>
302     * <pre>
303     * public boolean playMovie() {
304     *     MediaRouter mediaRouter = MediaRouter.getInstance(context);
305     *     MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
306     *
307     *     // First try using the remote playback interface, if supported.
308     *     if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
309     *         // The route supports remote playback.
310     *         // Try to send it the Uri of the movie to play.
311     *         Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
312     *         intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
313     *         intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
314     *         if (route.supportsControlRequest(intent)) {
315     *             route.sendControlRequest(intent, null);
316     *             return true; // sent the request to play the movie
317     *         }
318     *     }
319     *
320     *     // If remote playback was not possible, then play locally.
321     *     if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
322     *         // The route supports live video streaming.
323     *         // Prepare to play content locally in a window or in a presentation.
324     *         return playMovieInWindow();
325     *     }
326     *
327     *     // Neither interface is supported, so we can't play the movie to this route.
328     *     return false;
329     * }
330     * </pre>
331     *
332     * @return The selected route, which is guaranteed to never be null.
333     *
334     * @see RouteInfo#getControlFilters
335     * @see RouteInfo#supportsControlCategory
336     * @see RouteInfo#supportsControlRequest
337     */
338    @NonNull
339    public RouteInfo getSelectedRoute() {
340        checkCallingThread();
341        return sGlobal.getSelectedRoute();
342    }
343
344    /**
345     * Returns the selected route if it matches the specified selector, otherwise
346     * selects the default route and returns it.
347     *
348     * @param selector The selector to match.
349     * @return The previously selected route if it matched the selector, otherwise the
350     * newly selected default route which is guaranteed to never be null.
351     *
352     * @see MediaRouteSelector
353     * @see RouteInfo#matchesSelector
354     * @see RouteInfo#isDefault
355     */
356    @NonNull
357    public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) {
358        if (selector == null) {
359            throw new IllegalArgumentException("selector must not be null");
360        }
361        checkCallingThread();
362
363        if (DEBUG) {
364            Log.d(TAG, "updateSelectedRoute: " + selector);
365        }
366        RouteInfo route = sGlobal.getSelectedRoute();
367        if (!route.isDefault() && !route.matchesSelector(selector)) {
368            route = sGlobal.getDefaultRoute();
369            sGlobal.selectRoute(route);
370        }
371        return route;
372    }
373
374    /**
375     * Selects the specified route.
376     *
377     * @param route The route to select.
378     */
379    public void selectRoute(@NonNull RouteInfo route) {
380        if (route == null) {
381            throw new IllegalArgumentException("route must not be null");
382        }
383        checkCallingThread();
384
385        if (DEBUG) {
386            Log.d(TAG, "selectRoute: " + route);
387        }
388        sGlobal.selectRoute(route);
389    }
390
391    /**
392     * Unselects the current round and selects the default route instead.
393     * <p>
394     * The reason given must be one of:
395     * <ul>
396     * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
397     * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
398     * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
399     * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
400     * </ul>
401     *
402     * @param reason The reason for disconnecting the current route.
403     */
404    public void unselect(int reason) {
405        if (reason < MediaRouter.UNSELECT_REASON_UNKNOWN ||
406                reason > MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
407            throw new IllegalArgumentException("Unsupported reason to unselect route");
408        }
409        checkCallingThread();
410
411        sGlobal.selectRoute(getDefaultRoute(), reason);
412    }
413
414    /**
415     * Returns true if there is a route that matches the specified selector.
416     * <p>
417     * This method returns true if there are any available routes that match the
418     * selector regardless of whether they are enabled or disabled. If the
419     * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
420     * the method will only consider non-default routes.
421     * </p>
422     * <p class="note">
423     * On {@link ActivityManager#isLowRamDevice low-RAM devices} this method
424     * will return true if it is possible to discover a matching route even if
425     * discovery is not in progress or if no matching route has yet been found.
426     * Use {@link #AVAILABILITY_FLAG_REQUIRE_MATCH} to require an actual match.
427     * </p>
428     *
429     * @param selector The selector to match.
430     * @param flags Flags to control the determination of whether a route may be
431     *            available. May be zero or some combination of
432     *            {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and
433     *            {@link #AVAILABILITY_FLAG_REQUIRE_MATCH}.
434     * @return True if a matching route may be available.
435     */
436    public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) {
437        if (selector == null) {
438            throw new IllegalArgumentException("selector must not be null");
439        }
440        checkCallingThread();
441
442        return sGlobal.isRouteAvailable(selector, flags);
443    }
444
445    /**
446     * Registers a callback to discover routes that match the selector and to receive
447     * events when they change.
448     * <p>
449     * This is a convenience method that has the same effect as calling
450     * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags.
451     * </p>
452     *
453     * @param selector A route selector that indicates the kinds of routes that the
454     * callback would like to discover.
455     * @param callback The callback to add.
456     * @see #removeCallback
457     */
458    public void addCallback(MediaRouteSelector selector, Callback callback) {
459        addCallback(selector, callback, 0);
460    }
461
462    /**
463     * Registers a callback to discover routes that match the selector and to receive
464     * events when they change.
465     * <p>
466     * The selector describes the kinds of routes that the application wants to
467     * discover.  For example, if the application wants to use
468     * live audio routes then it should include the
469     * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category}
470     * in its selector when it adds a callback to the media router.
471     * The selector may include any number of categories.
472     * </p><p>
473     * If the callback has already been registered, then the selector is added to
474     * the set of selectors being monitored by the callback.
475     * </p><p>
476     * By default, the callback will only be invoked for events that affect routes
477     * that match the specified selector.  Event filtering may be disabled by specifying
478     * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered.
479     * </p><p>
480     * Applications should use the {@link #isRouteAvailable} method to determine
481     * whether is it possible to discover a route with the desired capabilities
482     * and therefore whether the media route button should be shown to the user.
483     * </p><p>
484     * The {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} flag should be used while the application
485     * is in the foreground to request that passive discovery be performed if there are
486     * sufficient resources to allow continuous passive discovery.
487     * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag will be
488     * ignored to conserve resources.
489     * </p><p>
490     * The {@link #CALLBACK_FLAG_FORCE_DISCOVERY} flag should be used when
491     * passive discovery absolutely must be performed, even on low-RAM devices.
492     * This flag has a significant performance impact on low-RAM devices
493     * since it may cause many media route providers to be started simultaneously.
494     * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
495     * performing passive discovery on these devices altogether.
496     * </p><p>
497     * The {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag should be used when the
498     * media route chooser dialog is showing to confirm the presence of available
499     * routes that the user may connect to.  This flag may use substantially more
500     * power.
501     * </p>
502     *
503     * <h3>Example</h3>
504     * <pre>
505     * public class MyActivity extends Activity {
506     *     private MediaRouter mRouter;
507     *     private MediaRouter.Callback mCallback;
508     *     private MediaRouteSelector mSelector;
509     *
510     *     protected void onCreate(Bundle savedInstanceState) {
511     *         super.onCreate(savedInstanceState);
512     *
513     *         mRouter = Mediarouter.getInstance(this);
514     *         mCallback = new MyCallback();
515     *         mSelector = new MediaRouteSelector.Builder()
516     *                 .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
517     *                 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
518     *                 .build();
519     *     }
520     *
521     *     // Add the callback on start to tell the media router what kinds of routes
522     *     // the application is interested in so that it can try to discover suitable ones.
523     *     public void onStart() {
524     *         super.onStart();
525     *
526     *         mediaRouter.addCallback(mSelector, mCallback,
527     *                 MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
528     *
529     *         MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
530     *         // do something with the route...
531     *     }
532     *
533     *     // Remove the selector on stop to tell the media router that it no longer
534     *     // needs to invest effort trying to discover routes of these kinds for now.
535     *     public void onStop() {
536     *         super.onStop();
537     *
538     *         mediaRouter.removeCallback(mCallback);
539     *     }
540     *
541     *     private final class MyCallback extends MediaRouter.Callback {
542     *         // Implement callback methods as needed.
543     *     }
544     * }
545     * </pre>
546     *
547     * @param selector A route selector that indicates the kinds of routes that the
548     * callback would like to discover.
549     * @param callback The callback to add.
550     * @param flags Flags to control the behavior of the callback.
551     * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
552     * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
553     * @see #removeCallback
554     */
555    public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback,
556            @CallbackFlags int flags) {
557        if (selector == null) {
558            throw new IllegalArgumentException("selector must not be null");
559        }
560        if (callback == null) {
561            throw new IllegalArgumentException("callback must not be null");
562        }
563        checkCallingThread();
564
565        if (DEBUG) {
566            Log.d(TAG, "addCallback: selector=" + selector
567                    + ", callback=" + callback + ", flags=" + Integer.toHexString(flags));
568        }
569
570        CallbackRecord record;
571        int index = findCallbackRecord(callback);
572        if (index < 0) {
573            record = new CallbackRecord(this, callback);
574            mCallbackRecords.add(record);
575        } else {
576            record = mCallbackRecords.get(index);
577        }
578        boolean updateNeeded = false;
579        if ((flags & ~record.mFlags) != 0) {
580            record.mFlags |= flags;
581            updateNeeded = true;
582        }
583        if (!record.mSelector.contains(selector)) {
584            record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
585                    .addSelector(selector)
586                    .build();
587            updateNeeded = true;
588        }
589        if (updateNeeded) {
590            sGlobal.updateDiscoveryRequest();
591        }
592    }
593
594    /**
595     * Removes the specified callback.  It will no longer receive events about
596     * changes to media routes.
597     *
598     * @param callback The callback to remove.
599     * @see #addCallback
600     */
601    public void removeCallback(@NonNull Callback callback) {
602        if (callback == null) {
603            throw new IllegalArgumentException("callback must not be null");
604        }
605        checkCallingThread();
606
607        if (DEBUG) {
608            Log.d(TAG, "removeCallback: callback=" + callback);
609        }
610
611        int index = findCallbackRecord(callback);
612        if (index >= 0) {
613            mCallbackRecords.remove(index);
614            sGlobal.updateDiscoveryRequest();
615        }
616    }
617
618    private int findCallbackRecord(Callback callback) {
619        final int count = mCallbackRecords.size();
620        for (int i = 0; i < count; i++) {
621            if (mCallbackRecords.get(i).mCallback == callback) {
622                return i;
623            }
624        }
625        return -1;
626    }
627
628    /**
629     * Registers a media route provider within this application process.
630     * <p>
631     * The provider will be added to the list of providers that all {@link MediaRouter}
632     * instances within this process can use to discover routes.
633     * </p>
634     *
635     * @param providerInstance The media route provider instance to add.
636     *
637     * @see MediaRouteProvider
638     * @see #removeCallback
639     */
640    public void addProvider(@NonNull MediaRouteProvider providerInstance) {
641        if (providerInstance == null) {
642            throw new IllegalArgumentException("providerInstance must not be null");
643        }
644        checkCallingThread();
645
646        if (DEBUG) {
647            Log.d(TAG, "addProvider: " + providerInstance);
648        }
649        sGlobal.addProvider(providerInstance);
650    }
651
652    /**
653     * Unregisters a media route provider within this application process.
654     * <p>
655     * The provider will be removed from the list of providers that all {@link MediaRouter}
656     * instances within this process can use to discover routes.
657     * </p>
658     *
659     * @param providerInstance The media route provider instance to remove.
660     *
661     * @see MediaRouteProvider
662     * @see #addCallback
663     */
664    public void removeProvider(@NonNull MediaRouteProvider providerInstance) {
665        if (providerInstance == null) {
666            throw new IllegalArgumentException("providerInstance must not be null");
667        }
668        checkCallingThread();
669
670        if (DEBUG) {
671            Log.d(TAG, "removeProvider: " + providerInstance);
672        }
673        sGlobal.removeProvider(providerInstance);
674    }
675
676    /**
677     * Adds a remote control client to enable remote control of the volume
678     * of the selected route.
679     * <p>
680     * The remote control client must have previously been registered with
681     * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient
682     * AudioManager.registerRemoteControlClient} method.
683     * </p>
684     *
685     * @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
686     */
687    public void addRemoteControlClient(@NonNull Object remoteControlClient) {
688        if (remoteControlClient == null) {
689            throw new IllegalArgumentException("remoteControlClient must not be null");
690        }
691        checkCallingThread();
692
693        if (DEBUG) {
694            Log.d(TAG, "addRemoteControlClient: " + remoteControlClient);
695        }
696        sGlobal.addRemoteControlClient(remoteControlClient);
697    }
698
699    /**
700     * Removes a remote control client.
701     *
702     * @param remoteControlClient The {@link android.media.RemoteControlClient}
703     *            to unregister.
704     */
705    public void removeRemoteControlClient(@NonNull Object remoteControlClient) {
706        if (remoteControlClient == null) {
707            throw new IllegalArgumentException("remoteControlClient must not be null");
708        }
709
710        if (DEBUG) {
711            Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient);
712        }
713        sGlobal.removeRemoteControlClient(remoteControlClient);
714    }
715
716    /**
717     * Sets the media session to enable remote control of the volume of the
718     * selected route. This should be used instead of
719     * {@link #addRemoteControlClient} when using media sessions. Set the
720     * session to null to clear it.
721     *
722     * @param mediaSession The {@link android.media.session.MediaSession} to
723     *            use.
724     */
725    public void setMediaSession(Object mediaSession) {
726        if (DEBUG) {
727            Log.d(TAG, "addMediaSession: " + mediaSession);
728        }
729        sGlobal.setMediaSession(mediaSession);
730    }
731
732    /**
733     * Sets a compat media session to enable remote control of the volume of the
734     * selected route. This should be used instead of
735     * {@link #addRemoteControlClient} when using {@link MediaSessionCompat}.
736     * Set the session to null to clear it.
737     *
738     * @param mediaSession
739     */
740    public void setMediaSessionCompat(MediaSessionCompat mediaSession) {
741        if (DEBUG) {
742            Log.d(TAG, "addMediaSessionCompat: " + mediaSession);
743        }
744        sGlobal.setMediaSessionCompat(mediaSession);
745    }
746
747    public MediaSessionCompat.Token getMediaSessionToken() {
748        return sGlobal.getMediaSessionToken();
749    }
750
751    /**
752     * Ensures that calls into the media router are on the correct thread.
753     * It pays to be a little paranoid when global state invariants are at risk.
754     */
755    static void checkCallingThread() {
756        if (Looper.myLooper() != Looper.getMainLooper()) {
757            throw new IllegalStateException("The media router service must only be "
758                    + "accessed on the application's main thread.");
759        }
760    }
761
762    static <T> boolean equal(T a, T b) {
763        return a == b || (a != null && b != null && a.equals(b));
764    }
765
766    /**
767     * Provides information about a media route.
768     * <p>
769     * Each media route has a list of {@link MediaControlIntent media control}
770     * {@link #getControlFilters intent filters} that describe the capabilities of the
771     * route and the manner in which it is used and controlled.
772     * </p>
773     */
774    public static class RouteInfo {
775        private final ProviderInfo mProvider;
776        private final String mDescriptorId;
777        private final String mUniqueId;
778        private String mName;
779        private String mDescription;
780        private Uri mIconUri;
781        private boolean mEnabled;
782        private boolean mConnecting;
783        private int mConnectionState;
784        private boolean mCanDisconnect;
785        private final ArrayList<IntentFilter> mControlFilters = new ArrayList<>();
786        private int mPlaybackType;
787        private int mPlaybackStream;
788        private int mDeviceType;
789        private int mVolumeHandling;
790        private int mVolume;
791        private int mVolumeMax;
792        private Display mPresentationDisplay;
793        private int mPresentationDisplayId = PRESENTATION_DISPLAY_ID_NONE;
794        private Bundle mExtras;
795        private IntentSender mSettingsIntent;
796        MediaRouteDescriptor mDescriptor;
797
798        /** @hide */
799        @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING,
800                CONNECTION_STATE_CONNECTED})
801        @Retention(RetentionPolicy.SOURCE)
802        private @interface ConnectionState {}
803
804        /**
805         * The default connection state indicating the route is disconnected.
806         *
807         * @see #getConnectionState
808         */
809        public static final int CONNECTION_STATE_DISCONNECTED = 0;
810
811        /**
812         * A connection state indicating the route is in the process of connecting and is not yet
813         * ready for use.
814         *
815         * @see #getConnectionState
816         */
817        public static final int CONNECTION_STATE_CONNECTING = 1;
818
819        /**
820         * A connection state indicating the route is connected.
821         *
822         * @see #getConnectionState
823         */
824        public static final int CONNECTION_STATE_CONNECTED = 2;
825
826        /** @hide */
827        @IntDef({PLAYBACK_TYPE_LOCAL,PLAYBACK_TYPE_REMOTE})
828        @Retention(RetentionPolicy.SOURCE)
829        private @interface PlaybackType {}
830
831        /**
832         * The default playback type, "local", indicating the presentation of the media
833         * is happening on the same device (e.g. a phone, a tablet) as where it is
834         * controlled from.
835         *
836         * @see #getPlaybackType
837         */
838        public static final int PLAYBACK_TYPE_LOCAL = 0;
839
840        /**
841         * A playback type indicating the presentation of the media is happening on
842         * a different device (i.e. the remote device) than where it is controlled from.
843         *
844         * @see #getPlaybackType
845         */
846        public static final int PLAYBACK_TYPE_REMOTE = 1;
847
848        /** @hide */
849        @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH})
850        @Retention(RetentionPolicy.SOURCE)
851        private @interface DeviceType {}
852
853        /**
854         * The default receiver device type of the route indicating the type is unknown.
855         *
856         * @see #getDeviceType
857         * @hide
858         */
859        public static final int DEVICE_TYPE_UNKNOWN = 0;
860
861        /**
862         * A receiver device type of the route indicating the presentation of the media is happening
863         * on a TV.
864         *
865         * @see #getDeviceType
866         */
867        public static final int DEVICE_TYPE_TV = 1;
868
869        /**
870         * A receiver device type of the route indicating the presentation of the media is happening
871         * on a speaker.
872         *
873         * @see #getDeviceType
874         */
875        public static final int DEVICE_TYPE_SPEAKER = 2;
876
877        /**
878         * A receiver device type of the route indicating the presentation of the media is happening
879         * on a bluetooth device such as a bluetooth speaker.
880         *
881         * @see #getDeviceType
882         * @hide
883         */
884        public static final int DEVICE_TYPE_BLUETOOTH = 3;
885
886        /** @hide */
887        @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
888        @Retention(RetentionPolicy.SOURCE)
889        private @interface PlaybackVolume {}
890
891        /**
892         * Playback information indicating the playback volume is fixed, i.e. it cannot be
893         * controlled from this object. An example of fixed playback volume is a remote player,
894         * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
895         * than attenuate at the source.
896         *
897         * @see #getVolumeHandling
898         */
899        public static final int PLAYBACK_VOLUME_FIXED = 0;
900
901        /**
902         * Playback information indicating the playback volume is variable and can be controlled
903         * from this object.
904         *
905         * @see #getVolumeHandling
906         */
907        public static final int PLAYBACK_VOLUME_VARIABLE = 1;
908
909        /**
910         * The default presentation display id indicating no presentation display is associated
911         * with the route.
912         * @hide
913         */
914        public static final int PRESENTATION_DISPLAY_ID_NONE = -1;
915
916        static final int CHANGE_GENERAL = 1 << 0;
917        static final int CHANGE_VOLUME = 1 << 1;
918        static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2;
919
920        // Should match to SystemMediaRouteProvider.PACKAGE_NAME.
921        static final String SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME = "android";
922
923        RouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
924            mProvider = provider;
925            mDescriptorId = descriptorId;
926            mUniqueId = uniqueId;
927        }
928
929        /**
930         * Gets information about the provider of this media route.
931         */
932        public ProviderInfo getProvider() {
933            return mProvider;
934        }
935
936        /**
937         * Gets the unique id of the route.
938         * <p>
939         * The route unique id functions as a stable identifier by which the route is known.
940         * For example, an application can use this id as a token to remember the
941         * selected route across restarts or to communicate its identity to a service.
942         * </p>
943         *
944         * @return The unique id of the route, never null.
945         */
946        @NonNull
947        public String getId() {
948            return mUniqueId;
949        }
950
951        /**
952         * Gets the user-visible name of the route.
953         * <p>
954         * The route name identifies the destination represented by the route.
955         * It may be a user-supplied name, an alias, or device serial number.
956         * </p>
957         *
958         * @return The user-visible name of a media route.  This is the string presented
959         * to users who may select this as the active route.
960         */
961        public String getName() {
962            return mName;
963        }
964
965        /**
966         * Gets the user-visible description of the route.
967         * <p>
968         * The route description describes the kind of destination represented by the route.
969         * It may be a user-supplied string, a model number or brand of device.
970         * </p>
971         *
972         * @return The description of the route, or null if none.
973         */
974        @Nullable
975        public String getDescription() {
976            return mDescription;
977        }
978
979        /**
980         * Gets the URI of the icon representing this route.
981         * <p>
982         * This icon will be used in picker UIs if available.
983         * </p>
984         *
985         * @return The URI of the icon representing this route, or null if none.
986         */
987        public Uri getIconUri() {
988            return mIconUri;
989        }
990
991        /**
992         * Returns true if this route is enabled and may be selected.
993         *
994         * @return True if this route is enabled.
995         */
996        public boolean isEnabled() {
997            return mEnabled;
998        }
999
1000        /**
1001         * Returns true if the route is in the process of connecting and is not
1002         * yet ready for use.
1003         *
1004         * @return True if this route is in the process of connecting.
1005         */
1006        public boolean isConnecting() {
1007            return mConnecting;
1008        }
1009
1010        /**
1011         * Gets the connection state of the route.
1012         *
1013         * @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED},
1014         * {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}.
1015         */
1016        @ConnectionState
1017        public int getConnectionState() {
1018            return mConnectionState;
1019        }
1020
1021        /**
1022         * Returns true if this route is currently selected.
1023         *
1024         * @return True if this route is currently selected.
1025         *
1026         * @see MediaRouter#getSelectedRoute
1027         */
1028        public boolean isSelected() {
1029            checkCallingThread();
1030            return sGlobal.getSelectedRoute() == this;
1031        }
1032
1033        /**
1034         * Returns true if this route is the default route.
1035         *
1036         * @return True if this route is the default route.
1037         *
1038         * @see MediaRouter#getDefaultRoute
1039         */
1040        public boolean isDefault() {
1041            checkCallingThread();
1042            return sGlobal.getDefaultRoute() == this;
1043        }
1044
1045        /**
1046         * Gets a list of {@link MediaControlIntent media control intent} filters that
1047         * describe the capabilities of this route and the media control actions that
1048         * it supports.
1049         *
1050         * @return A list of intent filters that specifies the media control intents that
1051         * this route supports.
1052         *
1053         * @see MediaControlIntent
1054         * @see #supportsControlCategory
1055         * @see #supportsControlRequest
1056         */
1057        public List<IntentFilter> getControlFilters() {
1058            return mControlFilters;
1059        }
1060
1061        /**
1062         * Returns true if the route supports at least one of the capabilities
1063         * described by a media route selector.
1064         *
1065         * @param selector The selector that specifies the capabilities to check.
1066         * @return True if the route supports at least one of the capabilities
1067         * described in the media route selector.
1068         */
1069        public boolean matchesSelector(@NonNull MediaRouteSelector selector) {
1070            if (selector == null) {
1071                throw new IllegalArgumentException("selector must not be null");
1072            }
1073            checkCallingThread();
1074            return selector.matchesControlFilters(mControlFilters);
1075        }
1076
1077        /**
1078         * Returns true if the route supports the specified
1079         * {@link MediaControlIntent media control} category.
1080         * <p>
1081         * Media control categories describe the capabilities of this route
1082         * such as whether it supports live audio streaming or remote playback.
1083         * </p>
1084         *
1085         * @param category A {@link MediaControlIntent media control} category
1086         * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
1087         * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
1088         * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
1089         * media control category.
1090         * @return True if the route supports the specified intent category.
1091         *
1092         * @see MediaControlIntent
1093         * @see #getControlFilters
1094         */
1095        public boolean supportsControlCategory(@NonNull String category) {
1096            if (category == null) {
1097                throw new IllegalArgumentException("category must not be null");
1098            }
1099            checkCallingThread();
1100
1101            int count = mControlFilters.size();
1102            for (int i = 0; i < count; i++) {
1103                if (mControlFilters.get(i).hasCategory(category)) {
1104                    return true;
1105                }
1106            }
1107            return false;
1108        }
1109
1110        /**
1111         * Returns true if the route supports the specified
1112         * {@link MediaControlIntent media control} category and action.
1113         * <p>
1114         * Media control actions describe specific requests that an application
1115         * can ask a route to perform.
1116         * </p>
1117         *
1118         * @param category A {@link MediaControlIntent media control} category
1119         * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
1120         * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
1121         * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
1122         * media control category.
1123         * @param action A {@link MediaControlIntent media control} action
1124         * such as {@link MediaControlIntent#ACTION_PLAY}.
1125         * @return True if the route supports the specified intent action.
1126         *
1127         * @see MediaControlIntent
1128         * @see #getControlFilters
1129         */
1130        public boolean supportsControlAction(@NonNull String category, @NonNull String action) {
1131            if (category == null) {
1132                throw new IllegalArgumentException("category must not be null");
1133            }
1134            if (action == null) {
1135                throw new IllegalArgumentException("action must not be null");
1136            }
1137            checkCallingThread();
1138
1139            int count = mControlFilters.size();
1140            for (int i = 0; i < count; i++) {
1141                IntentFilter filter = mControlFilters.get(i);
1142                if (filter.hasCategory(category) && filter.hasAction(action)) {
1143                    return true;
1144                }
1145            }
1146            return false;
1147        }
1148
1149        /**
1150         * Returns true if the route supports the specified
1151         * {@link MediaControlIntent media control} request.
1152         * <p>
1153         * Media control requests are used to request the route to perform
1154         * actions such as starting remote playback of a media item.
1155         * </p>
1156         *
1157         * @param intent A {@link MediaControlIntent media control intent}.
1158         * @return True if the route can handle the specified intent.
1159         *
1160         * @see MediaControlIntent
1161         * @see #getControlFilters
1162         */
1163        public boolean supportsControlRequest(@NonNull Intent intent) {
1164            if (intent == null) {
1165                throw new IllegalArgumentException("intent must not be null");
1166            }
1167            checkCallingThread();
1168
1169            ContentResolver contentResolver = sGlobal.getContentResolver();
1170            int count = mControlFilters.size();
1171            for (int i = 0; i < count; i++) {
1172                if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) {
1173                    return true;
1174                }
1175            }
1176            return false;
1177        }
1178
1179        /**
1180         * Sends a {@link MediaControlIntent media control} request to be performed
1181         * asynchronously by the route's destination.
1182         * <p>
1183         * Media control requests are used to request the route to perform
1184         * actions such as starting remote playback of a media item.
1185         * </p><p>
1186         * This function may only be called on a selected route.  Control requests
1187         * sent to unselected routes will fail.
1188         * </p>
1189         *
1190         * @param intent A {@link MediaControlIntent media control intent}.
1191         * @param callback A {@link ControlRequestCallback} to invoke with the result
1192         * of the request, or null if no result is required.
1193         *
1194         * @see MediaControlIntent
1195         */
1196        public void sendControlRequest(@NonNull Intent intent,
1197                @Nullable ControlRequestCallback callback) {
1198            if (intent == null) {
1199                throw new IllegalArgumentException("intent must not be null");
1200            }
1201            checkCallingThread();
1202
1203            sGlobal.sendControlRequest(this, intent, callback);
1204        }
1205
1206        /**
1207         * Gets the type of playback associated with this route.
1208         *
1209         * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL}
1210         * or {@link #PLAYBACK_TYPE_REMOTE}.
1211         */
1212        @PlaybackType
1213        public int getPlaybackType() {
1214            return mPlaybackType;
1215        }
1216
1217        /**
1218         * Gets the audio stream over which the playback associated with this route is performed.
1219         *
1220         * @return The stream over which the playback associated with this route is performed.
1221         */
1222        public int getPlaybackStream() {
1223            return mPlaybackStream;
1224        }
1225
1226        /**
1227         * Gets the type of the receiver device associated with this route.
1228         *
1229         * @return The type of the receiver device associated with this route:
1230         * {@link #DEVICE_TYPE_TV} or {@link #DEVICE_TYPE_SPEAKER}.
1231         */
1232        public int getDeviceType() {
1233            return mDeviceType;
1234        }
1235
1236
1237        /**
1238         * Gets whether the type of the receiver device associated with this route is
1239         * {@link #DEVICE_TYPE_BLUETOOTH}.
1240         * <p>
1241         * This is a workaround for platform version 23 or below where the system route provider
1242         * doesn't specify device type for bluetooth media routes.
1243         * </p>
1244         *
1245         * @return True if the receiver device type can be assumed to be
1246         *         {@link #DEVICE_TYPE_BLUETOOTH}, false otherwise.
1247         * @hide
1248         */
1249        public boolean isDeviceTypeBluetooth() {
1250            if (mDeviceType == DEVICE_TYPE_BLUETOOTH) {
1251                return true;
1252            }
1253            return isSystemMediaRouteProvider(this)
1254                    && supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
1255                    && !supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
1256        }
1257
1258        private static boolean isSystemMediaRouteProvider(MediaRouter.RouteInfo route) {
1259            return TextUtils.equals(route.getProviderInstance().getMetadata().getPackageName(),
1260                    SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME);
1261        }
1262
1263        /**
1264         * Gets information about how volume is handled on the route.
1265         *
1266         * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED}
1267         * or {@link #PLAYBACK_VOLUME_VARIABLE}.
1268         */
1269        @PlaybackVolume
1270        public int getVolumeHandling() {
1271            return mVolumeHandling;
1272        }
1273
1274        /**
1275         * Gets the current volume for this route. Depending on the route, this may only
1276         * be valid if the route is currently selected.
1277         *
1278         * @return The volume at which the playback associated with this route is performed.
1279         */
1280        public int getVolume() {
1281            return mVolume;
1282        }
1283
1284        /**
1285         * Gets the maximum volume at which the playback associated with this route is performed.
1286         *
1287         * @return The maximum volume at which the playback associated with
1288         * this route is performed.
1289         */
1290        public int getVolumeMax() {
1291            return mVolumeMax;
1292        }
1293
1294        /**
1295         * Gets whether this route supports disconnecting without interrupting
1296         * playback.
1297         *
1298         * @return True if this route can disconnect without stopping playback,
1299         *         false otherwise.
1300         */
1301        public boolean canDisconnect() {
1302            return mCanDisconnect;
1303        }
1304
1305        /**
1306         * Requests a volume change for this route asynchronously.
1307         * <p>
1308         * This function may only be called on a selected route.  It will have
1309         * no effect if the route is currently unselected.
1310         * </p>
1311         *
1312         * @param volume The new volume value between 0 and {@link #getVolumeMax}.
1313         */
1314        public void requestSetVolume(int volume) {
1315            checkCallingThread();
1316            sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume)));
1317        }
1318
1319        /**
1320         * Requests an incremental volume update for this route asynchronously.
1321         * <p>
1322         * This function may only be called on a selected route.  It will have
1323         * no effect if the route is currently unselected.
1324         * </p>
1325         *
1326         * @param delta The delta to add to the current volume.
1327         */
1328        public void requestUpdateVolume(int delta) {
1329            checkCallingThread();
1330            if (delta != 0) {
1331                sGlobal.requestUpdateVolume(this, delta);
1332            }
1333        }
1334
1335        /**
1336         * Gets the {@link Display} that should be used by the application to show
1337         * a {@link android.app.Presentation} on an external display when this route is selected.
1338         * Depending on the route, this may only be valid if the route is currently
1339         * selected.
1340         * <p>
1341         * The preferred presentation display may change independently of the route
1342         * being selected or unselected.  For example, the presentation display
1343         * of the default system route may change when an external HDMI display is connected
1344         * or disconnected even though the route itself has not changed.
1345         * </p><p>
1346         * This method may return null if there is no external display associated with
1347         * the route or if the display is not ready to show UI yet.
1348         * </p><p>
1349         * The application should listen for changes to the presentation display
1350         * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
1351         * show or dismiss its {@link android.app.Presentation} accordingly when the display
1352         * becomes available or is removed.
1353         * </p><p>
1354         * This method only makes sense for
1355         * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes.
1356         * </p>
1357         *
1358         * @return The preferred presentation display to use when this route is
1359         * selected or null if none.
1360         *
1361         * @see MediaControlIntent#CATEGORY_LIVE_VIDEO
1362         * @see android.app.Presentation
1363         */
1364        @Nullable
1365        public Display getPresentationDisplay() {
1366            checkCallingThread();
1367            if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
1368                mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId);
1369            }
1370            return mPresentationDisplay;
1371        }
1372
1373        /**
1374         * Gets the route's presentation display id, or -1 if none.
1375         * @hide
1376         */
1377        public int getPresentationDisplayId() {
1378            return mPresentationDisplayId;
1379        }
1380
1381        /**
1382         * Gets a collection of extra properties about this route that were supplied
1383         * by its media route provider, or null if none.
1384         */
1385        @Nullable
1386        public Bundle getExtras() {
1387            return mExtras;
1388        }
1389
1390        /**
1391         * Gets an intent sender for launching a settings activity for this
1392         * route.
1393         */
1394        @Nullable
1395        public IntentSender getSettingsIntent() {
1396            return mSettingsIntent;
1397        }
1398
1399        /**
1400         * Selects this media route.
1401         */
1402        public void select() {
1403            checkCallingThread();
1404            sGlobal.selectRoute(this);
1405        }
1406
1407        @Override
1408        public String toString() {
1409            return "MediaRouter.RouteInfo{ uniqueId=" + mUniqueId
1410                    + ", name=" + mName
1411                    + ", description=" + mDescription
1412                    + ", iconUri=" + mIconUri
1413                    + ", enabled=" + mEnabled
1414                    + ", connecting=" + mConnecting
1415                    + ", connectionState=" + mConnectionState
1416                    + ", canDisconnect=" + mCanDisconnect
1417                    + ", playbackType=" + mPlaybackType
1418                    + ", playbackStream=" + mPlaybackStream
1419                    + ", deviceType=" + mDeviceType
1420                    + ", volumeHandling=" + mVolumeHandling
1421                    + ", volume=" + mVolume
1422                    + ", volumeMax=" + mVolumeMax
1423                    + ", presentationDisplayId=" + mPresentationDisplayId
1424                    + ", extras=" + mExtras
1425                    + ", settingsIntent=" + mSettingsIntent
1426                    + ", providerPackageName=" + mProvider.getPackageName()
1427                    + " }";
1428        }
1429
1430        int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
1431            int changes = 0;
1432            if (mDescriptor != descriptor) {
1433                changes = updateDescriptor(descriptor);
1434            }
1435            return changes;
1436        }
1437
1438        int updateDescriptor(MediaRouteDescriptor descriptor) {
1439            int changes = 0;
1440            mDescriptor = descriptor;
1441            if (descriptor != null) {
1442                if (!equal(mName, descriptor.getName())) {
1443                    mName = descriptor.getName();
1444                    changes |= CHANGE_GENERAL;
1445                }
1446                if (!equal(mDescription, descriptor.getDescription())) {
1447                    mDescription = descriptor.getDescription();
1448                    changes |= CHANGE_GENERAL;
1449                }
1450                if (!equal(mIconUri, descriptor.getIconUri())) {
1451                    mIconUri = descriptor.getIconUri();
1452                    changes |= CHANGE_GENERAL;
1453                }
1454                if (mEnabled != descriptor.isEnabled()) {
1455                    mEnabled = descriptor.isEnabled();
1456                    changes |= CHANGE_GENERAL;
1457                }
1458                if (mConnecting != descriptor.isConnecting()) {
1459                    mConnecting = descriptor.isConnecting();
1460                    changes |= CHANGE_GENERAL;
1461                }
1462                if (mConnectionState != descriptor.getConnectionState()) {
1463                    mConnectionState = descriptor.getConnectionState();
1464                    changes |= CHANGE_GENERAL;
1465                }
1466                if (!mControlFilters.equals(descriptor.getControlFilters())) {
1467                    mControlFilters.clear();
1468                    mControlFilters.addAll(descriptor.getControlFilters());
1469                    changes |= CHANGE_GENERAL;
1470                }
1471                if (mPlaybackType != descriptor.getPlaybackType()) {
1472                    mPlaybackType = descriptor.getPlaybackType();
1473                    changes |= CHANGE_GENERAL;
1474                }
1475                if (mPlaybackStream != descriptor.getPlaybackStream()) {
1476                    mPlaybackStream = descriptor.getPlaybackStream();
1477                    changes |= CHANGE_GENERAL;
1478                }
1479                if (mDeviceType != descriptor.getDeviceType()) {
1480                    mDeviceType = descriptor.getDeviceType();
1481                    changes |= CHANGE_GENERAL;
1482                }
1483                if (mVolumeHandling != descriptor.getVolumeHandling()) {
1484                    mVolumeHandling = descriptor.getVolumeHandling();
1485                    changes |= CHANGE_GENERAL | CHANGE_VOLUME;
1486                }
1487                if (mVolume != descriptor.getVolume()) {
1488                    mVolume = descriptor.getVolume();
1489                    changes |= CHANGE_GENERAL | CHANGE_VOLUME;
1490                }
1491                if (mVolumeMax != descriptor.getVolumeMax()) {
1492                    mVolumeMax = descriptor.getVolumeMax();
1493                    changes |= CHANGE_GENERAL | CHANGE_VOLUME;
1494                }
1495                if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) {
1496                    mPresentationDisplayId = descriptor.getPresentationDisplayId();
1497                    mPresentationDisplay = null;
1498                    changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
1499                }
1500                if (!equal(mExtras, descriptor.getExtras())) {
1501                    mExtras = descriptor.getExtras();
1502                    changes |= CHANGE_GENERAL;
1503                }
1504                if (!equal(mSettingsIntent, descriptor.getSettingsActivity())) {
1505                    mSettingsIntent = descriptor.getSettingsActivity();
1506                    changes |= CHANGE_GENERAL;
1507                }
1508                if (mCanDisconnect != descriptor.canDisconnectAndKeepPlaying()) {
1509                    mCanDisconnect = descriptor.canDisconnectAndKeepPlaying();
1510                    changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
1511                }
1512            }
1513            return changes;
1514        }
1515
1516        String getDescriptorId() {
1517            return mDescriptorId;
1518        }
1519
1520        /** @hide */
1521        public MediaRouteProvider getProviderInstance() {
1522            return mProvider.getProviderInstance();
1523        }
1524    }
1525
1526    /**
1527     * Information about a route that consists of multiple other routes in a group.
1528     * @hide
1529     */
1530    public static class RouteGroup extends RouteInfo {
1531        private List<RouteInfo> mRoutes = new ArrayList<>();
1532
1533        RouteGroup(ProviderInfo provider, String descriptorId, String uniqueId) {
1534            super(provider, descriptorId, uniqueId);
1535        }
1536
1537        /**
1538         * @return The number of routes in this group
1539         */
1540        public int getRouteCount() {
1541            return mRoutes.size();
1542        }
1543
1544        /**
1545         * Returns the route in this group at the specified index
1546         *
1547         * @param index Index to fetch
1548         * @return The route at index
1549         */
1550        public RouteInfo getRouteAt(int index) {
1551            return mRoutes.get(index);
1552        }
1553
1554        /**
1555         * Returns the routes in this group
1556         *
1557         * @return The list of the routes in this group
1558         */
1559        public List<RouteInfo> getRoutes() {
1560            return mRoutes;
1561        }
1562
1563        @Override
1564        public String toString() {
1565            StringBuilder sb = new StringBuilder(super.toString());
1566            sb.append('[');
1567            final int count = mRoutes.size();
1568            for (int i = 0; i < count; i++) {
1569                if (i > 0) sb.append(", ");
1570                sb.append(mRoutes.get(i));
1571            }
1572            sb.append(']');
1573            return sb.toString();
1574        }
1575
1576        @Override
1577        int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
1578            boolean changed = false;
1579            if (mDescriptor != descriptor) {
1580                mDescriptor = descriptor;
1581                if (descriptor != null) {
1582                    List<String> groupMemberIds = descriptor.getGroupMemberIds();
1583                    List<RouteInfo> routes = new ArrayList<>();
1584                    changed = groupMemberIds.size() != mRoutes.size();
1585                    for (String groupMemberId : groupMemberIds) {
1586                        String uniqueId = sGlobal.getUniqueId(getProvider(), groupMemberId);
1587                        RouteInfo groupMember = sGlobal.getRoute(uniqueId);
1588                        if (groupMember != null) {
1589                            routes.add(groupMember);
1590                            if (!changed && !mRoutes.contains(groupMember)) {
1591                                changed = true;
1592                            }
1593                        }
1594                    }
1595                    if (changed) {
1596                        mRoutes = routes;
1597                    }
1598                }
1599            }
1600            return (changed ? CHANGE_GENERAL : 0) | super.updateDescriptor(descriptor);
1601        }
1602    }
1603
1604    /**
1605     * Provides information about a media route provider.
1606     * <p>
1607     * This object may be used to determine which media route provider has
1608     * published a particular route.
1609     * </p>
1610     */
1611    public static final class ProviderInfo {
1612        private final MediaRouteProvider mProviderInstance;
1613        private final List<RouteInfo> mRoutes = new ArrayList<>();
1614
1615        private final ProviderMetadata mMetadata;
1616        private MediaRouteProviderDescriptor mDescriptor;
1617        private Resources mResources;
1618        private boolean mResourcesNotAvailable;
1619
1620        ProviderInfo(MediaRouteProvider provider) {
1621            mProviderInstance = provider;
1622            mMetadata = provider.getMetadata();
1623        }
1624
1625        /**
1626         * Gets the provider's underlying {@link MediaRouteProvider} instance.
1627         */
1628        public MediaRouteProvider getProviderInstance() {
1629            checkCallingThread();
1630            return mProviderInstance;
1631        }
1632
1633        /**
1634         * Gets the package name of the media route provider.
1635         */
1636        public String getPackageName() {
1637            return mMetadata.getPackageName();
1638        }
1639
1640        /**
1641         * Gets the component name of the media route provider.
1642         */
1643        public ComponentName getComponentName() {
1644            return mMetadata.getComponentName();
1645        }
1646
1647        /**
1648         * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider.
1649         */
1650        public List<RouteInfo> getRoutes() {
1651            checkCallingThread();
1652            return mRoutes;
1653        }
1654
1655        Resources getResources() {
1656            if (mResources == null && !mResourcesNotAvailable) {
1657                String packageName = getPackageName();
1658                Context context = sGlobal.getProviderContext(packageName);
1659                if (context != null) {
1660                    mResources = context.getResources();
1661                } else {
1662                    Log.w(TAG, "Unable to obtain resources for route provider package: "
1663                            + packageName);
1664                    mResourcesNotAvailable = true;
1665                }
1666            }
1667            return mResources;
1668        }
1669
1670        boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
1671            if (mDescriptor != descriptor) {
1672                mDescriptor = descriptor;
1673                return true;
1674            }
1675            return false;
1676        }
1677
1678        int findRouteByDescriptorId(String id) {
1679            final int count = mRoutes.size();
1680            for (int i = 0; i < count; i++) {
1681                if (mRoutes.get(i).mDescriptorId.equals(id)) {
1682                    return i;
1683                }
1684            }
1685            return -1;
1686        }
1687
1688        @Override
1689        public String toString() {
1690            return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
1691                    + " }";
1692        }
1693    }
1694
1695    /**
1696     * Interface for receiving events about media routing changes.
1697     * All methods of this interface will be called from the application's main thread.
1698     * <p>
1699     * A Callback will only receive events relevant to routes that the callback
1700     * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
1701     * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}.
1702     * </p>
1703     *
1704     * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int)
1705     * @see MediaRouter#removeCallback(Callback)
1706     */
1707    public static abstract class Callback {
1708        /**
1709         * Called when the supplied media route becomes selected as the active route.
1710         *
1711         * @param router The media router reporting the event.
1712         * @param route The route that has been selected.
1713         */
1714        public void onRouteSelected(MediaRouter router, RouteInfo route) {
1715        }
1716
1717        /**
1718         * Called when the supplied media route becomes unselected as the active route.
1719         * For detailed reason, override {@link #onRouteUnselected(MediaRouter, RouteInfo, int)}
1720         * instead.
1721         *
1722         * @param router The media router reporting the event.
1723         * @param route The route that has been unselected.
1724         */
1725        public void onRouteUnselected(MediaRouter router, RouteInfo route) {
1726        }
1727
1728        /**
1729         * Called when the supplied media route becomes unselected as the active route.
1730         * The default implementation calls {@link #onRouteUnselected}.
1731         * <p>
1732         * The reason provided will be one of the following:
1733         * <ul>
1734         * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
1735         * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
1736         * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
1737         * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
1738         * </ul>
1739         *
1740         * @param router The media router reporting the event.
1741         * @param route The route that has been unselected.
1742         * @param reason The reason for unselecting the route.
1743         */
1744        public void onRouteUnselected(MediaRouter router, RouteInfo route, int reason) {
1745            onRouteUnselected(router, route);
1746        }
1747
1748        /**
1749         * Called when a media route has been added.
1750         *
1751         * @param router The media router reporting the event.
1752         * @param route The route that has become available for use.
1753         */
1754        public void onRouteAdded(MediaRouter router, RouteInfo route) {
1755        }
1756
1757        /**
1758         * Called when a media route has been removed.
1759         *
1760         * @param router The media router reporting the event.
1761         * @param route The route that has been removed from availability.
1762         */
1763        public void onRouteRemoved(MediaRouter router, RouteInfo route) {
1764        }
1765
1766        /**
1767         * Called when a property of the indicated media route has changed.
1768         *
1769         * @param router The media router reporting the event.
1770         * @param route The route that was changed.
1771         */
1772        public void onRouteChanged(MediaRouter router, RouteInfo route) {
1773        }
1774
1775        /**
1776         * Called when a media route's volume changes.
1777         *
1778         * @param router The media router reporting the event.
1779         * @param route The route whose volume changed.
1780         */
1781        public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
1782        }
1783
1784        /**
1785         * Called when a media route's presentation display changes.
1786         * <p>
1787         * This method is called whenever the route's presentation display becomes
1788         * available, is removed or has changes to some of its properties (such as its size).
1789         * </p>
1790         *
1791         * @param router The media router reporting the event.
1792         * @param route The route whose presentation display changed.
1793         *
1794         * @see RouteInfo#getPresentationDisplay()
1795         */
1796        public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
1797        }
1798
1799        /**
1800         * Called when a media route provider has been added.
1801         *
1802         * @param router The media router reporting the event.
1803         * @param provider The provider that has become available for use.
1804         */
1805        public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
1806        }
1807
1808        /**
1809         * Called when a media route provider has been removed.
1810         *
1811         * @param router The media router reporting the event.
1812         * @param provider The provider that has been removed from availability.
1813         */
1814        public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
1815        }
1816
1817        /**
1818         * Called when a property of the indicated media route provider has changed.
1819         *
1820         * @param router The media router reporting the event.
1821         * @param provider The provider that was changed.
1822         */
1823        public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
1824        }
1825    }
1826
1827    /**
1828     * Callback which is invoked with the result of a media control request.
1829     *
1830     * @see RouteInfo#sendControlRequest
1831     */
1832    public static abstract class ControlRequestCallback {
1833        /**
1834         * Called when a media control request succeeds.
1835         *
1836         * @param data Result data, or null if none.
1837         * Contents depend on the {@link MediaControlIntent media control action}.
1838         */
1839        public void onResult(Bundle data) {
1840        }
1841
1842        /**
1843         * Called when a media control request fails.
1844         *
1845         * @param error A localized error message which may be shown to the user, or null
1846         * if the cause of the error is unclear.
1847         * @param data Error data, or null if none.
1848         * Contents depend on the {@link MediaControlIntent media control action}.
1849         */
1850        public void onError(String error, Bundle data) {
1851        }
1852    }
1853
1854    private static final class CallbackRecord {
1855        public final MediaRouter mRouter;
1856        public final Callback mCallback;
1857        public MediaRouteSelector mSelector;
1858        public int mFlags;
1859
1860        public CallbackRecord(MediaRouter router, Callback callback) {
1861            mRouter = router;
1862            mCallback = callback;
1863            mSelector = MediaRouteSelector.EMPTY;
1864        }
1865
1866        public boolean filterRouteEvent(RouteInfo route) {
1867            return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
1868                    || route.matchesSelector(mSelector);
1869        }
1870    }
1871
1872    /**
1873     * Global state for the media router.
1874     * <p>
1875     * Media routes and media route providers are global to the process; their
1876     * state and the bulk of the media router implementation lives here.
1877     * </p>
1878     */
1879    private static final class GlobalMediaRouter
1880            implements SystemMediaRouteProvider.SyncCallback,
1881            RegisteredMediaRouteProviderWatcher.Callback {
1882        private final Context mApplicationContext;
1883        private final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
1884        private final ArrayList<RouteInfo> mRoutes = new ArrayList<>();
1885        private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>();
1886        private final ArrayList<ProviderInfo> mProviders = new ArrayList<>();
1887        private final ArrayList<RemoteControlClientRecord> mRemoteControlClients =
1888                new ArrayList<>();
1889        private final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
1890                new RemoteControlClientCompat.PlaybackInfo();
1891        private final ProviderCallback mProviderCallback = new ProviderCallback();
1892        private final CallbackHandler mCallbackHandler = new CallbackHandler();
1893        private final DisplayManagerCompat mDisplayManager;
1894        private final SystemMediaRouteProvider mSystemProvider;
1895        private final boolean mLowRam;
1896
1897        private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
1898        private RouteInfo mDefaultRoute;
1899        private RouteInfo mSelectedRoute;
1900        private RouteController mSelectedRouteController;
1901        private Map<String, RouteController> mGroupMemberControllers;
1902        private MediaRouteDiscoveryRequest mDiscoveryRequest;
1903        private MediaSessionRecord mMediaSession;
1904        private MediaSessionCompat mRccMediaSession;
1905        private MediaSessionCompat mCompatSession;
1906        private MediaSessionCompat.OnActiveChangeListener mSessionActiveListener =
1907                new MediaSessionCompat.OnActiveChangeListener() {
1908            @Override
1909            public void onActiveChanged() {
1910                if(mRccMediaSession != null) {
1911                    if (mRccMediaSession.isActive()) {
1912                        addRemoteControlClient(mRccMediaSession.getRemoteControlClient());
1913                    } else {
1914                        removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
1915                    }
1916                }
1917            }
1918        };
1919
1920        GlobalMediaRouter(Context applicationContext) {
1921            mApplicationContext = applicationContext;
1922            mDisplayManager = DisplayManagerCompat.getInstance(applicationContext);
1923            mLowRam = ActivityManagerCompat.isLowRamDevice(
1924                    (ActivityManager)applicationContext.getSystemService(
1925                            Context.ACTIVITY_SERVICE));
1926
1927            // Add the system media route provider for interoperating with
1928            // the framework media router.  This one is special and receives
1929            // synchronization messages from the media router.
1930            mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
1931            addProvider(mSystemProvider);
1932        }
1933
1934        public void start() {
1935            // Start watching for routes published by registered media route
1936            // provider services.
1937            mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher(
1938                    mApplicationContext, this);
1939            mRegisteredProviderWatcher.start();
1940        }
1941
1942        public MediaRouter getRouter(Context context) {
1943            MediaRouter router;
1944            for (int i = mRouters.size(); --i >= 0; ) {
1945                router = mRouters.get(i).get();
1946                if (router == null) {
1947                    mRouters.remove(i);
1948                } else if (router.mContext == context) {
1949                    return router;
1950                }
1951            }
1952            router = new MediaRouter(context);
1953            mRouters.add(new WeakReference<MediaRouter>(router));
1954            return router;
1955        }
1956
1957        public ContentResolver getContentResolver() {
1958            return mApplicationContext.getContentResolver();
1959        }
1960
1961        public Context getProviderContext(String packageName) {
1962            if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) {
1963                return mApplicationContext;
1964            }
1965            try {
1966                return mApplicationContext.createPackageContext(
1967                        packageName, Context.CONTEXT_RESTRICTED);
1968            } catch (NameNotFoundException ex) {
1969                return null;
1970            }
1971        }
1972
1973        public Display getDisplay(int displayId) {
1974            return mDisplayManager.getDisplay(displayId);
1975        }
1976
1977        public void sendControlRequest(RouteInfo route,
1978                Intent intent, ControlRequestCallback callback) {
1979            if (route == mSelectedRoute && mSelectedRouteController != null) {
1980                if (mSelectedRouteController.onControlRequest(intent, callback)) {
1981                    return;
1982                }
1983            }
1984            if (callback != null) {
1985                callback.onError(null, null);
1986            }
1987        }
1988
1989        public void requestSetVolume(RouteInfo route, int volume) {
1990            if (route == mSelectedRoute && mSelectedRouteController != null) {
1991                mSelectedRouteController.onSetVolume(volume);
1992            } else if (mGroupMemberControllers != null) {
1993                RouteController controller = mGroupMemberControllers.get(route.mDescriptorId);
1994                if (controller != null) {
1995                    controller.onSetVolume(volume);
1996                }
1997            }
1998        }
1999
2000        public void requestUpdateVolume(RouteInfo route, int delta) {
2001            if (route == mSelectedRoute && mSelectedRouteController != null) {
2002                mSelectedRouteController.onUpdateVolume(delta);
2003            }
2004        }
2005
2006        public RouteInfo getRoute(String uniqueId) {
2007            for (RouteInfo info : mRoutes) {
2008                if (info.mUniqueId.equals(uniqueId)) {
2009                    return info;
2010                }
2011            }
2012            return null;
2013        }
2014
2015        public List<RouteInfo> getRoutes() {
2016            return mRoutes;
2017        }
2018
2019        public List<ProviderInfo> getProviders() {
2020            return mProviders;
2021        }
2022
2023        public RouteInfo getDefaultRoute() {
2024            if (mDefaultRoute == null) {
2025                // This should never happen once the media router has been fully
2026                // initialized but it is good to check for the error in case there
2027                // is a bug in provider initialization.
2028                throw new IllegalStateException("There is no default route.  "
2029                        + "The media router has not yet been fully initialized.");
2030            }
2031            return mDefaultRoute;
2032        }
2033
2034        public RouteInfo getSelectedRoute() {
2035            if (mSelectedRoute == null) {
2036                // This should never happen once the media router has been fully
2037                // initialized but it is good to check for the error in case there
2038                // is a bug in provider initialization.
2039                throw new IllegalStateException("There is no currently selected route.  "
2040                        + "The media router has not yet been fully initialized.");
2041            }
2042            return mSelectedRoute;
2043        }
2044
2045        public void selectRoute(RouteInfo route) {
2046            selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
2047        }
2048
2049        public void selectRoute(RouteInfo route, int unselectReason) {
2050            if (!mRoutes.contains(route)) {
2051                Log.w(TAG, "Ignoring attempt to select removed route: " + route);
2052                return;
2053            }
2054            if (!route.mEnabled) {
2055                Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
2056                return;
2057            }
2058
2059            setSelectedRouteInternal(route, unselectReason);
2060        }
2061
2062        public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
2063            if (selector.isEmpty()) {
2064                return false;
2065            }
2066
2067            // On low-RAM devices, do not rely on actual discovery results unless asked to.
2068            if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) {
2069                return true;
2070            }
2071
2072            // Check whether any existing routes match the selector.
2073            final int routeCount = mRoutes.size();
2074            for (int i = 0; i < routeCount; i++) {
2075                RouteInfo route = mRoutes.get(i);
2076                if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
2077                        && route.isDefault()) {
2078                    continue;
2079                }
2080                if (route.matchesSelector(selector)) {
2081                    return true;
2082                }
2083            }
2084
2085            // It doesn't look like we can find a matching route right now.
2086            return false;
2087        }
2088
2089        public void updateDiscoveryRequest() {
2090            // Combine all of the callback selectors and active scan flags.
2091            boolean discover = false;
2092            boolean activeScan = false;
2093            MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
2094            for (int i = mRouters.size(); --i >= 0; ) {
2095                MediaRouter router = mRouters.get(i).get();
2096                if (router == null) {
2097                    mRouters.remove(i);
2098                } else {
2099                    final int count = router.mCallbackRecords.size();
2100                    for (int j = 0; j < count; j++) {
2101                        CallbackRecord callback = router.mCallbackRecords.get(j);
2102                        builder.addSelector(callback.mSelector);
2103                        if ((callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
2104                            activeScan = true;
2105                            discover = true; // perform active scan implies request discovery
2106                        }
2107                        if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) {
2108                            if (!mLowRam) {
2109                                discover = true;
2110                            }
2111                        }
2112                        if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) {
2113                            discover = true;
2114                        }
2115                    }
2116                }
2117            }
2118            MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY;
2119
2120            // Create a new discovery request.
2121            if (mDiscoveryRequest != null
2122                    && mDiscoveryRequest.getSelector().equals(selector)
2123                    && mDiscoveryRequest.isActiveScan() == activeScan) {
2124                return; // no change
2125            }
2126            if (selector.isEmpty() && !activeScan) {
2127                // Discovery is not needed.
2128                if (mDiscoveryRequest == null) {
2129                    return; // no change
2130                }
2131                mDiscoveryRequest = null;
2132            } else {
2133                // Discovery is needed.
2134                mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
2135            }
2136            if (DEBUG) {
2137                Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
2138            }
2139            if (discover && !activeScan && mLowRam) {
2140                Log.i(TAG, "Forcing passive route discovery on a low-RAM device, "
2141                        + "system performance may be affected.  Please consider using "
2142                        + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of "
2143                        + "CALLBACK_FLAG_FORCE_DISCOVERY.");
2144            }
2145
2146            // Notify providers.
2147            final int providerCount = mProviders.size();
2148            for (int i = 0; i < providerCount; i++) {
2149                mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest);
2150            }
2151        }
2152
2153        @Override
2154        public void addProvider(MediaRouteProvider providerInstance) {
2155            int index = findProviderInfo(providerInstance);
2156            if (index < 0) {
2157                // 1. Add the provider to the list.
2158                ProviderInfo provider = new ProviderInfo(providerInstance);
2159                mProviders.add(provider);
2160                if (DEBUG) {
2161                    Log.d(TAG, "Provider added: " + provider);
2162                }
2163                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
2164                // 2. Create the provider's contents.
2165                updateProviderContents(provider, providerInstance.getDescriptor());
2166                // 3. Register the provider callback.
2167                providerInstance.setCallback(mProviderCallback);
2168                // 4. Set the discovery request.
2169                providerInstance.setDiscoveryRequest(mDiscoveryRequest);
2170            }
2171        }
2172
2173        @Override
2174        public void removeProvider(MediaRouteProvider providerInstance) {
2175            int index = findProviderInfo(providerInstance);
2176            if (index >= 0) {
2177                // 1. Unregister the provider callback.
2178                providerInstance.setCallback(null);
2179                // 2. Clear the discovery request.
2180                providerInstance.setDiscoveryRequest(null);
2181                // 3. Delete the provider's contents.
2182                ProviderInfo provider = mProviders.get(index);
2183                updateProviderContents(provider, null);
2184                // 4. Remove the provider from the list.
2185                if (DEBUG) {
2186                    Log.d(TAG, "Provider removed: " + provider);
2187                }
2188                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
2189                mProviders.remove(index);
2190            }
2191        }
2192
2193        private void updateProviderDescriptor(MediaRouteProvider providerInstance,
2194                MediaRouteProviderDescriptor descriptor) {
2195            int index = findProviderInfo(providerInstance);
2196            if (index >= 0) {
2197                // Update the provider's contents.
2198                ProviderInfo provider = mProviders.get(index);
2199                updateProviderContents(provider, descriptor);
2200            }
2201        }
2202
2203        private int findProviderInfo(MediaRouteProvider providerInstance) {
2204            final int count = mProviders.size();
2205            for (int i = 0; i < count; i++) {
2206                if (mProviders.get(i).mProviderInstance == providerInstance) {
2207                    return i;
2208                }
2209            }
2210            return -1;
2211        }
2212
2213        private void updateProviderContents(ProviderInfo provider,
2214                MediaRouteProviderDescriptor providerDescriptor) {
2215            if (provider.updateDescriptor(providerDescriptor)) {
2216                // Update all existing routes and reorder them to match
2217                // the order of their descriptors.
2218                int targetIndex = 0;
2219                boolean selectedRouteDescriptorChanged = false;
2220                if (providerDescriptor != null) {
2221                    if (providerDescriptor.isValid()) {
2222                        final List<MediaRouteDescriptor> routeDescriptors =
2223                                providerDescriptor.getRoutes();
2224                        final int routeCount = routeDescriptors.size();
2225                        // Updating route group's contents requires all member routes' information.
2226                        // Add the groups to the lists and update them later.
2227                        List<Pair<RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>();
2228                        List<Pair<RouteInfo, MediaRouteDescriptor>> updatedGroups =
2229                                new ArrayList<>();
2230                        for (int i = 0; i < routeCount; i++) {
2231                            final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i);
2232                            final String id = routeDescriptor.getId();
2233                            final int sourceIndex = provider.findRouteByDescriptorId(id);
2234                            if (sourceIndex < 0) {
2235                                // 1. Add the route to the list.
2236                                String uniqueId = assignRouteUniqueId(provider, id);
2237                                boolean isGroup = routeDescriptor.getGroupMemberIds() != null;
2238                                RouteInfo route = isGroup ? new RouteGroup(provider, id, uniqueId) :
2239                                        new RouteInfo(provider, id, uniqueId);
2240                                provider.mRoutes.add(targetIndex++, route);
2241                                mRoutes.add(route);
2242                                // 2. Create the route's contents.
2243                                if (isGroup) {
2244                                    addedGroups.add(new Pair(route, routeDescriptor));
2245                                } else {
2246                                    route.maybeUpdateDescriptor(routeDescriptor);
2247                                    // 3. Notify clients about addition.
2248                                    if (DEBUG) {
2249                                        Log.d(TAG, "Route added: " + route);
2250                                    }
2251                                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
2252                                }
2253
2254                            } else if (sourceIndex < targetIndex) {
2255                                Log.w(TAG, "Ignoring route descriptor with duplicate id: "
2256                                        + routeDescriptor);
2257                            } else {
2258                                // 1. Reorder the route within the list.
2259                                RouteInfo route = provider.mRoutes.get(sourceIndex);
2260                                Collections.swap(provider.mRoutes,
2261                                        sourceIndex, targetIndex++);
2262                                // 2. Update the route's contents.
2263                                if (route instanceof RouteGroup) {
2264                                    updatedGroups.add(new Pair(route, routeDescriptor));
2265                                } else {
2266                                    // 3. Notify clients about changes.
2267                                    if (updateRouteDescriptorAndNotify(route, routeDescriptor)
2268                                            != 0) {
2269                                        if (route == mSelectedRoute) {
2270                                            selectedRouteDescriptorChanged = true;
2271                                        }
2272                                    }
2273                                }
2274                            }
2275                        }
2276                        // Update the new and/or existing groups.
2277                        for (Pair<RouteInfo, MediaRouteDescriptor> pair : addedGroups) {
2278                            RouteInfo route = pair.first;
2279                            route.maybeUpdateDescriptor(pair.second);
2280                            if (DEBUG) {
2281                                Log.d(TAG, "Route added: " + route);
2282                            }
2283                            mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
2284                        }
2285                        for (Pair<RouteInfo, MediaRouteDescriptor> pair : updatedGroups) {
2286                            RouteInfo route = pair.first;
2287                            if (updateRouteDescriptorAndNotify(route, pair.second) != 0) {
2288                                if (route == mSelectedRoute) {
2289                                    selectedRouteDescriptorChanged = true;
2290                                }
2291                            }
2292                        }
2293                    } else {
2294                        Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor);
2295                    }
2296                }
2297
2298                // Dispose all remaining routes that do not have matching descriptors.
2299                for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
2300                    // 1. Delete the route's contents.
2301                    RouteInfo route = provider.mRoutes.get(i);
2302                    route.maybeUpdateDescriptor(null);
2303                    // 2. Remove the route from the list.
2304                    mRoutes.remove(route);
2305                }
2306
2307                // Update the selected route if needed.
2308                updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged);
2309
2310                // Now notify clients about routes that were removed.
2311                // We do this after updating the selected route to ensure
2312                // that the framework media router observes the new route
2313                // selection before the removal since removing the currently
2314                // selected route may have side-effects.
2315                for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
2316                    RouteInfo route = provider.mRoutes.remove(i);
2317                    if (DEBUG) {
2318                        Log.d(TAG, "Route removed: " + route);
2319                    }
2320                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
2321                }
2322
2323                // Notify provider changed.
2324                if (DEBUG) {
2325                    Log.d(TAG, "Provider changed: " + provider);
2326                }
2327                mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
2328            }
2329        }
2330
2331        private int updateRouteDescriptorAndNotify(RouteInfo route,
2332                MediaRouteDescriptor routeDescriptor) {
2333            int changes = route.maybeUpdateDescriptor(routeDescriptor);
2334            if (changes != 0) {
2335                if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
2336                    if (DEBUG) {
2337                        Log.d(TAG, "Route changed: " + route);
2338                    }
2339                    mCallbackHandler.post(
2340                            CallbackHandler.MSG_ROUTE_CHANGED, route);
2341                }
2342                if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
2343                    if (DEBUG) {
2344                        Log.d(TAG, "Route volume changed: " + route);
2345                    }
2346                    mCallbackHandler.post(
2347                            CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
2348                }
2349                if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
2350                    if (DEBUG) {
2351                        Log.d(TAG, "Route presentation display changed: "
2352                                + route);
2353                    }
2354                    mCallbackHandler.post(CallbackHandler.
2355                            MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
2356                }
2357            }
2358            return changes;
2359        }
2360
2361        private String assignRouteUniqueId(ProviderInfo provider, String routeDescriptorId) {
2362            // Although route descriptor ids are unique within a provider, it's
2363            // possible for there to be two providers with the same package name.
2364            // Therefore we must dedupe the composite id.
2365            String componentName = provider.getComponentName().flattenToShortString();
2366            String uniqueId = componentName + ":" + routeDescriptorId;
2367            if (findRouteByUniqueId(uniqueId) < 0) {
2368                mUniqueIdMap.put(new Pair(componentName, routeDescriptorId), uniqueId);
2369                return uniqueId;
2370            }
2371            Log.w(TAG, "Either " + routeDescriptorId + " isn't unique in " + componentName
2372                    + " or we're trying to assign a unique ID for an already added route");
2373            for (int i = 2; ; i++) {
2374                String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i);
2375                if (findRouteByUniqueId(newUniqueId) < 0) {
2376                    mUniqueIdMap.put(new Pair(componentName, routeDescriptorId), newUniqueId);
2377                    return newUniqueId;
2378                }
2379            }
2380        }
2381
2382        private int findRouteByUniqueId(String uniqueId) {
2383            final int count = mRoutes.size();
2384            for (int i = 0; i < count; i++) {
2385                if (mRoutes.get(i).mUniqueId.equals(uniqueId)) {
2386                    return i;
2387                }
2388            }
2389            return -1;
2390        }
2391
2392        private String getUniqueId(ProviderInfo provider, String routeDescriptorId) {
2393            String componentName = provider.getComponentName().flattenToShortString();
2394            return mUniqueIdMap.get(new Pair(componentName, routeDescriptorId));
2395        }
2396
2397        private void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) {
2398            // Update default route.
2399            if (mDefaultRoute != null && !isRouteSelectable(mDefaultRoute)) {
2400                Log.i(TAG, "Clearing the default route because it "
2401                        + "is no longer selectable: " + mDefaultRoute);
2402                mDefaultRoute = null;
2403            }
2404            if (mDefaultRoute == null && !mRoutes.isEmpty()) {
2405                for (RouteInfo route : mRoutes) {
2406                    if (isSystemDefaultRoute(route) && isRouteSelectable(route)) {
2407                        mDefaultRoute = route;
2408                        Log.i(TAG, "Found default route: " + mDefaultRoute);
2409                        break;
2410                    }
2411                }
2412            }
2413
2414            // Update selected route.
2415            if (mSelectedRoute != null && !isRouteSelectable(mSelectedRoute)) {
2416                Log.i(TAG, "Unselecting the current route because it "
2417                        + "is no longer selectable: " + mSelectedRoute);
2418                setSelectedRouteInternal(null,
2419                        MediaRouter.UNSELECT_REASON_UNKNOWN);
2420            }
2421            if (mSelectedRoute == null) {
2422                // Choose a new route.
2423                // This will have the side-effect of updating the playback info when
2424                // the new route is selected.
2425                setSelectedRouteInternal(chooseFallbackRoute(),
2426                        MediaRouter.UNSELECT_REASON_UNKNOWN);
2427            } else if (selectedRouteDescriptorChanged) {
2428                // Update the playback info because the properties of the route have changed.
2429                updatePlaybackInfoFromSelectedRoute();
2430            }
2431        }
2432
2433        private RouteInfo chooseFallbackRoute() {
2434            // When the current route is removed or no longer selectable,
2435            // we want to revert to a live audio route if there is
2436            // one (usually Bluetooth A2DP).  Failing that, use
2437            // the default route.
2438            for (RouteInfo route : mRoutes) {
2439                if (route != mDefaultRoute
2440                        && isSystemLiveAudioOnlyRoute(route)
2441                        && isRouteSelectable(route)) {
2442                    return route;
2443                }
2444            }
2445            return mDefaultRoute;
2446        }
2447
2448        private boolean isSystemLiveAudioOnlyRoute(RouteInfo route) {
2449            return route.getProviderInstance() == mSystemProvider
2450                    && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
2451                    && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
2452        }
2453
2454        private boolean isRouteSelectable(RouteInfo route) {
2455            // This tests whether the route is still valid and enabled.
2456            // The route descriptor field is set to null when the route is removed.
2457            return route.mDescriptor != null && route.mEnabled;
2458        }
2459
2460        private boolean isSystemDefaultRoute(RouteInfo route) {
2461            return route.getProviderInstance() == mSystemProvider
2462                    && route.mDescriptorId.equals(
2463                            SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
2464        }
2465
2466        private void setSelectedRouteInternal(RouteInfo route, int unselectReason) {
2467            if (mSelectedRoute != route) {
2468                if (mSelectedRoute != null) {
2469                    if (DEBUG) {
2470                        Log.d(TAG, "Route unselected: " + mSelectedRoute + " reason: "
2471                                + unselectReason);
2472                    }
2473                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute,
2474                            unselectReason);
2475                    if (mSelectedRouteController != null) {
2476                        mSelectedRouteController.onUnselect(unselectReason);
2477                        mSelectedRouteController.onRelease();
2478                        mSelectedRouteController = null;
2479                    }
2480                    if (mGroupMemberControllers != null) {
2481                        for (RouteController controller : mGroupMemberControllers.values()) {
2482                            controller.onUnselect(unselectReason);
2483                            controller.onRelease();
2484                        }
2485                        mGroupMemberControllers = null;
2486                    }
2487                }
2488
2489                mSelectedRoute = route;
2490
2491                if (mSelectedRoute != null) {
2492                    mSelectedRouteController = route.getProviderInstance().onCreateRouteController(
2493                            route.mDescriptorId);
2494                    if (mSelectedRouteController != null) {
2495                        mSelectedRouteController.onSelect();
2496                    }
2497                    if (DEBUG) {
2498                        Log.d(TAG, "Route selected: " + mSelectedRoute);
2499                    }
2500                    mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
2501
2502                    if (mSelectedRoute instanceof RouteGroup) {
2503                        mGroupMemberControllers = new HashMap<>();
2504                        RouteGroup group = (RouteGroup) mSelectedRoute;
2505                        for (RouteInfo groupMember : group.getRoutes()) {
2506                            RouteController controller = groupMember.getProviderInstance()
2507                                    .onCreateRouteController(groupMember.mDescriptorId);
2508                            controller.onSelect();
2509                            mGroupMemberControllers.put(groupMember.mDescriptorId, controller);
2510                        }
2511                    }
2512                }
2513
2514                updatePlaybackInfoFromSelectedRoute();
2515            }
2516        }
2517
2518        @Override
2519        public RouteInfo getSystemRouteByDescriptorId(String id) {
2520            int providerIndex = findProviderInfo(mSystemProvider);
2521            if (providerIndex >= 0) {
2522                ProviderInfo provider = mProviders.get(providerIndex);
2523                int routeIndex = provider.findRouteByDescriptorId(id);
2524                if (routeIndex >= 0) {
2525                    return provider.mRoutes.get(routeIndex);
2526                }
2527            }
2528            return null;
2529        }
2530
2531        public void addRemoteControlClient(Object rcc) {
2532            int index = findRemoteControlClientRecord(rcc);
2533            if (index < 0) {
2534                RemoteControlClientRecord record = new RemoteControlClientRecord(rcc);
2535                mRemoteControlClients.add(record);
2536            }
2537        }
2538
2539        public void removeRemoteControlClient(Object rcc) {
2540            int index = findRemoteControlClientRecord(rcc);
2541            if (index >= 0) {
2542                RemoteControlClientRecord record = mRemoteControlClients.remove(index);
2543                record.disconnect();
2544            }
2545        }
2546
2547        public void setMediaSession(Object session) {
2548            if (mMediaSession != null) {
2549                mMediaSession.clearVolumeHandling();
2550            }
2551            if (session == null) {
2552                mMediaSession = null;
2553            } else {
2554                mMediaSession = new MediaSessionRecord(session);
2555                updatePlaybackInfoFromSelectedRoute();
2556            }
2557        }
2558
2559        public void setMediaSessionCompat(final MediaSessionCompat session) {
2560            mCompatSession = session;
2561            if (android.os.Build.VERSION.SDK_INT >= 21) {
2562                setMediaSession(session != null ? session.getMediaSession() : null);
2563            } else if (android.os.Build.VERSION.SDK_INT >= 14) {
2564                if (mRccMediaSession != null) {
2565                    removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
2566                    mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener);
2567                }
2568                mRccMediaSession = session;
2569                if (session != null) {
2570                    session.addOnActiveChangeListener(mSessionActiveListener);
2571                    if (session.isActive()) {
2572                        addRemoteControlClient(session.getRemoteControlClient());
2573                    }
2574                }
2575            }
2576        }
2577
2578        public MediaSessionCompat.Token getMediaSessionToken() {
2579            if (mMediaSession != null) {
2580                return mMediaSession.getToken();
2581            } else if (mCompatSession != null) {
2582                return mCompatSession.getSessionToken();
2583            }
2584            return null;
2585        }
2586
2587        private int findRemoteControlClientRecord(Object rcc) {
2588            final int count = mRemoteControlClients.size();
2589            for (int i = 0; i < count; i++) {
2590                RemoteControlClientRecord record = mRemoteControlClients.get(i);
2591                if (record.getRemoteControlClient() == rcc) {
2592                    return i;
2593                }
2594            }
2595            return -1;
2596        }
2597
2598        private void updatePlaybackInfoFromSelectedRoute() {
2599            if (mSelectedRoute != null) {
2600                mPlaybackInfo.volume = mSelectedRoute.getVolume();
2601                mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax();
2602                mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling();
2603                mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream();
2604                mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType();
2605
2606                final int count = mRemoteControlClients.size();
2607                for (int i = 0; i < count; i++) {
2608                    RemoteControlClientRecord record = mRemoteControlClients.get(i);
2609                    record.updatePlaybackInfo();
2610                }
2611                if (mMediaSession != null) {
2612                    if (mSelectedRoute == getDefaultRoute()) {
2613                        // Local route
2614                        mMediaSession.clearVolumeHandling();
2615                    } else {
2616                        @VolumeProviderCompat.ControlType int controlType =
2617                                VolumeProviderCompat.VOLUME_CONTROL_FIXED;
2618                        if (mPlaybackInfo.volumeHandling
2619                                == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
2620                            controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
2621                        }
2622                        mMediaSession.configureVolume(controlType, mPlaybackInfo.volumeMax,
2623                                mPlaybackInfo.volume);
2624                    }
2625                }
2626            } else {
2627                if (mMediaSession != null) {
2628                    mMediaSession.clearVolumeHandling();
2629                }
2630            }
2631        }
2632
2633        private final class ProviderCallback extends MediaRouteProvider.Callback {
2634            @Override
2635            public void onDescriptorChanged(MediaRouteProvider provider,
2636                    MediaRouteProviderDescriptor descriptor) {
2637                updateProviderDescriptor(provider, descriptor);
2638            }
2639        }
2640
2641        private final class MediaSessionRecord {
2642            private final MediaSessionCompat mMsCompat;
2643
2644            private @VolumeProviderCompat.ControlType int mControlType;
2645            private int mMaxVolume;
2646            private VolumeProviderCompat mVpCompat;
2647
2648            public MediaSessionRecord(Object mediaSession) {
2649                mMsCompat = MediaSessionCompat.obtain(mApplicationContext, mediaSession);
2650            }
2651
2652            public void configureVolume(@VolumeProviderCompat.ControlType int controlType,
2653                    int max, int current) {
2654                if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) {
2655                    // If we haven't changed control type or max just set the
2656                    // new current volume
2657                    mVpCompat.setCurrentVolume(current);
2658                } else {
2659                    // Otherwise create a new provider and update
2660                    mVpCompat = new VolumeProviderCompat(controlType, max, current) {
2661                        @Override
2662                        public void onSetVolumeTo(final int volume) {
2663                            mCallbackHandler.post(new Runnable() {
2664                                @Override
2665                                public void run() {
2666                                    if (mSelectedRoute != null) {
2667                                        mSelectedRoute.requestSetVolume(volume);
2668                                    }
2669                                }
2670                            });
2671                        }
2672
2673                        @Override
2674                        public void onAdjustVolume(final int direction) {
2675                            mCallbackHandler.post(new Runnable() {
2676                                @Override
2677                                public void run() {
2678                                    if (mSelectedRoute != null) {
2679                                        mSelectedRoute.requestUpdateVolume(direction);
2680                                    }
2681                                }
2682                            });
2683                        }
2684                    };
2685                    mMsCompat.setPlaybackToRemote(mVpCompat);
2686                }
2687            }
2688
2689            public void clearVolumeHandling() {
2690                mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream);
2691                mVpCompat = null;
2692            }
2693
2694            public MediaSessionCompat.Token getToken() {
2695                return mMsCompat.getSessionToken();
2696            }
2697
2698        }
2699
2700        private final class RemoteControlClientRecord
2701                implements RemoteControlClientCompat.VolumeCallback {
2702            private final RemoteControlClientCompat mRccCompat;
2703            private boolean mDisconnected;
2704
2705            public RemoteControlClientRecord(Object rcc) {
2706                mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc);
2707                mRccCompat.setVolumeCallback(this);
2708                updatePlaybackInfo();
2709            }
2710
2711            public Object getRemoteControlClient() {
2712                return mRccCompat.getRemoteControlClient();
2713            }
2714
2715            public void disconnect() {
2716                mDisconnected = true;
2717                mRccCompat.setVolumeCallback(null);
2718            }
2719
2720            public void updatePlaybackInfo() {
2721                mRccCompat.setPlaybackInfo(mPlaybackInfo);
2722            }
2723
2724            @Override
2725            public void onVolumeSetRequest(int volume) {
2726                if (!mDisconnected && mSelectedRoute != null) {
2727                    mSelectedRoute.requestSetVolume(volume);
2728                }
2729            }
2730
2731            @Override
2732            public void onVolumeUpdateRequest(int direction) {
2733                if (!mDisconnected && mSelectedRoute != null) {
2734                    mSelectedRoute.requestUpdateVolume(direction);
2735                }
2736            }
2737        }
2738
2739        private final class CallbackHandler extends Handler {
2740            private final ArrayList<CallbackRecord> mTempCallbackRecords =
2741                    new ArrayList<CallbackRecord>();
2742
2743            private static final int MSG_TYPE_MASK = 0xff00;
2744            private static final int MSG_TYPE_ROUTE = 0x0100;
2745            private static final int MSG_TYPE_PROVIDER = 0x0200;
2746
2747            public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
2748            public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
2749            public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
2750            public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
2751            public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
2752            public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
2753            public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
2754
2755            public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
2756            public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
2757            public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;
2758
2759            public void post(int msg, Object obj) {
2760                obtainMessage(msg, obj).sendToTarget();
2761            }
2762
2763            public void post(int msg, Object obj, int arg) {
2764                Message message = obtainMessage(msg, obj);
2765                message.arg1 = arg;
2766                message.sendToTarget();
2767            }
2768
2769            @Override
2770            public void handleMessage(Message msg) {
2771                final int what = msg.what;
2772                final Object obj = msg.obj;
2773                final int arg = msg.arg1;
2774
2775                // Synchronize state with the system media router.
2776                syncWithSystemProvider(what, obj);
2777
2778                // Invoke all registered callbacks.
2779                // Build a list of callbacks before invoking them in case callbacks
2780                // are added or removed during dispatch.
2781                try {
2782                    for (int i = mRouters.size(); --i >= 0; ) {
2783                        MediaRouter router = mRouters.get(i).get();
2784                        if (router == null) {
2785                            mRouters.remove(i);
2786                        } else {
2787                            mTempCallbackRecords.addAll(router.mCallbackRecords);
2788                        }
2789                    }
2790
2791                    final int callbackCount = mTempCallbackRecords.size();
2792                    for (int i = 0; i < callbackCount; i++) {
2793                        invokeCallback(mTempCallbackRecords.get(i), what, obj, arg);
2794                    }
2795                } finally {
2796                    mTempCallbackRecords.clear();
2797                }
2798            }
2799
2800            private void syncWithSystemProvider(int what, Object obj) {
2801                switch (what) {
2802                    case MSG_ROUTE_ADDED:
2803                        mSystemProvider.onSyncRouteAdded((RouteInfo)obj);
2804                        break;
2805                    case MSG_ROUTE_REMOVED:
2806                        mSystemProvider.onSyncRouteRemoved((RouteInfo)obj);
2807                        break;
2808                    case MSG_ROUTE_CHANGED:
2809                        mSystemProvider.onSyncRouteChanged((RouteInfo)obj);
2810                        break;
2811                    case MSG_ROUTE_SELECTED:
2812                        mSystemProvider.onSyncRouteSelected((RouteInfo)obj);
2813                        break;
2814                }
2815            }
2816
2817            private void invokeCallback(CallbackRecord record, int what, Object obj, int arg) {
2818                final MediaRouter router = record.mRouter;
2819                final MediaRouter.Callback callback = record.mCallback;
2820                switch (what & MSG_TYPE_MASK) {
2821                    case MSG_TYPE_ROUTE: {
2822                        final RouteInfo route = (RouteInfo)obj;
2823                        if (!record.filterRouteEvent(route)) {
2824                            break;
2825                        }
2826                        switch (what) {
2827                            case MSG_ROUTE_ADDED:
2828                                callback.onRouteAdded(router, route);
2829                                break;
2830                            case MSG_ROUTE_REMOVED:
2831                                callback.onRouteRemoved(router, route);
2832                                break;
2833                            case MSG_ROUTE_CHANGED:
2834                                callback.onRouteChanged(router, route);
2835                                break;
2836                            case MSG_ROUTE_VOLUME_CHANGED:
2837                                callback.onRouteVolumeChanged(router, route);
2838                                break;
2839                            case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
2840                                callback.onRoutePresentationDisplayChanged(router, route);
2841                                break;
2842                            case MSG_ROUTE_SELECTED:
2843                                callback.onRouteSelected(router, route);
2844                                break;
2845                            case MSG_ROUTE_UNSELECTED:
2846                                callback.onRouteUnselected(router, route, arg);
2847                                break;
2848                        }
2849                        break;
2850                    }
2851                    case MSG_TYPE_PROVIDER: {
2852                        final ProviderInfo provider = (ProviderInfo)obj;
2853                        switch (what) {
2854                            case MSG_PROVIDER_ADDED:
2855                                callback.onProviderAdded(router, provider);
2856                                break;
2857                            case MSG_PROVIDER_REMOVED:
2858                                callback.onProviderRemoved(router, provider);
2859                                break;
2860                            case MSG_PROVIDER_CHANGED:
2861                                callback.onProviderChanged(router, provider);
2862                                break;
2863                        }
2864                    }
2865                }
2866            }
2867        }
2868    }
2869}
2870