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