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(Comparator.comparingInt(
306                 r -> -packagePriority.getOrDefault(r.getPackageName(), 0)));
307         return routes;
308     }
309 
getFilteredRoutes(@onNull RoutingSessionInfo sessionInfo, boolean includeSelectedRoutes, @Nullable Predicate<MediaRoute2Info> additionalFilter)310     private List<MediaRoute2Info> getFilteredRoutes(@NonNull RoutingSessionInfo sessionInfo,
311             boolean includeSelectedRoutes,
312             @Nullable Predicate<MediaRoute2Info> additionalFilter) {
313         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
314 
315         List<MediaRoute2Info> routes = new ArrayList<>();
316 
317         Set<String> deduplicationIdSet = new ArraySet<>();
318         String packageName = sessionInfo.getClientPackageName();
319         RouteDiscoveryPreference discoveryPreference =
320                 mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY);
321 
322         for (MediaRoute2Info route : getSortedRoutes(discoveryPreference)) {
323             if (!route.isVisibleTo(packageName)) {
324                 continue;
325             }
326             boolean transferableRoutesContainRoute =
327                     sessionInfo.getTransferableRoutes().contains(route.getId());
328             boolean selectedRoutesContainRoute =
329                     sessionInfo.getSelectedRoutes().contains(route.getId());
330             if (transferableRoutesContainRoute
331                     || (includeSelectedRoutes && selectedRoutesContainRoute)) {
332                 routes.add(route);
333                 continue;
334             }
335             if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) {
336                 continue;
337             }
338             if (!discoveryPreference.getAllowedPackages().isEmpty()
339                     && (route.getPackageName() == null
340                     || !discoveryPreference.getAllowedPackages()
341                     .contains(route.getPackageName()))) {
342                 continue;
343             }
344             if (additionalFilter != null && !additionalFilter.test(route)) {
345                 continue;
346             }
347             if (discoveryPreference.shouldRemoveDuplicates()) {
348                 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) {
349                     continue;
350                 }
351                 deduplicationIdSet.addAll(route.getDeduplicationIds());
352             }
353             routes.add(route);
354         }
355         return routes;
356     }
357 
358     /**
359      * Returns the preferred features of the specified package name.
360      */
361     @NonNull
getDiscoveryPreference(@onNull String packageName)362     public RouteDiscoveryPreference getDiscoveryPreference(@NonNull String packageName) {
363         Objects.requireNonNull(packageName, "packageName must not be null");
364 
365         return mDiscoveryPreferenceMap.getOrDefault(packageName, RouteDiscoveryPreference.EMPTY);
366     }
367 
368     /**
369      * Returns the {@link RouteListingPreference} of the app with the given {@code packageName}, or
370      * null if the app has not set any.
371      */
372     @Nullable
getRouteListingPreference(@onNull String packageName)373     public RouteListingPreference getRouteListingPreference(@NonNull String packageName) {
374         Preconditions.checkArgument(!TextUtils.isEmpty(packageName));
375         return mPackageToRouteListingPreferenceMap.get(packageName);
376     }
377 
378     /**
379      * Gets the system routing session for the given {@code targetPackageName}. Apps can select a
380      * route that is not the global route. (e.g. an app can select the device route while BT route
381      * is available.)
382      *
383      * @param targetPackageName the package name of the application.
384      */
385     @Nullable
getSystemRoutingSession(@ullable String targetPackageName)386     public RoutingSessionInfo getSystemRoutingSession(@Nullable String targetPackageName) {
387         try {
388             return mMediaRouterService.getSystemSessionInfoForPackage(
389                     mContext.getPackageName(), targetPackageName);
390         } catch (RemoteException ex) {
391             throw ex.rethrowFromSystemServer();
392         }
393     }
394 
395     /**
396      * Gets the routing session of a media session.
397      * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_LOCAL local playback},
398      * the system routing session is returned.
399      * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_REMOTE remote playback},
400      * it returns the corresponding routing session or {@code null} if it's unavailable.
401      */
402     @Nullable
getRoutingSessionForMediaController(MediaController mediaController)403     public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) {
404         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
405         if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
406             return getSystemRoutingSession(mediaController.getPackageName());
407         }
408         for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
409             if (areSessionsMatched(mediaController, sessionInfo)) {
410                 return sessionInfo;
411             }
412         }
413         return null;
414     }
415 
416     /**
417      * Gets routing sessions of an application with the given package name.
418      * The first element of the returned list is the system routing session.
419      *
420      * @param packageName the package name of the application that is routing.
421      * @see #getSystemRoutingSession(String)
422      */
423     @NonNull
getRoutingSessions(@onNull String packageName)424     public List<RoutingSessionInfo> getRoutingSessions(@NonNull String packageName) {
425         Objects.requireNonNull(packageName, "packageName must not be null");
426 
427         List<RoutingSessionInfo> sessions = new ArrayList<>();
428         sessions.add(getSystemRoutingSession(packageName));
429 
430         for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
431             if (TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) {
432                 sessions.add(sessionInfo);
433             }
434         }
435         return sessions;
436     }
437 
438     /**
439      * Gets the list of all routing sessions except the system routing session.
440      * <p>
441      * If you want to transfer media of an application, use {@link #getRoutingSessions(String)}.
442      * If you want to get only the system routing session, use
443      * {@link #getSystemRoutingSession(String)}.
444      *
445      * @see #getRoutingSessions(String)
446      * @see #getSystemRoutingSession(String)
447      */
448     @NonNull
getRemoteSessions()449     public List<RoutingSessionInfo> getRemoteSessions() {
450         try {
451             return mMediaRouterService.getRemoteSessions(mClient);
452         } catch (RemoteException ex) {
453             throw ex.rethrowFromSystemServer();
454         }
455     }
456 
457     /**
458      * Gets the list of all discovered routes.
459      */
460     @NonNull
getAllRoutes()461     public List<MediaRoute2Info> getAllRoutes() {
462         List<MediaRoute2Info> routes = new ArrayList<>();
463         synchronized (mRoutesLock) {
464             routes.addAll(mRoutes.values());
465         }
466         return routes;
467     }
468 
469     /**
470      * Transfers a {@link RoutingSessionInfo routing session} belonging to a specified package name
471      * to a {@link MediaRoute2Info media route}.
472      *
473      * <p>Same as {@link #transfer(RoutingSessionInfo, MediaRoute2Info)}, but resolves the routing
474      * session based on the provided package name.
475      */
476     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer( @onNull String packageName, @NonNull MediaRoute2Info route, @NonNull UserHandle userHandle)477     public void transfer(
478             @NonNull String packageName,
479             @NonNull MediaRoute2Info route,
480             @NonNull UserHandle userHandle) {
481         Objects.requireNonNull(packageName, "packageName must not be null");
482         Objects.requireNonNull(route, "route must not be null");
483 
484         List<RoutingSessionInfo> sessionInfos = getRoutingSessions(packageName);
485         RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
486         transfer(targetSession, route, userHandle, packageName);
487     }
488 
489     /**
490      * Transfers a routing session to a media route.
491      *
492      * <p>{@link Callback#onTransferred} or {@link Callback#onTransferFailed} will be called
493      * depending on the result.
494      *
495      * @param sessionInfo the routing session info to transfer
496      * @param route the route transfer to
497      * @param transferInitiatorUserHandle the user handle of an app initiated the transfer
498      * @param transferInitiatorPackageName the package name of an app initiated the transfer
499      * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo)
500      * @see Callback#onTransferFailed(RoutingSessionInfo, MediaRoute2Info)
501      */
502     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)503     public void transfer(
504             @NonNull RoutingSessionInfo sessionInfo,
505             @NonNull MediaRoute2Info route,
506             @NonNull UserHandle transferInitiatorUserHandle,
507             @NonNull String transferInitiatorPackageName) {
508         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
509         Objects.requireNonNull(route, "route must not be null");
510         Objects.requireNonNull(transferInitiatorUserHandle);
511         Objects.requireNonNull(transferInitiatorPackageName);
512 
513         Log.v(TAG, "Transferring routing session. session= " + sessionInfo + ", route=" + route);
514 
515         synchronized (mRoutesLock) {
516             if (!mRoutes.containsKey(route.getId())) {
517                 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId());
518                 notifyTransferFailed(sessionInfo, route);
519                 return;
520             }
521         }
522 
523         if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
524             transferToRoute(
525                     sessionInfo, route, transferInitiatorUserHandle, transferInitiatorPackageName);
526         } else {
527             requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
528                     transferInitiatorPackageName);
529         }
530     }
531 
532     /**
533      * Requests a volume change for a route asynchronously.
534      * <p>
535      * It may have no effect if the route is currently not selected.
536      * </p>
537      *
538      * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}
539      *               (inclusive).
540      */
setRouteVolume(@onNull MediaRoute2Info route, int volume)541     public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
542         Objects.requireNonNull(route, "route must not be null");
543 
544         if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
545             Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring.");
546             return;
547         }
548         if (volume < 0 || volume > route.getVolumeMax()) {
549             Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring");
550             return;
551         }
552 
553         try {
554             int requestId = mNextRequestId.getAndIncrement();
555             mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
556         } catch (RemoteException ex) {
557             throw ex.rethrowFromSystemServer();
558         }
559     }
560 
561     /**
562      * Requests a volume change for a routing session asynchronously.
563      *
564      * @param volume The new volume value between 0 and {@link RoutingSessionInfo#getVolumeMax}
565      *               (inclusive).
566      */
setSessionVolume(@onNull RoutingSessionInfo sessionInfo, int volume)567     public void setSessionVolume(@NonNull RoutingSessionInfo sessionInfo, int volume) {
568         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
569 
570         if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
571             Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring.");
572             return;
573         }
574         if (volume < 0 || volume > sessionInfo.getVolumeMax()) {
575             Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring");
576             return;
577         }
578 
579         try {
580             int requestId = mNextRequestId.getAndIncrement();
581             mMediaRouterService.setSessionVolumeWithManager(
582                     mClient, requestId, sessionInfo.getId(), volume);
583         } catch (RemoteException ex) {
584             throw ex.rethrowFromSystemServer();
585         }
586     }
587 
updateRoutesOnHandler(@onNull List<MediaRoute2Info> routes)588     void updateRoutesOnHandler(@NonNull List<MediaRoute2Info> routes) {
589         synchronized (mRoutesLock) {
590             mRoutes.clear();
591             for (MediaRoute2Info route : routes) {
592                 mRoutes.put(route.getId(), route);
593             }
594         }
595 
596         notifyRoutesUpdated();
597     }
598 
createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo)599     void createSessionOnHandler(int requestId, RoutingSessionInfo sessionInfo) {
600         TransferRequest matchingRequest = null;
601         for (TransferRequest request : mTransferRequests) {
602             if (request.mRequestId == requestId) {
603                 matchingRequest = request;
604                 break;
605             }
606         }
607 
608         if (matchingRequest == null) {
609             return;
610         }
611 
612         mTransferRequests.remove(matchingRequest);
613 
614         MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute;
615 
616         if (sessionInfo == null) {
617             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
618             return;
619         } else if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) {
620             Log.w(TAG, "The session does not contain the requested route. "
621                     + "(requestedRouteId=" + requestedRoute.getId()
622                     + ", actualRoutes=" + sessionInfo.getSelectedRoutes()
623                     + ")");
624             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
625             return;
626         } else if (!TextUtils.equals(requestedRoute.getProviderId(),
627                 sessionInfo.getProviderId())) {
628             Log.w(TAG, "The session's provider ID does not match the requested route's. "
629                     + "(requested route's providerId=" + requestedRoute.getProviderId()
630                     + ", actual providerId=" + sessionInfo.getProviderId()
631                     + ")");
632             notifyTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
633             return;
634         }
635         notifyTransferred(matchingRequest.mOldSessionInfo, sessionInfo);
636     }
637 
handleFailureOnHandler(int requestId, int reason)638     void handleFailureOnHandler(int requestId, int reason) {
639         TransferRequest matchingRequest = null;
640         for (TransferRequest request : mTransferRequests) {
641             if (request.mRequestId == requestId) {
642                 matchingRequest = request;
643                 break;
644             }
645         }
646 
647         if (matchingRequest != null) {
648             mTransferRequests.remove(matchingRequest);
649             notifyTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute);
650             return;
651         }
652         notifyRequestFailed(reason);
653     }
654 
handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo)655     void handleSessionsUpdatedOnHandler(RoutingSessionInfo sessionInfo) {
656         for (TransferRequest request : mTransferRequests) {
657             String sessionId = request.mOldSessionInfo.getId();
658             if (!TextUtils.equals(sessionId, sessionInfo.getId())) {
659                 continue;
660             }
661             if (sessionInfo.getSelectedRoutes().contains(request.mTargetRoute.getId())) {
662                 mTransferRequests.remove(request);
663                 notifyTransferred(request.mOldSessionInfo, sessionInfo);
664                 break;
665             }
666         }
667         notifySessionUpdated(sessionInfo);
668     }
669 
notifyRoutesUpdated()670     private void notifyRoutesUpdated() {
671         for (CallbackRecord record: mCallbackRecords) {
672             record.mExecutor.execute(() -> record.mCallback.onRoutesUpdated());
673         }
674     }
675 
notifySessionUpdated(RoutingSessionInfo sessionInfo)676     void notifySessionUpdated(RoutingSessionInfo sessionInfo) {
677         for (CallbackRecord record : mCallbackRecords) {
678             record.mExecutor.execute(() -> record.mCallback.onSessionUpdated(sessionInfo));
679         }
680     }
681 
notifySessionReleased(RoutingSessionInfo session)682     void notifySessionReleased(RoutingSessionInfo session) {
683         for (CallbackRecord record : mCallbackRecords) {
684             record.mExecutor.execute(() -> record.mCallback.onSessionReleased(session));
685         }
686     }
687 
notifyRequestFailed(int reason)688     void notifyRequestFailed(int reason) {
689         for (CallbackRecord record : mCallbackRecords) {
690             record.mExecutor.execute(() -> record.mCallback.onRequestFailed(reason));
691         }
692     }
693 
notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)694     void notifyTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) {
695         for (CallbackRecord record : mCallbackRecords) {
696             record.mExecutor.execute(() -> record.mCallback.onTransferred(oldSession, newSession));
697         }
698     }
699 
notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route)700     void notifyTransferFailed(RoutingSessionInfo sessionInfo, MediaRoute2Info route) {
701         for (CallbackRecord record : mCallbackRecords) {
702             record.mExecutor.execute(() -> record.mCallback.onTransferFailed(sessionInfo, route));
703         }
704     }
705 
updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference)706     void updateDiscoveryPreference(String packageName, RouteDiscoveryPreference preference) {
707         if (preference == null) {
708             mDiscoveryPreferenceMap.remove(packageName);
709             return;
710         }
711         RouteDiscoveryPreference prevPreference =
712                 mDiscoveryPreferenceMap.put(packageName, preference);
713         if (Objects.equals(preference, prevPreference)) {
714             return;
715         }
716         for (CallbackRecord record : mCallbackRecords) {
717             record.mExecutor.execute(() -> record.mCallback
718                     .onDiscoveryPreferenceChanged(packageName, preference));
719         }
720     }
721 
updateRouteListingPreference( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)722     private void updateRouteListingPreference(
723             @NonNull String packageName, @Nullable RouteListingPreference routeListingPreference) {
724         RouteListingPreference oldRouteListingPreference =
725                 routeListingPreference == null
726                         ? mPackageToRouteListingPreferenceMap.remove(packageName)
727                         : mPackageToRouteListingPreferenceMap.put(
728                                 packageName, routeListingPreference);
729         if (Objects.equals(oldRouteListingPreference, routeListingPreference)) {
730             return;
731         }
732         for (CallbackRecord record : mCallbackRecords) {
733             record.mExecutor.execute(
734                     () ->
735                             record.mCallback.onRouteListingPreferenceUpdated(
736                                     packageName, routeListingPreference));
737         }
738     }
739 
740     /**
741      * Gets the unmodifiable list of selected routes for the session.
742      */
743     @NonNull
getSelectedRoutes(@onNull RoutingSessionInfo sessionInfo)744     public List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo sessionInfo) {
745         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
746 
747         synchronized (mRoutesLock) {
748             return sessionInfo.getSelectedRoutes().stream().map(mRoutes::get)
749                     .filter(Objects::nonNull)
750                     .collect(Collectors.toList());
751         }
752     }
753 
754     /**
755      * Gets the unmodifiable list of selectable routes for the session.
756      */
757     @NonNull
getSelectableRoutes(@onNull RoutingSessionInfo sessionInfo)758     public List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
759         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
760 
761         List<String> selectedRouteIds = sessionInfo.getSelectedRoutes();
762 
763         synchronized (mRoutesLock) {
764             return sessionInfo.getSelectableRoutes().stream()
765                     .filter(routeId -> !selectedRouteIds.contains(routeId))
766                     .map(mRoutes::get)
767                     .filter(Objects::nonNull)
768                     .collect(Collectors.toList());
769         }
770     }
771 
772     /**
773      * Gets the unmodifiable list of deselectable routes for the session.
774      */
775     @NonNull
getDeselectableRoutes(@onNull RoutingSessionInfo sessionInfo)776     public List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo sessionInfo) {
777         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
778 
779         List<String> selectedRouteIds = sessionInfo.getSelectedRoutes();
780 
781         synchronized (mRoutesLock) {
782             return sessionInfo.getDeselectableRoutes().stream()
783                     .filter(routeId -> selectedRouteIds.contains(routeId))
784                     .map(mRoutes::get)
785                     .filter(Objects::nonNull)
786                     .collect(Collectors.toList());
787         }
788     }
789 
790     /**
791      * Selects a route for the remote session. After a route is selected, the media is expected
792      * to be played to the all the selected routes. This is different from {@link
793      * #transfer(RoutingSessionInfo, MediaRoute2Info)} transferring to a route},
794      * where the media is expected to 'move' from one route to another.
795      * <p>
796      * The given route must satisfy all of the following conditions:
797      * <ul>
798      * <li>it should not be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li>
799      * <li>it should be included in {@link #getSelectableRoutes(RoutingSessionInfo)}</li>
800      * </ul>
801      * If the route doesn't meet any of above conditions, it will be ignored.
802      *
803      * @see #getSelectedRoutes(RoutingSessionInfo)
804      * @see #getSelectableRoutes(RoutingSessionInfo)
805      * @see Callback#onSessionUpdated(RoutingSessionInfo)
806      */
selectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)807     public void selectRoute(@NonNull RoutingSessionInfo sessionInfo,
808             @NonNull MediaRoute2Info route) {
809         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
810         Objects.requireNonNull(route, "route must not be null");
811 
812         if (sessionInfo.getSelectedRoutes().contains(route.getId())) {
813             Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
814             return;
815         }
816 
817         if (!sessionInfo.getSelectableRoutes().contains(route.getId())) {
818             Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
819             return;
820         }
821 
822         try {
823             int requestId = mNextRequestId.getAndIncrement();
824             mMediaRouterService.selectRouteWithManager(
825                     mClient, requestId, sessionInfo.getId(), route);
826         } catch (RemoteException ex) {
827             throw ex.rethrowFromSystemServer();
828         }
829     }
830 
831     /**
832      * Deselects a route from the remote session. After a route is deselected, the media is
833      * expected to be stopped on the deselected routes.
834      * <p>
835      * The given route must satisfy all of the following conditions:
836      * <ul>
837      * <li>it should be included in {@link #getSelectedRoutes(RoutingSessionInfo)}</li>
838      * <li>it should be included in {@link #getDeselectableRoutes(RoutingSessionInfo)}</li>
839      * </ul>
840      * If the route doesn't meet any of above conditions, it will be ignored.
841      *
842      * @see #getSelectedRoutes(RoutingSessionInfo)
843      * @see #getDeselectableRoutes(RoutingSessionInfo)
844      * @see Callback#onSessionUpdated(RoutingSessionInfo)
845      */
deselectRoute(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)846     public void deselectRoute(@NonNull RoutingSessionInfo sessionInfo,
847             @NonNull MediaRoute2Info route) {
848         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
849         Objects.requireNonNull(route, "route must not be null");
850 
851         if (!sessionInfo.getSelectedRoutes().contains(route.getId())) {
852             Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
853             return;
854         }
855 
856         if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) {
857             Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
858             return;
859         }
860 
861         try {
862             int requestId = mNextRequestId.getAndIncrement();
863             mMediaRouterService.deselectRouteWithManager(
864                     mClient, requestId, sessionInfo.getId(), route);
865         } catch (RemoteException ex) {
866             throw ex.rethrowFromSystemServer();
867         }
868     }
869 
870     /**
871      * Requests releasing a session.
872      * <p>
873      * If a session is released, any operation on the session will be ignored.
874      * {@link Callback#onSessionReleased(RoutingSessionInfo)} will be called
875      * when the session is released.
876      * </p>
877      *
878      * @see Callback#onTransferred(RoutingSessionInfo, RoutingSessionInfo)
879      */
releaseSession(@onNull RoutingSessionInfo sessionInfo)880     public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
881         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
882 
883         try {
884             int requestId = mNextRequestId.getAndIncrement();
885             mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId());
886         } catch (RemoteException ex) {
887             throw ex.rethrowFromSystemServer();
888         }
889     }
890 
891     /**
892      * Transfers the remote session to the given route.
893      *
894      * @hide
895      */
896     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)897     private void transferToRoute(
898             @NonNull RoutingSessionInfo session,
899             @NonNull MediaRoute2Info route,
900             @NonNull UserHandle transferInitiatorUserHandle,
901             @NonNull String transferInitiatorPackageName) {
902         int requestId = createTransferRequest(session, route);
903 
904         try {
905             mMediaRouterService.transferToRouteWithManager(
906                     mClient,
907                     requestId,
908                     session.getId(),
909                     route,
910                     transferInitiatorUserHandle,
911                     transferInitiatorPackageName);
912         } catch (RemoteException ex) {
913             throw ex.rethrowFromSystemServer();
914         }
915     }
916 
requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiationPackageName)917     private void requestCreateSession(RoutingSessionInfo oldSession, MediaRoute2Info route,
918             @NonNull UserHandle transferInitiatorUserHandle,
919             @NonNull String transferInitiationPackageName) {
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, transferInitiatorUserHandle,
931                     transferInitiationPackageName);
932         } catch (RemoteException ex) {
933             throw ex.rethrowFromSystemServer();
934         }
935     }
936 
createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route)937     private int createTransferRequest(RoutingSessionInfo session, MediaRoute2Info route) {
938         int requestId = mNextRequestId.getAndIncrement();
939         TransferRequest transferRequest = new TransferRequest(requestId, session, route);
940         mTransferRequests.add(transferRequest);
941 
942         Message timeoutMessage =
943                 obtainMessage(MediaRouter2Manager::handleTransferTimeout, this, transferRequest);
944         mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS);
945         return requestId;
946     }
947 
handleTransferTimeout(TransferRequest request)948     private void handleTransferTimeout(TransferRequest request) {
949         boolean removed = mTransferRequests.remove(request);
950         if (removed) {
951             notifyTransferFailed(request.mOldSessionInfo, request.mTargetRoute);
952         }
953     }
954 
955 
areSessionsMatched(MediaController mediaController, RoutingSessionInfo sessionInfo)956     private boolean areSessionsMatched(MediaController mediaController,
957             RoutingSessionInfo sessionInfo) {
958         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
959         String volumeControlId = playbackInfo.getVolumeControlId();
960         if (volumeControlId == null) {
961             return false;
962         }
963 
964         if (TextUtils.equals(volumeControlId, sessionInfo.getId())) {
965             return true;
966         }
967         // Workaround for provider not being able to know the unique session ID.
968         return TextUtils.equals(volumeControlId, sessionInfo.getOriginalId())
969                 && TextUtils.equals(mediaController.getPackageName(),
970                 sessionInfo.getOwnerPackageName());
971     }
972 
973     /**
974      * Interface for receiving events about media routing changes.
975      */
976     public interface Callback {
977 
978         /**
979          * Called when the routes list changes. This includes adding, modifying, or removing
980          * individual routes.
981          */
onRoutesUpdated()982         default void onRoutesUpdated() {}
983 
984         /**
985          * Called when a session is changed.
986          * @param session the updated session
987          */
onSessionUpdated(@onNull RoutingSessionInfo session)988         default void onSessionUpdated(@NonNull RoutingSessionInfo session) {}
989 
990         /**
991          * Called when a session is released.
992          * @param session the released session.
993          * @see #releaseSession(RoutingSessionInfo)
994          */
onSessionReleased(@onNull RoutingSessionInfo session)995         default void onSessionReleased(@NonNull RoutingSessionInfo session) {}
996 
997         /**
998          * Called when media is transferred.
999          *
1000          * @param oldSession the previous session
1001          * @param newSession the new session
1002          */
onTransferred(@onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)1003         default void onTransferred(@NonNull RoutingSessionInfo oldSession,
1004                 @NonNull RoutingSessionInfo newSession) { }
1005 
1006         /**
1007          * Called when {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} fails.
1008          */
onTransferFailed(@onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)1009         default void onTransferFailed(@NonNull RoutingSessionInfo session,
1010                 @NonNull MediaRoute2Info route) { }
1011 
1012         /**
1013          * Called when the preferred route features of an app is changed.
1014          *
1015          * @param packageName the package name of the application
1016          * @param preferredFeatures the list of preferred route features set by an application.
1017          */
onPreferredFeaturesChanged(@onNull String packageName, @NonNull List<String> preferredFeatures)1018         default void onPreferredFeaturesChanged(@NonNull String packageName,
1019                 @NonNull List<String> preferredFeatures) {}
1020 
1021         /**
1022          * Called when the preferred route features of an app is changed.
1023          *
1024          * @param packageName the package name of the application
1025          * @param discoveryPreference the new discovery preference set by the application.
1026          */
onDiscoveryPreferenceChanged(@onNull String packageName, @NonNull RouteDiscoveryPreference discoveryPreference)1027         default void onDiscoveryPreferenceChanged(@NonNull String packageName,
1028                 @NonNull RouteDiscoveryPreference discoveryPreference) {
1029             onPreferredFeaturesChanged(packageName, discoveryPreference.getPreferredFeatures());
1030         }
1031 
1032         /**
1033          * Called when the app with the given {@code packageName} updates its {@link
1034          * MediaRouter2#setRouteListingPreference route listing preference}.
1035          *
1036          * @param packageName The package name of the app that changed its listing preference.
1037          * @param routeListingPreference The new {@link RouteListingPreference} set by the app with
1038          *     the given {@code packageName}. Maybe null if an app has unset its preference (by
1039          *     passing null to {@link MediaRouter2#setRouteListingPreference}).
1040          */
onRouteListingPreferenceUpdated( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)1041         default void onRouteListingPreferenceUpdated(
1042                 @NonNull String packageName,
1043                 @Nullable RouteListingPreference routeListingPreference) {}
1044 
1045         /**
1046          * Called when a previous request has failed.
1047          *
1048          * @param reason the reason that the request has failed. Can be one of followings:
1049          *               {@link MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
1050          *               {@link MediaRoute2ProviderService#REASON_REJECTED},
1051          *               {@link MediaRoute2ProviderService#REASON_NETWORK_ERROR},
1052          *               {@link MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
1053          *               {@link MediaRoute2ProviderService#REASON_INVALID_COMMAND},
1054          */
onRequestFailed(int reason)1055         default void onRequestFailed(int reason) {}
1056     }
1057 
1058     final class CallbackRecord {
1059         public final Executor mExecutor;
1060         public final Callback mCallback;
1061 
CallbackRecord(Executor executor, Callback callback)1062         CallbackRecord(Executor executor, Callback callback) {
1063             mExecutor = executor;
1064             mCallback = callback;
1065         }
1066 
1067         @Override
equals(Object obj)1068         public boolean equals(Object obj) {
1069             if (this == obj) {
1070                 return true;
1071             }
1072             if (!(obj instanceof CallbackRecord)) {
1073                 return false;
1074             }
1075             return mCallback == ((CallbackRecord) obj).mCallback;
1076         }
1077 
1078         @Override
hashCode()1079         public int hashCode() {
1080             return mCallback.hashCode();
1081         }
1082     }
1083 
1084     static final class TransferRequest {
1085         public final int mRequestId;
1086         public final RoutingSessionInfo mOldSessionInfo;
1087         public final MediaRoute2Info mTargetRoute;
1088 
TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo, @NonNull MediaRoute2Info targetRoute)1089         TransferRequest(int requestId, @NonNull RoutingSessionInfo oldSessionInfo,
1090                 @NonNull MediaRoute2Info targetRoute) {
1091             mRequestId = requestId;
1092             mOldSessionInfo = oldSessionInfo;
1093             mTargetRoute = targetRoute;
1094         }
1095     }
1096 
1097     class Client extends IMediaRouter2Manager.Stub {
1098         @Override
notifySessionCreated(int requestId, RoutingSessionInfo session)1099         public void notifySessionCreated(int requestId, RoutingSessionInfo session) {
1100             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::createSessionOnHandler,
1101                     MediaRouter2Manager.this, requestId, session));
1102         }
1103 
1104         @Override
notifySessionUpdated(RoutingSessionInfo session)1105         public void notifySessionUpdated(RoutingSessionInfo session) {
1106             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleSessionsUpdatedOnHandler,
1107                     MediaRouter2Manager.this, session));
1108         }
1109 
1110         @Override
notifySessionReleased(RoutingSessionInfo session)1111         public void notifySessionReleased(RoutingSessionInfo session) {
1112             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::notifySessionReleased,
1113                     MediaRouter2Manager.this, session));
1114         }
1115 
1116         @Override
notifyRequestFailed(int requestId, int reason)1117         public void notifyRequestFailed(int requestId, int reason) {
1118             // Note: requestId is not used.
1119             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::handleFailureOnHandler,
1120                     MediaRouter2Manager.this, requestId, reason));
1121         }
1122 
1123         @Override
notifyDiscoveryPreferenceChanged(String packageName, RouteDiscoveryPreference discoveryPreference)1124         public void notifyDiscoveryPreferenceChanged(String packageName,
1125                 RouteDiscoveryPreference discoveryPreference) {
1126             mHandler.sendMessage(obtainMessage(MediaRouter2Manager::updateDiscoveryPreference,
1127                     MediaRouter2Manager.this, packageName, discoveryPreference));
1128         }
1129 
1130         @Override
notifyRouteListingPreferenceChange( String packageName, @Nullable RouteListingPreference routeListingPreference)1131         public void notifyRouteListingPreferenceChange(
1132                 String packageName, @Nullable RouteListingPreference routeListingPreference) {
1133             mHandler.sendMessage(
1134                     obtainMessage(
1135                             MediaRouter2Manager::updateRouteListingPreference,
1136                             MediaRouter2Manager.this,
1137                             packageName,
1138                             routeListingPreference));
1139         }
1140 
1141         @Override
notifyRoutesUpdated(List<MediaRoute2Info> routes)1142         public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
1143             mHandler.sendMessage(
1144                     obtainMessage(
1145                             MediaRouter2Manager::updateRoutesOnHandler,
1146                             MediaRouter2Manager.this,
1147                             routes));
1148         }
1149 
1150         @Override
invalidateInstance()1151         public void invalidateInstance() {
1152             // Should never happen since MediaRouter2Manager should only be used with
1153             // MEDIA_CONTENT_CONTROL, which cannot be revoked.
1154         }
1155     }
1156 }
1157