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