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