• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 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 
17 package android.media;
18 
19 import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING;
20 import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
21 
22 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
23 
24 import android.Manifest;
25 import android.annotation.CallbackExecutor;
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.RequiresPermission;
29 import android.content.Context;
30 import android.media.session.MediaController;
31 import android.media.session.MediaSessionManager;
32 import android.os.Handler;
33 import android.os.Message;
34 import android.os.RemoteException;
35 import android.os.ServiceManager;
36 import android.os.UserHandle;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.util.ArraySet;
40 import android.util.Log;
41 
42 import com.android.internal.annotations.GuardedBy;
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.util.Preconditions;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.Comparator;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 import java.util.concurrent.ConcurrentHashMap;
55 import java.util.concurrent.ConcurrentMap;
56 import java.util.concurrent.CopyOnWriteArrayList;
57 import java.util.concurrent.Executor;
58 import java.util.concurrent.atomic.AtomicInteger;
59 import java.util.function.Predicate;
60 import java.util.stream.Collectors;
61 
62 /**
63  * A class that monitors and controls media routing of other apps.
64  * {@link android.Manifest.permission#MEDIA_CONTENT_CONTROL} is required to use this class,
65  * or {@link SecurityException} will be thrown.
66  * @hide
67  */
68 public final class MediaRouter2Manager {
69     private static final String TAG = "MR2Manager";
70     private static final Object sLock = new Object();
71     /**
72      * The request ID for requests not asked by this instance.
73      * Shouldn't be used for a valid request.
74      * @hide
75      */
76     public static final int REQUEST_ID_NONE = 0;
77     /** @hide */
78     @VisibleForTesting
79     public static final int TRANSFER_TIMEOUT_MS = 30_000;
80 
81     @GuardedBy("sLock")
82     private static MediaRouter2Manager sInstance;
83 
84     private final Context mContext;
85     private final MediaSessionManager mMediaSessionManager;
86     private final Client mClient;
87     private final IMediaRouterService mMediaRouterService;
88     private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
89     final Handler mHandler;
90     final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = new CopyOnWriteArrayList<>();
91 
92     private final Object mRoutesLock = new Object();
93     @GuardedBy("mRoutesLock")
94     private final Map<String, MediaRoute2Info> mRoutes = new HashMap<>();
95     @NonNull
96     final ConcurrentMap<String, RouteDiscoveryPreference> mDiscoveryPreferenceMap =
97             new ConcurrentHashMap<>();
98     // TODO(b/241888071): Merge mDiscoveryPreferenceMap and mPackageToRouteListingPreferenceMap into
99     //     a single record object maintained by a single package-to-record map.
100     @NonNull
101     private final ConcurrentMap<String, RouteListingPreference>
102             mPackageToRouteListingPreferenceMap = new ConcurrentHashMap<>();
103 
104     private final AtomicInteger mNextRequestId = new AtomicInteger(1);
105     private final CopyOnWriteArrayList<TransferRequest> mTransferRequests =
106             new CopyOnWriteArrayList<>();
107 
108     /**
109      * Gets an instance of media router manager that controls media route of other applications.
110      *
111      * @return The media router manager instance for the context.
112      */
getInstance(@onNull Context context)113     public static MediaRouter2Manager getInstance(@NonNull Context context) {
114         Objects.requireNonNull(context, "context must not be null");
115         synchronized (sLock) {
116             if (sInstance == null) {
117                 sInstance = new MediaRouter2Manager(context);
118             }
119             return sInstance;
120         }
121     }
122 
MediaRouter2Manager(Context context)123     private MediaRouter2Manager(Context context) {
124         mContext = context.getApplicationContext();
125         mMediaRouterService = IMediaRouterService.Stub.asInterface(
126                 ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
127         mMediaSessionManager = (MediaSessionManager) context
128                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
129         mHandler = new Handler(context.getMainLooper());
130         mClient = new Client();
131         try {
132             mMediaRouterService.registerManager(mClient, context.getPackageName());
133         } catch (RemoteException ex) {
134             throw ex.rethrowFromSystemServer();
135         }
136     }
137 
138     /**
139      * Registers a callback to listen route info.
140      *
141      * @param executor the executor that runs the callback
142      * @param callback the callback to add
143      */
registerCallback(@onNull @allbackExecutor Executor executor, @NonNull Callback callback)144     public void registerCallback(@NonNull @CallbackExecutor Executor executor,
145             @NonNull Callback callback) {
146         Objects.requireNonNull(executor, "executor must not be null");
147         Objects.requireNonNull(callback, "callback must not be null");
148 
149         CallbackRecord callbackRecord = new CallbackRecord(executor, callback);
150         if (!mCallbackRecords.addIfAbsent(callbackRecord)) {
151             Log.w(TAG, "Ignoring to register the same callback twice.");
152             return;
153         }
154     }
155 
156     /**
157      * Unregisters the specified callback.
158      *
159      * @param callback the callback to unregister
160      */
unregisterCallback(@onNull Callback callback)161     public void unregisterCallback(@NonNull Callback callback) {
162         Objects.requireNonNull(callback, "callback must not be null");
163 
164         if (!mCallbackRecords.remove(new CallbackRecord(null, callback))) {
165             Log.w(TAG, "unregisterCallback: Ignore unknown callback. " + callback);
166             return;
167         }
168     }
169 
170     /**
171      * Registers a request to scan for remote routes.
172      *
173      * <p>Increases the count of active scanning requests. When the count transitions from zero to
174      * one, sends a request to the system server to start scanning.
175      *
176      * <p>Clients must {@link #unregisterScanRequest() unregister their scan requests} when scanning
177      * is no longer needed, to avoid unnecessary resource usage.
178      */
registerScanRequest()179     public void registerScanRequest() {
180         if (mScanRequestCount.getAndIncrement() == 0) {
181             try {
182                 mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_WHILE_INTERACTIVE);
183             } catch (RemoteException ex) {
184                 throw ex.rethrowFromSystemServer();
185             }
186         }
187     }
188 
189     /**
190      * Unregisters a scan request made by {@link #registerScanRequest()}.
191      *
192      * <p>Decreases the count of active scanning requests. When the count transitions from one to
193      * zero, sends a request to the system server to stop scanning.
194      *
195      * @throws IllegalStateException If called while there are no active scan requests.
196      */
unregisterScanRequest()197     public void unregisterScanRequest() {
198         if (mScanRequestCount.updateAndGet(
199                 count -> {
200                     if (count == 0) {
201                         throw new IllegalStateException(
202                                 "No active scan requests to unregister.");
203                     } else {
204                         return --count;
205                     }
206                 })
207                 == 0) {
208             try {
209                 mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_NOT_SCANNING);
210             } catch (RemoteException ex) {
211                 throw ex.rethrowFromSystemServer();
212             }
213         }
214     }
215 
216     /**
217      * Gets a {@link android.media.session.MediaController} associated with the
218      * given routing session.
219      * If there is no matching media session, {@code null} is returned.
220      */
221     @Nullable
getMediaControllerForRoutingSession( @onNull RoutingSessionInfo sessionInfo)222     public MediaController getMediaControllerForRoutingSession(
223             @NonNull RoutingSessionInfo sessionInfo) {
224         for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
225             if (areSessionsMatched(controller, sessionInfo)) {
226                 return controller;
227             }
228         }
229         return null;
230     }
231 
232     /**
233      * Gets available routes for an application.
234      *
235      * @param packageName the package name of the application
236      */
237     @NonNull
getAvailableRoutes(@onNull String packageName)238     public List<MediaRoute2Info> getAvailableRoutes(@NonNull String packageName) {
239         Objects.requireNonNull(packageName, "packageName must not be null");
240 
241         List<RoutingSessionInfo> sessions = getRoutingSessions(packageName);
242         return getAvailableRoutes(sessions.get(sessions.size() - 1));
243     }
244 
245     /**
246      * Gets routes that can be transferable seamlessly for an application.
247      *
248      * @param packageName the package name of the application
249      */
250     @NonNull
getTransferableRoutes(@onNull String packageName)251     public List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) {
252         Objects.requireNonNull(packageName, "packageName must not be null");
253 
254         List<RoutingSessionInfo> sessions = getRoutingSessions(packageName);
255         return getTransferableRoutes(sessions.get(sessions.size() - 1));
256     }
257 
258     /**
259      * Gets available routes for the given routing session.
260      * The returned routes can be passed to
261      * {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} for transferring the routing session.
262      *
263      * @param sessionInfo the routing session that would be transferred
264      */
265     @NonNull
getAvailableRoutes(@onNull RoutingSessionInfo sessionInfo)266     public List<MediaRoute2Info> getAvailableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
267         return getFilteredRoutes(sessionInfo, /*includeSelectedRoutes=*/true,
268                 /*additionalFilter=*/null);
269     }
270 
271     /**
272      * Gets routes that can be transferable seamlessly for the given routing session.
273      * The returned routes can be passed to
274      * {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} for transferring the routing session.
275      * <p>
276      * This includes routes that are {@link RoutingSessionInfo#getTransferableRoutes() transferable}
277      * by provider itself and routes that are different playback type (e.g. local/remote)
278      * from the given routing session.
279      *
280      * @param sessionInfo the routing session that would be transferred
281      */
282     @NonNull
getTransferableRoutes(@onNull RoutingSessionInfo sessionInfo)283     public List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
284         return getFilteredRoutes(sessionInfo, /*includeSelectedRoutes=*/false,
285                 (route) -> sessionInfo.isSystemSession() ^ route.isSystemRoute());
286     }
287 
getSortedRoutes(RouteDiscoveryPreference preference)288     private List<MediaRoute2Info> getSortedRoutes(RouteDiscoveryPreference preference) {
289         if (!preference.shouldRemoveDuplicates()) {
290             synchronized (mRoutesLock) {
291                 return List.copyOf(mRoutes.values());
292             }
293         }
294         Map<String, Integer> packagePriority = new ArrayMap<>();
295         int count = preference.getDeduplicationPackageOrder().size();
296         for (int i = 0; i < count; i++) {
297             // the last package will have 1 as the priority
298             packagePriority.put(preference.getDeduplicationPackageOrder().get(i), count - i);
299         }
300         ArrayList<MediaRoute2Info> routes;
301         synchronized (mRoutesLock) {
302             routes = new ArrayList<>(mRoutes.values());
303         }
304         // take the negative for descending order
305         routes.sort(
306                 Comparator.comparingInt(
307                         r -> -packagePriority.getOrDefault(r.getProviderPackageName(), 0)));
308         return routes;
309     }
310 
getFilteredRoutes( @onNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, @Nullable Predicate<MediaRoute2Info> additionalFilter)311     private List<MediaRoute2Info> getFilteredRoutes(
312             @NonNull RoutingSessionInfo sessionInfo,
313             boolean includeSelectedRoutes,
314             @Nullable Predicate<MediaRoute2Info> additionalFilter) {
315         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
316 
317         List<MediaRoute2Info> routes = new ArrayList<>();
318 
319         Set<String> deduplicationIdSet = new ArraySet<>();
320         String packageName = sessionInfo.getClientPackageName();
321         RouteDiscoveryPreference discoveryPreference =
322                 mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY);
323 
324         for (MediaRoute2Info route : getSortedRoutes(discoveryPreference)) {
325             if (!route.isVisibleTo(packageName)) {
326                 continue;
327             }
328             boolean transferableRoutesContainRoute =
329                     sessionInfo.getTransferableRoutes().contains(route.getId());
330             boolean selectedRoutesContainRoute =
331                     sessionInfo.getSelectedRoutes().contains(route.getId());
332             if (transferableRoutesContainRoute
333                     || (includeSelectedRoutes && selectedRoutesContainRoute)) {
334                 routes.add(route);
335                 continue;
336             }
337             if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) {
338                 continue;
339             }
340             if (!discoveryPreference.getAllowedPackages().isEmpty()
341                     && (route.getProviderPackageName() == null
342                             || !discoveryPreference
343                                     .getAllowedPackages()
344                                     .contains(route.getProviderPackageName()))) {
345                 continue;
346             }
347             if (additionalFilter != null && !additionalFilter.test(route)) {
348                 continue;
349             }
350             if (discoveryPreference.shouldRemoveDuplicates()) {
351                 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) {
352                     continue;
353                 }
354                 deduplicationIdSet.addAll(route.getDeduplicationIds());
355             }
356             routes.add(route);
357         }
358         return routes;
359     }
360 
361     /**
362      * Returns the preferred features of the specified package name.
363      */
364     @NonNull
getDiscoveryPreference(@onNull String packageName)365     public RouteDiscoveryPreference getDiscoveryPreference(@NonNull String packageName) {
366         Objects.requireNonNull(packageName, "packageName must not be null");
367 
368         return mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY);
369     }
370 
371     /**
372      * Returns the {@link RouteListingPreference} of the app with the given {@code packageName}, or
373      * null if the app has not set any.
374      */
375     @Nullable
getRouteListingPreference(@onNull String packageName)376     public RouteListingPreference getRouteListingPreference(@NonNull String packageName) {
377         Preconditions.checkArgument(!TextUtils.isEmpty(packageName));
378         return mPackageToRouteListingPreferenceMap.get(packageName);
379     }
380 
381     /**
382      * Gets the system routing session for the given {@code targetPackageName}. Apps can select a
383      * route that is not the global route. (e.g. an app can select the device route while BT route
384      * is available.)
385      *
386      * @param targetPackageName the package name of the application.
387      */
388     @Nullable
getSystemRoutingSession(@ullable String targetPackageName)389     public RoutingSessionInfo getSystemRoutingSession(@Nullable String targetPackageName) {
390         try {
391             return mMediaRouterService.getSystemSessionInfoForPackage(
392                     mContext.getPackageName(), targetPackageName);
393         } catch (RemoteException ex) {
394             throw ex.rethrowFromSystemServer();
395         }
396     }
397 
398     /**
399      * Gets the routing session of a media session.
400      * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_LOCAL local playback},
401      * the system routing session is returned.
402      * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_REMOTE remote playback},
403      * it returns the corresponding routing session or {@code null} if it's unavailable.
404      */
405     @Nullable
getRoutingSessionForMediaController(MediaController mediaController)406     public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) {
407         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
408         if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
409             return getSystemRoutingSession(mediaController.getPackageName());
410         }
411         for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
412             if (areSessionsMatched(mediaController, sessionInfo)) {
413                 return sessionInfo;
414             }
415         }
416         return null;
417     }
418 
419     /**
420      * Gets routing sessions of an application with the given package name.
421      * The first element of the returned list is the system routing session.
422      *
423      * @param packageName the package name of the application that is routing.
424      * @see #getSystemRoutingSession(String)
425      */
426     @NonNull
getRoutingSessions(@onNull String packageName)427     public List<RoutingSessionInfo> getRoutingSessions(@NonNull String packageName) {
428         Objects.requireNonNull(packageName, "packageName must not be null");
429 
430         List<RoutingSessionInfo> sessions = new ArrayList<>();
431         sessions.add(getSystemRoutingSession(packageName));
432 
433         for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
434             if (TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) {
435                 sessions.add(sessionInfo);
436             }
437         }
438         return sessions;
439     }
440 
441     /**
442      * Gets the list of all routing sessions except the system routing session.
443      * <p>
444      * If you want to transfer media of an application, use {@link #getRoutingSessions(String)}.
445      * If you want to get only the system routing session, use
446      * {@link #getSystemRoutingSession(String)}.
447      *
448      * @see #getRoutingSessions(String)
449      * @see #getSystemRoutingSession(String)
450      */
451     @NonNull
getRemoteSessions()452     public List<RoutingSessionInfo> getRemoteSessions() {
453         try {
454             return mMediaRouterService.getRemoteSessions(mClient);
455         } catch (RemoteException ex) {
456             throw ex.rethrowFromSystemServer();
457         }
458     }
459 
460     /**
461      * Gets the list of all discovered routes.
462      */
463     @NonNull
getAllRoutes()464     public List<MediaRoute2Info> getAllRoutes() {
465         List<MediaRoute2Info> routes = new ArrayList<>();
466         synchronized (mRoutesLock) {
467             routes.addAll(mRoutes.values());
468         }
469         return routes;
470     }
471 
472     /**
473      * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name
474      * to a {@link MediaRoute2Info media route}.
475      *
476      * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing
477      * session based on the provided package name.
478      */
479     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer( @onNull String packageName, @NonNull MediaRoute2Info route, @NonNull UserHandle userHandle)480     public void transfer(
481             @NonNull String packageName,
482             @NonNull MediaRoute2Info route,
483             @NonNull UserHandle userHandle) {
484         Objects.requireNonNull(packageName, "packageName must not be null");
485         Objects.requireNonNull(route, "route must not be null");
486 
487         List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName);
488         RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
489         transfer(targetSession, route, userHandle, packageName);
490     }
491 
492     /**
493      * Transfers a routing session to a media route.
494      *
495      * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called
496      * depending on the result.
497      *
498      * @param sessionInfo the routing session info to transfer
499      * @param route the route transfer to
500      * @param transferInitiatorUserHandle the user handle of an app initiated the transfer
501      * @param transferInitiatorPackageName the package name of an app initiated the transfer
502      * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo)
503      * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info)
504      */
505     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)506     public void transfer(
507             @NonNull RoutingSessionInfo sessionInfo,
508             @NonNull MediaRoute2Info route,
509             @NonNull UserHandle transferInitiatorUserHandle,
510             @NonNull String transferInitiatorPackageName) {
511         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
512         Objects.requireNonNull(route, "route must not be null");
513         Objects.requireNonNull(transferInitiatorUserHandle);
514         Objects.requireNonNull(transferInitiatorPackageName);
515 
516         Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route);
517 
518         synchronized (mRoutesLock) {
519             if (!mRoutes.containsKey(route.getId())) {
520                 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId());
521                 notifyTransferFailed(sessionInfo, route);
522                 return;
523             }
524         }
525 
526         if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
527             transferToRoute(
528                     sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName);
529         } else {
530             requestCreateSession(sessionInfo, route);
531         }
532     }
533 
534     /**
535      * Requests a volume change for a route asynchronously.
536      * <p>
537      * It may have no effect if the route is currently not selected.
538      * </p>
539      *
540      * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}
541      *               (inclusive).
542      */
setRouteVolume(@onNull MediaRoute2Info route, int volume)543     public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
544         Objects.requireNonNull(route, "route must not be null");
545 
546         if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
547             Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring.");
548             return;
549         }
550         if (volume < 0 || volume > route.getVolumeMax()) {
551             Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring");
552             return;
553         }
554 
555         try {
556             int requestId = mNextRequestId.getAndIncrement();
557             mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
558         } catch (RemoteException ex) {
559             throw ex.rethrowFromSystemServer();
560         }
561     }
562 
563     /**
564      * Requests a volume change for a routing session asynchronously.
565      *
566      * @param volume The new volume value between 0 and {@link RoutingSessionInfo#getVolumeMax}
567      *               (inclusive).
568      */
setSessionVolume(@onNull RoutingSessionInfo sessionInfo, int volume)569     public void setSessionVolume(@NonNull RoutingSessionInfo sessionInfo, int volume) {
570         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
571 
572         if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
573             Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring.");
574             return;
575         }
576         if (volume < 0 || volume > sessionInfo.getVolumeMax()) {
577             Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring");
578             return;
579         }
580 
581         try {
582             int requestId = mNextRequestId.getAndIncrement();
583             mMediaRouterService.setSessionVolumeWithManager(
584                     mClient, requestId, sessionInfo.getId(), volume);
585         } catch (RemoteException ex) {
586             throw ex.rethrowFromSystemServer();
587         }
588     }
589 
updateRoutesOnHandler(@onNull List<MediaRoute2Info> routes)590     void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) {
591         synchronized (mRoutesLock) {
592             mRoutes.clear();
593             for (MediaRoute2Info route : routes) {
594                 mRoutes.put(route.getId(), route);
595             }
596         }
597 
598         notifyRoutesUpdated();
599     }
600 
createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo)601     void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) {
602         TransferRequest matchingRequest = null;
603         for (TransferRequest request : mTransferRequests) {
604             if (request.mRequestId == requestId) {
605                 matchingRequest = request;
606                 break;
607             }
608         }
609 
610         if (matchingRequest == null) {
611             return;
612         }
613 
614         mTransferRequests.remove(matchingRequest);
615 
616         MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute;
617 
618         if (sessionInfo == null) {
619             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
620             return;
621         } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) {
622             Log.w(TAG, "The session does not contain the requested route. "
623                     + "(requestedRouteId=" + requestedRoute.getId()
624                     + ", actualRoutes=" + sessionInfo.getSelectedRoutes()
625                     + ")");
626             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
627             return;
628         } else if (!TextUtils.equals(requestedRoute.getProviderId(),
629                 sessionInfo.getProviderId())) {
630             Log.w(TAG, "The session's provider ID does not match the requested route's. "
631                     + "(requested route's providerId=" + requestedRoute.getProviderId()
632                     + ", actual providerId=" + sessionInfo.getProviderId()
633                     + ")");
634             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
635             return;
636         }
637         notifyTransferred(matchingRequest.mOldSessionInfo, sessionInfo);
638     }
639 
handleFailureOnHandler(int requestId, int reason)640     void handleFailureOnHandler(int requestId, int reason) {
641         TransferRequest matchingRequest = null;
642         for (TransferRequest request : mTransferRequests) {
643             if (request.mRequestId == requestId) {
644                 matchingRequest = request;
645                 break;
646             }
647         }
648 
649         if (matchingRequest != null) {
650             mTransferRequests.remove(matchingRequest);
651             notifyTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute);
652             return;
653         }
654         notifyRequestFailed(reason);
655     }
656 
handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo)657     void handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo) {
658         for (TransferRequest request : mTransferRequests) {
659             String sessionId = request.mOldSessionInfo.getId();
660             if (!TextUtils.equals(sessionId, sessionInfo.getId())) {
661                 continue;
662             }
663             if (sessionInfo.getSelectedRoutes().contains(request.mTargetRoute.getId())) {
664                 mTransferRequests.remove(request);
665                 notifyTransferred(request.mOldSessionInfo, sessionInfo);
666                 break;
667             }
668         }
669         notifySessionUpdated(sessionInfo);
670     }
671 
notifyRoutesUpdated()672     private void notifyRoutesUpdated() {
673         for (CallbackRecord record: mCallbackRecords) {
674             record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated());
675         }
676     }
677 
notifySessionUpdated(RoutingSessionInfo sessionInfo)678     void notifySessionUpdated(RoutingSessionInfo sessionInfo) {
679         for (CallbackRecord record : mCallbackRecords) {
680             record.mExecutor.execute(() -> record.mCallback.onSessionUpdated(sessionInfo));
681         }
682     }
683 
notifySessionReleased(RoutingSessionInfo session)684     void notifySessionReleased(RoutingSessionInfo session) {
685         for (CallbackRecord record : mCallbackRecords) {
686             record.mExecutor.execute(() -> record.mCallback.onSessionReleased(session));
687         }
688     }
689 
notifyRequestFailed(int reason)690     void notifyRequestFailed(int reason) {
691         for (CallbackRecord record : mCallbackRecords) {
692             record.mExecutor.execute(() -> record.mCallback.onRequestFailed(reason));
693         }
694     }
695 
notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)696     void notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) {
697         for (CallbackRecord record : mCallbackRecords) {
698             record.mExecutor.execute(() -> record.mCallback.onTransferred(oldSession, newSession));
699         }
700     }
701 
notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route)702     void notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route) {
703         for (CallbackRecord record : mCallbackRecords) {
704             record.mExecutor.execute(() -> record.mCallback.onTransferFailed(sessionInfo, route));
705         }
706     }
707 
updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference)708     void updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference) {
709         if (preference == null) {
710             mDiscoveryPreferenceMap.remove(packageName);
711             return;
712         }
713         RouteDiscoveryPreference prevPreference =
714                 mDiscoveryPreferenceMap.put(packageName, preference);
715         if (Objects.equals(preference, prevPreference)) {
716             return;
717         }
718         for (CallbackRecord record : mCallbackRecords) {
719             record.mExecutor.execute(() -> record.mCallback
720                     .onDiscoveryPreferenceChanged(packageName, preference));
721         }
722     }
723 
updateRouteListingPreference( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)724     private void updateRouteListingPreference(
725             @NonNull String packageName, @Nullable RouteListingPreference routeListingPreference) {
726         RouteListingPreference oldRouteListingPreference =
727                 routeListingPreference == null
728                         ? mPackageToRouteListingPreferenceMap.remove(packageName)
729                         : mPackageToRouteListingPreferenceMap.put(
730                                 packageName, routeListingPreference);
731         if (Objects.equals(oldRouteListingPreference, routeListingPreference)) {
732             return;
733         }
734         for (CallbackRecord record : mCallbackRecords) {
735             record.mExecutor.execute(
736                     () ->
737                             record.mCallback.onRouteListingPreferenceUpdated(
738                                     packageName, routeListingPreference));
739         }
740     }
741 
742     /**
743      * Gets the unmodifiable list of selected routes for the session.
744      */
745     @NonNull
getSelectedRoutes(@onNull RoutingSessionInfo sessionInfo)746     public List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo sessionInfo) {
747         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
748 
749         synchronized (mRoutesLock) {
750             return sessionInfo.getSelectedRoutes().stream().map(mRoutes::get)
751                     .filter(Objects::nonNull)
752                     .collect(Collectors.toList());
753         }
754     }
755 
756     /**
757      * Gets the unmodifiable list of selectable routes for the session.
758      */
759     @NonNull
getSelectableRoutes(@onNull RoutingSessionInfo sessionInfo)760     public List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
761         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
762 
763         List<String> selectedRouteIds = sessionInfo.getSelectedRoutes();
764 
765         synchronized (mRoutesLock) {
766             return sessionInfo.getSelectableRoutes().stream()
767                     .filter(routeId -> !selectedRouteIds.contains(routeId))
768                     .map(mRoutes::get)
769                     .filter(Objects::nonNull)
770                     .collect(Collectors.toList());
771         }
772     }
773 
774     /**
775      * Gets the unmodifiable list of deselectable routes for the session.
776      */
777     @NonNull
getDeselectableRoutes(@onNull RoutingSessionInfo sessionInfo)778     public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
779         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
780 
781         List<String> selectedRouteIds = sessionInfo.getSelectedRoutes();
782 
783         synchronized (mRoutesLock) {
784             return sessionInfo.getDeselectableRoutes().stream()
785                     .filter(routeId -> selectedRouteIds.contains(routeId))
786                     .map(mRoutes::get)
787                     .filter(Objects::nonNull)
788                     .collect(Collectors.toList());
789         }
790     }
791 
792     /**
793      * Selects a route for the remote session. After a route is selected, the media is expected
794      * to be played to the all the selected routes. This is different from {@link
795      * #transfer(RoutingSessionInfo, MediaRoute2Info)} transferring to a route},
796      * where the media is expected to 'move' from one route to another.
797      * <p>
798      * The given route must satisfy all of the following conditions:
799      * <ul>
800      * <li>it should not be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li>
801      * <li>it should be included in {@link #getSelectableRoutes(RoutingSessionInfo)}</li>
802      * </ul>
803      * If the route doesn't meet any of above conditions, it will be ignored.
804      *
805      * @see #getSelectedRoutes(RoutingSessionInfo)
806      * @see #getSelectableRoutes(RoutingSessionInfo)
807      * @see Callback#onSessionUpdated(RoutingSessionInfo)
808      */
selectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)809     public void selectRoute(@NonNull RoutingSessionInfo sessionInfo,
810             @NonNull MediaRoute2Info route) {
811         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
812         Objects.requireNonNull(route, "route must not be null");
813 
814         if (sessionInfo.getSelectedRoutes().contains(route.getId())) {
815             Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
816             return;
817         }
818 
819         if (!sessionInfo.getSelectableRoutes().contains(route.getId())) {
820             Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
821             return;
822         }
823 
824         try {
825             int requestId = mNextRequestId.getAndIncrement();
826             mMediaRouterService.selectRouteWithManager(
827                     mClient, requestId, sessionInfo.getId(), route);
828         } catch (RemoteException ex) {
829             throw ex.rethrowFromSystemServer();
830         }
831     }
832 
833     /**
834      * Deselects a route from the remote session. After a route is deselected, the media is
835      * expected to be stopped on the deselected routes.
836      * <p>
837      * The given route must satisfy all of the following conditions:
838      * <ul>
839      * <li>it should be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li>
840      * <li>it should be included in {@link #getDeselectableRoutes(RoutingSessionInfo)}</li>
841      * </ul>
842      * If the route doesn't meet any of above conditions, it will be ignored.
843      *
844      * @see #getSelectedRoutes(RoutingSessionInfo)
845      * @see #getDeselectableRoutes(RoutingSessionInfo)
846      * @see Callback#onSessionUpdated(RoutingSessionInfo)
847      */
deselectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)848     public void deselectRoute(@NonNull RoutingSessionInfo sessionInfo,
849             @NonNull MediaRoute2Info route) {
850         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
851         Objects.requireNonNull(route, "route must not be null");
852 
853         if (!sessionInfo.getSelectedRoutes().contains(route.getId())) {
854             Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
855             return;
856         }
857 
858         if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) {
859             Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
860             return;
861         }
862 
863         try {
864             int requestId = mNextRequestId.getAndIncrement();
865             mMediaRouterService.deselectRouteWithManager(
866                     mClient, requestId, sessionInfo.getId(), route);
867         } catch (RemoteException ex) {
868             throw ex.rethrowFromSystemServer();
869         }
870     }
871 
872     /**
873      * Requests releasing a session.
874      * <p>
875      * If a session is released, any operation on the session will be ignored.
876      * {@link Callback#onSessionReleased(RoutingSessionInfo)} will be called
877      * when the session is released.
878      * </p>
879      *
880      * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo)
881      */
releaseSession(@onNull RoutingSessionInfo sessionInfo)882     public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
883         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
884 
885         try {
886             int requestId = mNextRequestId.getAndIncrement();
887             mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId());
888         } catch (RemoteException ex) {
889             throw ex.rethrowFromSystemServer();
890         }
891     }
892 
893     /**
894      * Transfers the remote session to the given route.
895      *
896      * @hide
897      */
898     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)899     private void transferToRoute(
900             @NonNull RoutingSessionInfo session,
901             @NonNull MediaRoute2Info route,
902             @NonNull UserHandle transferInitiatorUserHandle,
903             @NonNull String transferInitiatorPackageName) {
904         int requestId = createTransferRequest(session, route);
905 
906         try {
907             mMediaRouterService.transferToRouteWithManager(
908                     mClient,
909                     requestId,
910                     session.getId(),
911                     route,
912                     transferInitiatorUserHandle,
913                     transferInitiatorPackageName);
914         } catch (RemoteException ex) {
915             throw ex.rethrowFromSystemServer();
916         }
917     }
918 
requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route)919     private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route) {
920         if (TextUtils.isEmpty(oldSession.getClientPackageName())) {
921             Log.w(TAG, "requestCreateSession: Can't create a session without package name.");
922             notifyTransferFailed(oldSession, route);
923             return;
924         }
925 
926         int requestId = createTransferRequest(oldSession, route);
927 
928         try {
929             mMediaRouterService.requestCreateSessionWithManager(
930                     mClient, requestId, oldSession, route);
931         } catch (RemoteException ex) {
932             throw ex.rethrowFromSystemServer();
933         }
934     }
935 
createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route)936     private int createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route) {
937         int requestId = mNextRequestId.getAndIncrement();
938         TransferRequest transferRequest = new TransferRequest(requestId, session, route);
939         mTransferRequests.add(transferRequest);
940 
941         Message timeoutMessage =
942                 obtainMessage(MediaRouter2Manager::handleTransferTimeout, this, transferRequest);
943         mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS);
944         return requestId;
945     }
946 
handleTransferTimeout(TransferRequest request)947     private void handleTransferTimeout(TransferRequest request) {
948         boolean removed = mTransferRequests.remove(request);
949         if (removed) {
950             notifyTransferFailed(request.mOldSessionInfo, request.mTargetRoute);
951         }
952     }
953 
954 
areSessionsMatched(MediaController mediaController, RoutingSessionInfo sessionInfo)955     private boolean areSessionsMatched(MediaController mediaController,
956             RoutingSessionInfo sessionInfo) {
957         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
958         String volumeControlId = playbackInfo.getVolumeControlId();
959         if (volumeControlId == null) {
960             return false;
961         }
962 
963         if (TextUtils.equals(volumeControlId, sessionInfo.getId())) {
964             return true;
965         }
966         // Workaround for provider not being able to know the unique session ID.
967         return TextUtils.equals(volumeControlId, sessionInfo.getOriginalId())
968                 && TextUtils.equals(mediaController.getPackageName(),
969                 sessionInfo.getOwnerPackageName());
970     }
971 
972     /**
973      * Interface for receiving events about media routing changes.
974      */
975     public interface Callback {
976 
977         /**
978          * Called when the routes list changes. This includes adding, modifying, or removing
979          * individual routes.
980          */
onRoutesUpdated()981         default void onRoutesUpdated() {}
982 
983         /**
984          * Called when a session is changed.
985          * @param session the updated session
986          */
onSessionUpdated(@onNull RoutingSessionInfo session)987         default void onSessionUpdated(@NonNull RoutingSessionInfo session) {}
988 
989         /**
990          * Called when a session is released.
991          * @param session the released session.
992          * @see #releaseSession(RoutingSessionInfo)
993          */
onSessionReleased(@onNull RoutingSessionInfo session)994         default void onSessionReleased(@NonNull RoutingSessionInfo session) {}
995 
996         /**
997          * Called when media is transferred.
998          *
999          * @param oldSession the previous session
1000          * @param newSession the new session
1001          */
onTransferred(@onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)1002         default void onTransferred(@NonNull RoutingSessionInfo oldSession,
1003                 @NonNull RoutingSessionInfo newSession) { }
1004 
1005         /**
1006          * Called when {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} fails.
1007          */
onTransferFailed(@onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)1008         default void onTransferFailed(@NonNull RoutingSessionInfo session,
1009                 @NonNull MediaRoute2Info route) { }
1010 
1011         /**
1012          * Called when the preferred route features of an app is changed.
1013          *
1014          * @param packageName the package name of the application
1015          * @param preferredFeatures the list of preferred route features set by an application.
1016          */
onPreferredFeaturesChanged(@onNull String packageName, @NonNull List<String> preferredFeatures)1017         default void onPreferredFeaturesChanged(@NonNull String packageName,
1018                 @NonNull List<String> preferredFeatures) {}
1019 
1020         /**
1021          * Called when the preferred route features of an app is changed.
1022          *
1023          * @param packageName the package name of the application
1024          * @param discoveryPreference the new discovery preference set by the application.
1025          */
onDiscoveryPreferenceChanged(@onNull String packageName, @NonNull RouteDiscoveryPreference discoveryPreference)1026         default void onDiscoveryPreferenceChanged(@NonNull String packageName,
1027                 @NonNull RouteDiscoveryPreference discoveryPreference) {
1028             onPreferredFeaturesChanged(packageName, discoveryPreference.getPreferredFeatures());
1029         }
1030 
1031         /**
1032          * Called when the app with the given {@code packageName} updates its {@link
1033          * MediaRouter2#setRouteListingPreference route listing preference}.
1034          *
1035          * @param packageName The package name of the app that changed its listing preference.
1036          * @param routeListingPreference The new {@link RouteListingPreference} set by the app with
1037          *     the given {@code packageName}. Maybe null if an app has unset its preference (by
1038          *     passing null to {@link MediaRouter2#setRouteListingPreference}).
1039          */
onRouteListingPreferenceUpdated( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)1040         default void onRouteListingPreferenceUpdated(
1041                 @NonNull String packageName,
1042                 @Nullable RouteListingPreference routeListingPreference) {}
1043 
1044         /**
1045          * Called when a previous request has failed.
1046          *
1047          * @param reason the reason that the request has failed. Can be one of followings:
1048          *               {@link MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
1049          *               {@link MediaRoute2ProviderService#REASON_REJECTED},
1050          *               {@link MediaRoute2ProviderService#REASON_NETWORK_ERROR},
1051          *               {@link MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
1052          *               {@link MediaRoute2ProviderService#REASON_INVALID_COMMAND},
1053          */
onRequestFailed(int reason)1054         default void onRequestFailed(int reason) {}
1055     }
1056 
1057     final class CallbackRecord {
1058         public final Executor mExecutor;
1059         public final Callback mCallback;
1060 
CallbackRecord(Executor executor, Callback callback)1061         CallbackRecord(Executor executor, Callback callback) {
1062             mExecutor = executor;
1063             mCallback = callback;
1064         }
1065 
1066         @Override
equals(Object obj)1067         public boolean equals(Object obj) {
1068             if (this == obj) {
1069                 return true;
1070             }
1071             if (!(obj instanceof CallbackRecord)) {
1072                 return false;
1073             }
1074             return mCallback == ((CallbackRecord) obj).mCallback;
1075         }
1076 
1077         @Override
hashCode()1078         public int hashCode() {
1079             return mCallback.hashCode();
1080         }
1081     }
1082 
1083     static final class TransferRequest {
1084         public final int mRequestId;
1085         public final RoutingSessionInfo mOldSessionInfo;
1086         public final MediaRoute2Info mTargetRoute;
1087 
TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, @NonNull MediaRoute2Info targetRoute)1088         TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo,
1089                 @NonNull MediaRoute2Info targetRoute) {
1090             mRequestId = requestId;
1091             mOldSessionInfo = oldSessionInfo;
1092             mTargetRoute = targetRoute;
1093         }
1094     }
1095 
1096     class Client extends IMediaRouter2Manager.Stub {
1097         @Override
notifySessionCreated(int requestId, RoutingSessionInfo session)1098         public void notifySessionCreated(int requestId, RoutingSessionInfo session) {
1099             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::createSessionOnHandler,
1100                     MediaRouter2Manager.this, requestId, session));
1101         }
1102 
1103         @Override
notifySessionUpdated(RoutingSessionInfo session)1104         public void notifySessionUpdated(RoutingSessionInfo session) {
1105             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleSessionsUpdatedOnHandler,
1106                     MediaRouter2Manager.this, session));
1107         }
1108 
1109         @Override
notifySessionReleased(RoutingSessionInfo session)1110         public void notifySessionReleased(RoutingSessionInfo session) {
1111             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifySessionReleased,
1112                     MediaRouter2Manager.this, session));
1113         }
1114 
1115         @Override
notifyRequestFailed(int requestId, int reason)1116         public void notifyRequestFailed(int requestId, int reason) {
1117             // Note: requestId is not used.
1118             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleFailureOnHandler,
1119                     MediaRouter2Manager.this, requestId, reason));
1120         }
1121 
1122         @Override
notifyDiscoveryPreferenceChanged(String packageName, RouteDiscoveryPreference discoveryPreference)1123         public void notifyDiscoveryPreferenceChanged(String packageName,
1124                 RouteDiscoveryPreference discoveryPreference) {
1125             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::updateDiscoveryPreference,
1126                     MediaRouter2Manager.this, packageName, discoveryPreference));
1127         }
1128 
1129         @Override
notifyRouteListingPreferenceChange( String packageName, @Nullable RouteListingPreference routeListingPreference)1130         public void notifyRouteListingPreferenceChange(
1131                 String packageName, @Nullable RouteListingPreference routeListingPreference) {
1132             mHandler.sendMessage(
1133                     obtainMessage(
1134                             MediaRouter2Manager::updateRouteListingPreference,
1135                             MediaRouter2Manager.this,
1136                             packageName,
1137                             routeListingPreference));
1138         }
1139 
1140         @Override
notifyDeviceSuggestionsUpdated( String packageName, String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo)1141         public void notifyDeviceSuggestionsUpdated(
1142                 String packageName,
1143                 String suggestingPackageName,
1144                 @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
1145             // MediaRouter2Manager doesn't support device suggestions
1146         }
1147 
1148         @Override
notifyRoutesUpdated(List<MediaRoute2Info> routes)1149         public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
1150             mHandler.sendMessage(
1151                     obtainMessage(
1152                             MediaRouter2Manager::updateRoutesOnHandler,
1153                             MediaRouter2Manager.this,
1154                             routes));
1155         }
1156 
1157         @Override
invalidateInstance()1158         public void invalidateInstance() {
1159             // Should never happen since MediaRouter2Manager should only be used with
1160             // MEDIA_CONTENT_CONTROL, which cannot be revoked.
1161         }
1162     }
1163 }
1164