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