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