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.Manifest;
22 import android.annotation.CallbackExecutor;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.RequiresPermission;
26 import android.annotation.SystemApi;
27 import android.annotation.TestApi;
28 import android.content.Context;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.Looper;
32 import android.os.RemoteException;
33 import android.os.ServiceManager;
34 import android.text.TextUtils;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import com.android.internal.annotations.GuardedBy;
40 
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.Comparator;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Set;
48 import java.util.concurrent.CopyOnWriteArrayList;
49 import java.util.concurrent.Executor;
50 import java.util.concurrent.atomic.AtomicBoolean;
51 import java.util.concurrent.atomic.AtomicInteger;
52 import java.util.stream.Collectors;
53 
54 /**
55  * This API is not generally intended for third party application developers. Use the
56  * <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
57  * <a href="{@docRoot}reference/androidx/mediarouter/media/package-summary.html">Media Router
58  * Library</a> for consistent behavior across all devices.
59  *
60  * <p>MediaRouter2 allows applications to control the routing of media channels and streams from
61  * the current device to remote speakers and devices.
62  */
63 // TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute)
64 //       Not only MediaRouter2, but also to service / manager / provider.
65 // TODO: ensure thread-safe and document it
66 public final class MediaRouter2 {
67     private static final String TAG = "MR2";
68     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
69     private static final Object sSystemRouterLock = new Object();
70     private static final Object sRouterLock = new Object();
71 
72     // The maximum time for the old routing controller available after transfer.
73     private static final int TRANSFER_TIMEOUT_MS = 30_000;
74     // The manager request ID representing that no manager is involved.
75     private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE;
76 
77     @GuardedBy("sSystemRouterLock")
78     private static Map<String, MediaRouter2> sSystemMediaRouter2Map = new ArrayMap<>();
79 
80     private static MediaRouter2Manager sManager;
81 
82     @GuardedBy("sRouterLock")
83     private static MediaRouter2 sInstance;
84 
85     private final Context mContext;
86     private final IMediaRouterService mMediaRouterService;
87     private final Object mLock = new Object();
88 
89     private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords =
90             new CopyOnWriteArrayList<>();
91     private final CopyOnWriteArrayList<TransferCallbackRecord> mTransferCallbackRecords =
92             new CopyOnWriteArrayList<>();
93     private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords =
94             new CopyOnWriteArrayList<>();
95 
96     private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests =
97             new CopyOnWriteArrayList<>();
98 
99     // TODO: Specify the fields that are only used (or not used) by system media router.
100     private final String mClientPackageName;
101     final ManagerCallback mManagerCallback;
102 
103     private final String mPackageName;
104 
105     /**
106      * Stores the latest copy of all routes received from the system server, without any filtering,
107      * sorting, or deduplication.
108      *
109      * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key.
110      */
111     @GuardedBy("mLock")
112     final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>();
113 
114     @GuardedBy("mLock")
115     @Nullable
116     private RouteListingPreference mRouteListingPreference;
117 
118     final RoutingController mSystemController;
119 
120     @GuardedBy("mLock")
121     private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
122 
123     // TODO: Make MediaRouter2 is always connected to the MediaRouterService.
124     @GuardedBy("mLock")
125     MediaRouter2Stub mStub;
126 
127     @GuardedBy("mLock")
128     private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
129 
130     private final AtomicInteger mNextRequestId = new AtomicInteger(1);
131     private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false);
132 
133     final Handler mHandler;
134 
135     /**
136      * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback
137      * dispatch. This is only used to determine what callback a route should be assigned to (added,
138      * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.
139      */
140     private volatile ArrayMap<String, MediaRoute2Info> mPreviousRoutes = new ArrayMap<>();
141 
142     /**
143      * Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be
144      * accessed through {@link #getRoutes()}.
145      *
146      * <p>This list is a copy of {@link #mRoutes} which has undergone filtering, sorting, and
147      * deduplication using criteria in {@link #mDiscoveryPreference}.
148      *
149      * @see #filterRoutesWithCompositePreferenceLocked(List)
150      */
151     private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList();
152     private volatile OnGetControllerHintsListener mOnGetControllerHintsListener;
153 
154     /** Gets an instance of the media router associated with the context. */
155     @NonNull
getInstance(@onNull Context context)156     public static MediaRouter2 getInstance(@NonNull Context context) {
157         Objects.requireNonNull(context, "context must not be null");
158         synchronized (sRouterLock) {
159             if (sInstance == null) {
160                 sInstance = new MediaRouter2(context.getApplicationContext());
161             }
162             return sInstance;
163         }
164     }
165 
166     /**
167      * Gets an instance of the system media router which controls the app's media routing. Returns
168      * {@code null} if the given package name is invalid. There are several things to note when
169      * using the media routers created with this method.
170      *
171      * <p>First of all, the discovery preference passed to {@link #registerRouteCallback} will have
172      * no effect. The callback will be called accordingly with the client app's discovery
173      * preference. Therefore, it is recommended to pass {@link RouteDiscoveryPreference#EMPTY}
174      * there.
175      *
176      * <p>Also, do not keep/compare the instances of the {@link RoutingController}, since they are
177      * always newly created with the latest session information whenever below methods are called:
178      *
179      * <ul>
180      *   <li>{@link #getControllers()}
181      *   <li>{@link #getController(String)}}
182      *   <li>{@link TransferCallback#onTransfer(RoutingController, RoutingController)}
183      *   <li>{@link TransferCallback#onStop(RoutingController)}
184      *   <li>{@link ControllerCallback#onControllerUpdated(RoutingController)}
185      * </ul>
186      *
187      * Therefore, in order to track the current routing status, keep the controller's ID instead,
188      * and use {@link #getController(String)} and {@link #getSystemController()} for getting
189      * controllers.
190      *
191      * <p>Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}.
192      *
193      * @param clientPackageName the package name of the app to control
194      * @throws SecurityException if the caller doesn't have MODIFY_AUDIO_ROUTING permission.
195      * @hide
196      */
197     @SystemApi
198     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
199     @Nullable
getInstance( @onNull Context context, @NonNull String clientPackageName)200     public static MediaRouter2 getInstance(
201             @NonNull Context context, @NonNull String clientPackageName) {
202         Objects.requireNonNull(context, "context must not be null");
203         Objects.requireNonNull(clientPackageName, "clientPackageName must not be null");
204 
205         // Note: Even though this check could be somehow bypassed, the other permission checks
206         // in system server will not allow MediaRouter2Manager to be registered.
207         IMediaRouterService serviceBinder =
208                 IMediaRouterService.Stub.asInterface(
209                         ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
210         try {
211             // verifyPackageExists throws SecurityException if the caller doesn't hold
212             // MEDIA_CONTENT_CONTROL permission.
213             if (!serviceBinder.verifyPackageExists(clientPackageName)) {
214                 Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring.");
215                 return null;
216             }
217         } catch (RemoteException e) {
218             e.rethrowFromSystemServer();
219         }
220 
221         synchronized (sSystemRouterLock) {
222             MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName);
223             if (instance == null) {
224                 if (sManager == null) {
225                     sManager = MediaRouter2Manager.getInstance(context.getApplicationContext());
226                 }
227                 instance = new MediaRouter2(context, clientPackageName);
228                 sSystemMediaRouter2Map.put(clientPackageName, instance);
229                 // Using direct executor here, since MediaRouter2Manager also posts
230                 // to the main handler.
231                 sManager.registerCallback(Runnable::run, instance.mManagerCallback);
232             }
233             return instance;
234         }
235     }
236 
237     /**
238      * Starts scanning remote routes.
239      *
240      * <p>Route discovery can happen even when the {@link #startScan()} is not called. This is
241      * because the scanning could be started before by other apps. Therefore, calling this method
242      * after calling {@link #stopScan()} does not necessarily mean that the routes found before are
243      * removed and added again.
244      *
245      * <p>Use {@link RouteCallback} to get the route related events.
246      *
247      * <p>Note that calling start/stopScan is applied to all system routers in the same process.
248      *
249      * <p>This will be no-op for non-system media routers.
250      *
251      * @see #stopScan()
252      * @see #getInstance(Context, String)
253      * @hide
254      */
255     @SystemApi
256     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
startScan()257     public void startScan() {
258         if (isSystemRouter()) {
259             if (!mIsScanning.getAndSet(true)) {
260                 sManager.registerScanRequest();
261             }
262         }
263     }
264 
265     /**
266      * Stops scanning remote routes to reduce resource consumption.
267      *
268      * <p>Route discovery can be continued even after this method is called. This is because the
269      * scanning is only turned off when all the apps stop scanning. Therefore, calling this method
270      * does not necessarily mean the routes are removed. Also, for the same reason it does not mean
271      * that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards.
272      *
273      * <p>Use {@link RouteCallback} to get the route related events.
274      *
275      * <p>Note that calling start/stopScan is applied to all system routers in the same process.
276      *
277      * <p>This will be no-op for non-system media routers.
278      *
279      * @see #startScan()
280      * @see #getInstance(Context, String)
281      * @hide
282      */
283     @SystemApi
284     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
stopScan()285     public void stopScan() {
286         if (isSystemRouter()) {
287             if (mIsScanning.getAndSet(false)) {
288                 sManager.unregisterScanRequest();
289             }
290         }
291     }
292 
MediaRouter2(Context appContext)293     private MediaRouter2(Context appContext) {
294         mContext = appContext;
295         mMediaRouterService =
296                 IMediaRouterService.Stub.asInterface(
297                         ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
298         mPackageName = mContext.getPackageName();
299         mHandler = new Handler(Looper.getMainLooper());
300 
301         List<MediaRoute2Info> currentSystemRoutes = null;
302         RoutingSessionInfo currentSystemSessionInfo = null;
303         try {
304             currentSystemRoutes = mMediaRouterService.getSystemRoutes();
305             currentSystemSessionInfo = mMediaRouterService.getSystemSessionInfo();
306         } catch (RemoteException ex) {
307             ex.rethrowFromSystemServer();
308         }
309 
310         if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) {
311             throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong.");
312         }
313 
314         if (currentSystemSessionInfo == null) {
315             throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong.");
316         }
317 
318         for (MediaRoute2Info route : currentSystemRoutes) {
319             mRoutes.put(route.getId(), route);
320         }
321         mSystemController = new SystemRoutingController(currentSystemSessionInfo);
322 
323         // Only used by system MediaRouter2.
324         mClientPackageName = null;
325         mManagerCallback = null;
326     }
327 
MediaRouter2(Context context, String clientPackageName)328     private MediaRouter2(Context context, String clientPackageName) {
329         mContext = context;
330         mClientPackageName = clientPackageName;
331         mManagerCallback = new ManagerCallback();
332         mHandler = new Handler(Looper.getMainLooper());
333         mSystemController =
334                 new SystemRoutingController(
335                         ensureClientPackageNameForSystemSession(
336                                 sManager.getSystemRoutingSession(clientPackageName)));
337         mDiscoveryPreference = sManager.getDiscoveryPreference(clientPackageName);
338         updateAllRoutesFromManager();
339 
340         // Only used by non-system MediaRouter2.
341         mMediaRouterService = null;
342         mPackageName = null;
343     }
344 
345     /**
346      * Returns whether any route in {@code routeList} has a same unique ID with given route.
347      *
348      * @hide
349      */
checkRouteListContainsRouteId( @onNull List<MediaRoute2Info> routeList, @NonNull String routeId)350     static boolean checkRouteListContainsRouteId(
351             @NonNull List<MediaRoute2Info> routeList, @NonNull String routeId) {
352         for (MediaRoute2Info info : routeList) {
353             if (TextUtils.equals(routeId, info.getId())) {
354                 return true;
355             }
356         }
357         return false;
358     }
359 
360     /**
361      * Gets the client package name of the app which this media router controls.
362      *
363      * <p>This will return null for non-system media routers.
364      *
365      * @see #getInstance(Context, String)
366      * @hide
367      */
368     @SystemApi
369     @Nullable
getClientPackageName()370     public String getClientPackageName() {
371         return mClientPackageName;
372     }
373 
374     /**
375      * Registers a callback to discover routes and to receive events when they change.
376      *
377      * <p>If the specified callback is already registered, its registration will be updated for the
378      * given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}.
379      */
registerRouteCallback( @onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)380     public void registerRouteCallback(
381             @NonNull @CallbackExecutor Executor executor,
382             @NonNull RouteCallback routeCallback,
383             @NonNull RouteDiscoveryPreference preference) {
384         Objects.requireNonNull(executor, "executor must not be null");
385         Objects.requireNonNull(routeCallback, "callback must not be null");
386         Objects.requireNonNull(preference, "preference must not be null");
387         if (isSystemRouter()) {
388             preference = RouteDiscoveryPreference.EMPTY;
389         }
390 
391         RouteCallbackRecord record = new RouteCallbackRecord(executor, routeCallback, preference);
392 
393         mRouteCallbackRecords.remove(record);
394         // It can fail to add the callback record if another registration with the same callback
395         // is happening but it's okay because either this or the other registration should be done.
396         mRouteCallbackRecords.addIfAbsent(record);
397 
398         if (isSystemRouter()) {
399             return;
400         }
401 
402         synchronized (mLock) {
403             if (mStub == null) {
404                 MediaRouter2Stub stub = new MediaRouter2Stub();
405                 try {
406                     mMediaRouterService.registerRouter2(stub, mPackageName);
407                     mStub = stub;
408                 } catch (RemoteException ex) {
409                     ex.rethrowFromSystemServer();
410                 }
411             }
412             if (mStub != null && updateDiscoveryPreferenceIfNeededLocked()) {
413                 try {
414                     mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference);
415                 } catch (RemoteException ex) {
416                     ex.rethrowFromSystemServer();
417                 }
418             }
419         }
420     }
421 
422     /**
423      * Unregisters the given callback. The callback will no longer receive events. If the callback
424      * has not been added or been removed already, it is ignored.
425      *
426      * @param routeCallback the callback to unregister
427      * @see #registerRouteCallback
428      */
unregisterRouteCallback(@onNull RouteCallback routeCallback)429     public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) {
430         Objects.requireNonNull(routeCallback, "callback must not be null");
431 
432         if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) {
433             Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback");
434             return;
435         }
436 
437         if (isSystemRouter()) {
438             return;
439         }
440 
441         synchronized (mLock) {
442             if (mStub == null) {
443                 return;
444             }
445             if (updateDiscoveryPreferenceIfNeededLocked()) {
446                 try {
447                     mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference);
448                 } catch (RemoteException ex) {
449                     Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
450                 }
451             }
452             if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
453                 try {
454                     mMediaRouterService.unregisterRouter2(mStub);
455                 } catch (RemoteException ex) {
456                     ex.rethrowFromSystemServer();
457                 }
458                 mStub = null;
459             }
460         }
461     }
462 
463     /**
464      * Shows the system output switcher dialog.
465      *
466      * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on
467      * the screen.
468      *
469      * <p>The appearance and precise behaviour of the system output switcher dialog may vary across
470      * different devices, OS versions, and form factors, but the basic functionality stays the same.
471      *
472      * <p>See <a
473      * href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">Output
474      * Switcher documentation</a> for more details.
475      *
476      * @return {@code true} if the output switcher dialog is being shown, or {@code false} if the
477      * call is ignored because the app is in the background.
478      */
showSystemOutputSwitcher()479     public boolean showSystemOutputSwitcher() {
480         synchronized (mLock) {
481             try {
482                 return mMediaRouterService.showMediaOutputSwitcher(mPackageName);
483             } catch (RemoteException ex) {
484                 ex.rethrowFromSystemServer();
485             }
486         }
487         return false;
488     }
489 
490     /**
491      * Sets the {@link RouteListingPreference} of the app associated to this media router.
492      *
493      * <p>Use this method to inform the system UI of the routes that you would like to list for
494      * media routing, via the Output Switcher.
495      *
496      * <p>You should call this method before {@link #registerRouteCallback registering any route
497      * callbacks} and immediately after receiving any {@link RouteCallback#onRoutesUpdated route
498      * updates} in order to keep the system UI in a consistent state. You can also call this method
499      * at any other point to update the listing preference dynamically.
500      *
501      * <p>Notes:
502      *
503      * <ol>
504      *   <li>You should not include the ids of two or more routes with a match in their {@link
505      *       MediaRoute2Info#getDeduplicationIds() deduplication ids}. If you do, the system will
506      *       deduplicate them using its own criteria.
507      *   <li>You can use this method to rank routes in the output switcher, placing the more
508      *       important routes first. The system might override the proposed ranking.
509      *   <li>You can use this method to avoid listing routes using dynamic criteria. For example,
510      *       you can limit access to a specific type of device according to runtime criteria.
511      * </ol>
512      *
513      * @param routeListingPreference The {@link RouteListingPreference} for the system to use for
514      *     route listing. When null, the system uses its default listing criteria.
515      */
setRouteListingPreference(@ullable RouteListingPreference routeListingPreference)516     public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
517         synchronized (mLock) {
518             if (Objects.equals(mRouteListingPreference, routeListingPreference)) {
519                 // Nothing changed. We return early to save a call to the system server.
520                 return;
521             }
522             mRouteListingPreference = routeListingPreference;
523             try {
524                 if (mStub == null) {
525                     MediaRouter2Stub stub = new MediaRouter2Stub();
526                     mMediaRouterService.registerRouter2(stub, mPackageName);
527                     mStub = stub;
528                 }
529                 mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference);
530             } catch (RemoteException ex) {
531                 ex.rethrowFromSystemServer();
532             }
533         }
534     }
535 
536     @GuardedBy("mLock")
updateDiscoveryPreferenceIfNeededLocked()537     private boolean updateDiscoveryPreferenceIfNeededLocked() {
538         RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder(
539                 mRouteCallbackRecords.stream().map(record -> record.mPreference).collect(
540                         Collectors.toList())).build();
541 
542         if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) {
543             return false;
544         }
545         mDiscoveryPreference = newDiscoveryPreference;
546         updateFilteredRoutesLocked();
547         return true;
548     }
549 
550     /**
551      * Gets the list of all discovered routes. This list includes the routes that are not related to
552      * the client app.
553      *
554      * <p>This will return an empty list for non-system media routers.
555      *
556      * @hide
557      */
558     @SystemApi
559     @NonNull
getAllRoutes()560     public List<MediaRoute2Info> getAllRoutes() {
561         if (isSystemRouter()) {
562             return sManager.getAllRoutes();
563         }
564         return Collections.emptyList();
565     }
566 
567     /**
568      * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media
569      * router.
570      *
571      * <p>Please note that the list can be changed before callbacks are invoked.
572      *
573      * @return the list of routes that contains at least one of the route features in discovery
574      *     preferences registered by the application
575      */
576     @NonNull
getRoutes()577     public List<MediaRoute2Info> getRoutes() {
578         synchronized (mLock) {
579             return mFilteredRoutes;
580         }
581     }
582 
583     /**
584      * Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}.
585      * If you register the same callback twice or more, it will be ignored.
586      *
587      * @param executor the executor to execute the callback on
588      * @param callback the callback to register
589      * @see #unregisterTransferCallback
590      */
registerTransferCallback( @onNull @allbackExecutor Executor executor, @NonNull TransferCallback callback)591     public void registerTransferCallback(
592             @NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) {
593         Objects.requireNonNull(executor, "executor must not be null");
594         Objects.requireNonNull(callback, "callback must not be null");
595 
596         TransferCallbackRecord record = new TransferCallbackRecord(executor, callback);
597         if (!mTransferCallbackRecords.addIfAbsent(record)) {
598             Log.w(TAG, "registerTransferCallback: Ignoring the same callback");
599             return;
600         }
601     }
602 
603     /**
604      * Unregisters the given callback. The callback will no longer receive events.
605      * If the callback has not been added or been removed already, it is ignored.
606      *
607      * @param callback the callback to unregister
608      * @see #registerTransferCallback
609      */
unregisterTransferCallback(@onNull TransferCallback callback)610     public void unregisterTransferCallback(@NonNull TransferCallback callback) {
611         Objects.requireNonNull(callback, "callback must not be null");
612 
613         if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) {
614             Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback");
615             return;
616         }
617     }
618 
619     /**
620      * Registers a {@link ControllerCallback}. If you register the same callback twice or more, it
621      * will be ignored.
622      *
623      * @see #unregisterControllerCallback(ControllerCallback)
624      */
registerControllerCallback( @onNull @allbackExecutor Executor executor, @NonNull ControllerCallback callback)625     public void registerControllerCallback(
626             @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) {
627         Objects.requireNonNull(executor, "executor must not be null");
628         Objects.requireNonNull(callback, "callback must not be null");
629 
630         ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback);
631         if (!mControllerCallbackRecords.addIfAbsent(record)) {
632             Log.w(TAG, "registerControllerCallback: Ignoring the same callback");
633             return;
634         }
635     }
636 
637     /**
638      * Unregisters a {@link ControllerCallback}. The callback will no longer receive events.
639      * If the callback has not been added or been removed already, it is ignored.
640      *
641      * @see #registerControllerCallback(Executor, ControllerCallback)
642      */
unregisterControllerCallback(@onNull ControllerCallback callback)643     public void unregisterControllerCallback(@NonNull ControllerCallback callback) {
644         Objects.requireNonNull(callback, "callback must not be null");
645 
646         if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) {
647             Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback");
648             return;
649         }
650     }
651 
652     /**
653      * Sets an {@link OnGetControllerHintsListener} to send hints when creating a
654      * {@link RoutingController}. To send the hints, listener should be set <em>BEFORE</em> calling
655      * {@link #transferTo(MediaRoute2Info)}.
656      *
657      * @param listener A listener to send optional app-specific hints when creating a controller.
658      *     {@code null} for unset.
659      */
setOnGetControllerHintsListener(@ullable OnGetControllerHintsListener listener)660     public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) {
661         if (isSystemRouter()) {
662             return;
663         }
664         mOnGetControllerHintsListener = listener;
665     }
666 
667     /**
668      * Transfers the current media to the given route. If it's necessary a new
669      * {@link RoutingController} is created or it is handled within the current routing controller.
670      *
671      * @param route the route you want to transfer the current media to. Pass {@code null} to
672      *              stop routing of the current media.
673      * @see TransferCallback#onTransfer
674      * @see TransferCallback#onTransferFailure
675      */
transferTo(@onNull MediaRoute2Info route)676     public void transferTo(@NonNull MediaRoute2Info route) {
677         if (isSystemRouter()) {
678             sManager.transfer(mClientPackageName, route);
679             return;
680         }
681 
682         Log.v(TAG, "Transferring to route: " + route);
683 
684         boolean routeFound;
685         synchronized (mLock) {
686             // TODO: Check thread-safety
687             routeFound = mRoutes.containsKey(route.getId());
688         }
689         if (!routeFound) {
690             notifyTransferFailure(route);
691             return;
692         }
693 
694         RoutingController controller = getCurrentController();
695         if (controller.getRoutingSessionInfo().getTransferableRoutes().contains(route.getId())) {
696             controller.transferToRoute(route);
697             return;
698         }
699 
700         requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE);
701     }
702 
703     /**
704      * Stops the current media routing. If the {@link #getSystemController() system controller}
705      * controls the media routing, this method is a no-op.
706      */
stop()707     public void stop() {
708         if (isSystemRouter()) {
709             List<RoutingSessionInfo> sessionInfos = sManager.getRoutingSessions(mClientPackageName);
710             RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1);
711             sManager.releaseSession(sessionToRelease);
712             return;
713         }
714         getCurrentController().release();
715     }
716 
717     /**
718      * Transfers the media of a routing controller to the given route.
719      *
720      * <p>This will be no-op for non-system media routers.
721      *
722      * @param controller a routing controller controlling media routing.
723      * @param route the route you want to transfer the media to.
724      * @hide
725      */
726     @SystemApi
727     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer(@onNull RoutingController controller, @NonNull MediaRoute2Info route)728     public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
729         if (isSystemRouter()) {
730             sManager.transfer(controller.getRoutingSessionInfo(), route);
731             return;
732         }
733     }
734 
requestCreateController( @onNull RoutingController controller, @NonNull MediaRoute2Info route, long managerRequestId)735     void requestCreateController(
736             @NonNull RoutingController controller,
737             @NonNull MediaRoute2Info route,
738             long managerRequestId) {
739 
740         final int requestId = mNextRequestId.getAndIncrement();
741 
742         ControllerCreationRequest request =
743                 new ControllerCreationRequest(requestId, managerRequestId, route, controller);
744         mControllerCreationRequests.add(request);
745 
746         OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
747         Bundle controllerHints = null;
748         if (listener != null) {
749             controllerHints = listener.onGetControllerHints(route);
750             if (controllerHints != null) {
751                 controllerHints = new Bundle(controllerHints);
752             }
753         }
754 
755         MediaRouter2Stub stub;
756         synchronized (mLock) {
757             stub = mStub;
758         }
759         if (stub != null) {
760             try {
761                 mMediaRouterService.requestCreateSessionWithRouter2(
762                         stub,
763                         requestId,
764                         managerRequestId,
765                         controller.getRoutingSessionInfo(),
766                         route,
767                         controllerHints);
768             } catch (RemoteException ex) {
769                 Log.e(TAG, "createControllerForTransfer: "
770                                 + "Failed to request for creating a controller.", ex);
771                 mControllerCreationRequests.remove(request);
772                 if (managerRequestId == MANAGER_REQUEST_ID_NONE) {
773                     notifyTransferFailure(route);
774                 }
775             }
776         }
777     }
778 
779     @NonNull
getCurrentController()780     private RoutingController getCurrentController() {
781         List<RoutingController> controllers = getControllers();
782         return controllers.get(controllers.size() - 1);
783     }
784 
785     /**
786      * Gets a {@link RoutingController} which can control the routes provided by system.
787      * e.g. Phone speaker, wired headset, Bluetooth, etc.
788      *
789      * <p>Note: The system controller can't be released. Calling {@link RoutingController#release()}
790      * will be ignored.
791      *
792      * <p>This method always returns the same instance.
793      */
794     @NonNull
getSystemController()795     public RoutingController getSystemController() {
796         return mSystemController;
797     }
798 
799     /**
800      * Gets a {@link RoutingController} whose ID is equal to the given ID.
801      * Returns {@code null} if there is no matching controller.
802      */
803     @Nullable
getController(@onNull String id)804     public RoutingController getController(@NonNull String id) {
805         Objects.requireNonNull(id, "id must not be null");
806         for (RoutingController controller : getControllers()) {
807             if (TextUtils.equals(id, controller.getId())) {
808                 return controller;
809             }
810         }
811         return null;
812     }
813 
814     /**
815      * Gets the list of currently active {@link RoutingController routing controllers} on which
816      * media can be played.
817      *
818      * <p>Note: The list returned here will never be empty. The first element in the list is
819      * always the {@link #getSystemController() system controller}.
820      */
821     @NonNull
getControllers()822     public List<RoutingController> getControllers() {
823         List<RoutingController> result = new ArrayList<>();
824 
825         if (isSystemRouter()) {
826             // Unlike non-system MediaRouter2, controller instances cannot be kept,
827             // since the transfer events initiated from other apps will not come through manager.
828             List<RoutingSessionInfo> sessions = sManager.getRoutingSessions(mClientPackageName);
829             for (RoutingSessionInfo session : sessions) {
830                 RoutingController controller;
831                 if (session.isSystemSession()) {
832                     mSystemController.setRoutingSessionInfo(
833                             ensureClientPackageNameForSystemSession(session));
834                     controller = mSystemController;
835                 } else {
836                     controller = new RoutingController(session);
837                 }
838                 result.add(controller);
839             }
840             return result;
841         }
842 
843         result.add(0, mSystemController);
844         synchronized (mLock) {
845             result.addAll(mNonSystemRoutingControllers.values());
846         }
847         return result;
848     }
849 
850     /**
851      * Requests a volume change for the route asynchronously.
852      * It may have no effect if the route is currently not selected.
853      *
854      * <p>This will be no-op for non-system media routers.
855      *
856      * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
857      * @see #getInstance(Context, String)
858      * @hide
859      */
860     @SystemApi
861     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
setRouteVolume(@onNull MediaRoute2Info route, int volume)862     public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
863         Objects.requireNonNull(route, "route must not be null");
864 
865         if (isSystemRouter()) {
866             sManager.setRouteVolume(route, volume);
867             return;
868         }
869         // If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2()
870     }
871 
syncRoutesOnHandler( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)872     void syncRoutesOnHandler(
873             List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
874         if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) {
875             Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes
876                     + ", currentSystemSessionInfo=" + currentSystemSessionInfo);
877             return;
878         }
879 
880         synchronized (mLock) {
881             mRoutes.clear();
882             for (MediaRoute2Info route : currentRoutes) {
883                 mRoutes.put(route.getId(), route);
884             }
885             updateFilteredRoutesLocked();
886         }
887 
888         RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo();
889         mSystemController.setRoutingSessionInfo(currentSystemSessionInfo);
890         if (!oldInfo.equals(currentSystemSessionInfo)) {
891             notifyControllerUpdated(mSystemController);
892         }
893     }
894 
dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes)895     void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) {
896         List<MediaRoute2Info> addedRoutes = new ArrayList<>();
897         List<MediaRoute2Info> removedRoutes = new ArrayList<>();
898         List<MediaRoute2Info> changedRoutes = new ArrayList<>();
899 
900         Set<String> newRouteIds =
901                 newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
902 
903         for (MediaRoute2Info route : newRoutes) {
904             MediaRoute2Info prevRoute = mPreviousRoutes.get(route.getId());
905             if (prevRoute == null) {
906                 addedRoutes.add(route);
907             } else if (!prevRoute.equals(route)) {
908                 changedRoutes.add(route);
909             }
910         }
911 
912         for (int i = 0; i < mPreviousRoutes.size(); i++) {
913             if (!newRouteIds.contains(mPreviousRoutes.keyAt(i))) {
914                 removedRoutes.add(mPreviousRoutes.valueAt(i));
915             }
916         }
917 
918         // update previous routes
919         for (MediaRoute2Info route : removedRoutes) {
920             mPreviousRoutes.remove(route.getId());
921         }
922         for (MediaRoute2Info route : addedRoutes) {
923             mPreviousRoutes.put(route.getId(), route);
924         }
925         for (MediaRoute2Info route : changedRoutes) {
926             mPreviousRoutes.put(route.getId(), route);
927         }
928 
929         if (!addedRoutes.isEmpty()) {
930             notifyRoutesAdded(addedRoutes);
931         }
932         if (!removedRoutes.isEmpty()) {
933             notifyRoutesRemoved(removedRoutes);
934         }
935         if (!changedRoutes.isEmpty()) {
936             notifyRoutesChanged(changedRoutes);
937         }
938 
939         // Note: We don't notify clients of changes in route ordering.
940         if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) {
941             notifyRoutesUpdated(newRoutes);
942         }
943     }
944 
updateRoutesOnHandler(List<MediaRoute2Info> routes)945     void updateRoutesOnHandler(List<MediaRoute2Info> routes) {
946         synchronized (mLock) {
947             mRoutes.clear();
948             for (MediaRoute2Info route : routes) {
949                 mRoutes.put(route.getId(), route);
950             }
951             updateFilteredRoutesLocked();
952         }
953     }
954 
955     /** Updates filtered routes and dispatch callbacks */
956     @GuardedBy("mLock")
updateFilteredRoutesLocked()957     void updateFilteredRoutesLocked() {
958         mFilteredRoutes =
959                 Collections.unmodifiableList(
960                         filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values())));
961         mHandler.sendMessage(
962                 obtainMessage(
963                         MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler,
964                         this,
965                         mFilteredRoutes));
966     }
967 
968     /**
969      * Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller
970      * creation has failed, then it calls {@link TransferCallback#onTransferFailure}.
971      *
972      * <p>Pass {@code null} to sessionInfo for the failure case.
973      */
createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo)974     void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
975         ControllerCreationRequest matchingRequest = null;
976         for (ControllerCreationRequest request : mControllerCreationRequests) {
977             if (request.mRequestId == requestId) {
978                 matchingRequest = request;
979                 break;
980             }
981         }
982 
983         if (matchingRequest == null) {
984             Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request.");
985             return;
986         }
987 
988         mControllerCreationRequests.remove(matchingRequest);
989         MediaRoute2Info requestedRoute = matchingRequest.mRoute;
990 
991         // TODO: Notify the reason for failure.
992         if (sessionInfo == null) {
993             notifyTransferFailure(requestedRoute);
994             return;
995         } else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
996             Log.w(
997                     TAG,
998                     "The session's provider ID does not match the requested route's. "
999                             + "(requested route's providerId="
1000                             + requestedRoute.getProviderId()
1001                             + ", actual providerId="
1002                             + sessionInfo.getProviderId()
1003                             + ")");
1004             notifyTransferFailure(requestedRoute);
1005             return;
1006         }
1007 
1008         RoutingController oldController = matchingRequest.mOldController;
1009         // When the old controller is released before transferred, treat it as a failure.
1010         // This could also happen when transfer is requested twice or more.
1011         if (!oldController.scheduleRelease()) {
1012             Log.w(
1013                     TAG,
1014                     "createControllerOnHandler: "
1015                             + "Ignoring controller creation for released old controller. "
1016                             + "oldController="
1017                             + oldController);
1018             if (!sessionInfo.isSystemSession()) {
1019                 new RoutingController(sessionInfo).release();
1020             }
1021             notifyTransferFailure(requestedRoute);
1022             return;
1023         }
1024 
1025         RoutingController newController;
1026         if (sessionInfo.isSystemSession()) {
1027             newController = getSystemController();
1028             newController.setRoutingSessionInfo(sessionInfo);
1029         } else {
1030             newController = new RoutingController(sessionInfo);
1031             synchronized (mLock) {
1032                 mNonSystemRoutingControllers.put(newController.getId(), newController);
1033             }
1034         }
1035 
1036         notifyTransfer(oldController, newController);
1037     }
1038 
updateControllerOnHandler(RoutingSessionInfo sessionInfo)1039     void updateControllerOnHandler(RoutingSessionInfo sessionInfo) {
1040         if (sessionInfo == null) {
1041             Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo.");
1042             return;
1043         }
1044 
1045         if (sessionInfo.isSystemSession()) {
1046             // The session info is sent from SystemMediaRoute2Provider.
1047             RoutingController systemController = getSystemController();
1048             systemController.setRoutingSessionInfo(sessionInfo);
1049             notifyControllerUpdated(systemController);
1050             return;
1051         }
1052 
1053         RoutingController matchingController;
1054         synchronized (mLock) {
1055             matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId());
1056         }
1057 
1058         if (matchingController == null) {
1059             Log.w(
1060                     TAG,
1061                     "updateControllerOnHandler: Matching controller not found. uniqueSessionId="
1062                             + sessionInfo.getId());
1063             return;
1064         }
1065 
1066         RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo();
1067         if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
1068             Log.w(
1069                     TAG,
1070                     "updateControllerOnHandler: Provider IDs are not matched. old="
1071                             + oldInfo.getProviderId()
1072                             + ", new="
1073                             + sessionInfo.getProviderId());
1074             return;
1075         }
1076 
1077         matchingController.setRoutingSessionInfo(sessionInfo);
1078         notifyControllerUpdated(matchingController);
1079     }
1080 
releaseControllerOnHandler(RoutingSessionInfo sessionInfo)1081     void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) {
1082         if (sessionInfo == null) {
1083             Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo.");
1084             return;
1085         }
1086 
1087         RoutingController matchingController;
1088         synchronized (mLock) {
1089             matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId());
1090         }
1091 
1092         if (matchingController == null) {
1093             if (DEBUG) {
1094                 Log.d(
1095                         TAG,
1096                         "releaseControllerOnHandler: Matching controller not found. "
1097                                 + "uniqueSessionId="
1098                                 + sessionInfo.getId());
1099             }
1100             return;
1101         }
1102 
1103         RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo();
1104         if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
1105             Log.w(
1106                     TAG,
1107                     "releaseControllerOnHandler: Provider IDs are not matched. old="
1108                             + oldInfo.getProviderId()
1109                             + ", new="
1110                             + sessionInfo.getProviderId());
1111             return;
1112         }
1113 
1114         matchingController.releaseInternal(/* shouldReleaseSession= */ false);
1115     }
1116 
onRequestCreateControllerByManagerOnHandler( RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId)1117     void onRequestCreateControllerByManagerOnHandler(
1118             RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) {
1119         RoutingController controller;
1120         if (oldSession.isSystemSession()) {
1121             controller = getSystemController();
1122         } else {
1123             synchronized (mLock) {
1124                 controller = mNonSystemRoutingControllers.get(oldSession.getId());
1125             }
1126         }
1127         if (controller == null) {
1128             return;
1129         }
1130         requestCreateController(controller, route, managerRequestId);
1131     }
1132 
1133     /**
1134      * Returns whether this router is created with {@link #getInstance(Context, String)}. This kind
1135      * of router can control the target app's media routing.
1136      */
isSystemRouter()1137     private boolean isSystemRouter() {
1138         return mClientPackageName != null;
1139     }
1140 
1141     /**
1142      * Returns a {@link RoutingSessionInfo} which has the client package name. The client package
1143      * name is set only when the given sessionInfo doesn't have it. Should only used for system
1144      * media routers.
1145      */
ensureClientPackageNameForSystemSession( @onNull RoutingSessionInfo sessionInfo)1146     private RoutingSessionInfo ensureClientPackageNameForSystemSession(
1147             @NonNull RoutingSessionInfo sessionInfo) {
1148         if (!sessionInfo.isSystemSession()
1149                 || !TextUtils.isEmpty(sessionInfo.getClientPackageName())) {
1150             return sessionInfo;
1151         }
1152 
1153         return new RoutingSessionInfo.Builder(sessionInfo)
1154                 .setClientPackageName(mClientPackageName)
1155                 .build();
1156     }
1157 
getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder)1158     private List<MediaRoute2Info> getSortedRoutes(
1159             List<MediaRoute2Info> routes, List<String> packageOrder) {
1160         if (packageOrder.isEmpty()) {
1161             return routes;
1162         }
1163         Map<String, Integer> packagePriority = new ArrayMap<>();
1164         int count = packageOrder.size();
1165         for (int i = 0; i < count; i++) {
1166             // the last package will have 1 as the priority
1167             packagePriority.put(packageOrder.get(i), count - i);
1168         }
1169         ArrayList<MediaRoute2Info> sortedRoutes = new ArrayList<>(routes);
1170         // take the negative for descending order
1171         sortedRoutes.sort(
1172                 Comparator.comparingInt(r -> -packagePriority.getOrDefault(r.getPackageName(), 0)));
1173         return sortedRoutes;
1174     }
1175 
1176     @GuardedBy("mLock")
filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes)1177     private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
1178             List<MediaRoute2Info> routes) {
1179 
1180         Set<String> deduplicationIdSet = new ArraySet<>();
1181 
1182         List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
1183         for (MediaRoute2Info route :
1184                 getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) {
1185             if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) {
1186                 continue;
1187             }
1188             if (!mDiscoveryPreference.getAllowedPackages().isEmpty()
1189                     && (route.getPackageName() == null
1190                             || !mDiscoveryPreference
1191                                     .getAllowedPackages()
1192                                     .contains(route.getPackageName()))) {
1193                 continue;
1194             }
1195             if (mDiscoveryPreference.shouldRemoveDuplicates()) {
1196                 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) {
1197                     continue;
1198                 }
1199                 deduplicationIdSet.addAll(route.getDeduplicationIds());
1200             }
1201             filteredRoutes.add(route);
1202         }
1203         return filteredRoutes;
1204     }
1205 
filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)1206     private List<MediaRoute2Info> filterRoutesWithIndividualPreference(
1207             List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
1208         List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
1209         if (isSystemRouter()) {
1210             // Individual discovery preferences do not apply for the system router.
1211             filteredRoutes.addAll(routes);
1212             return filteredRoutes;
1213         }
1214         for (MediaRoute2Info route : routes) {
1215             if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) {
1216                 continue;
1217             }
1218             if (!discoveryPreference.getAllowedPackages().isEmpty()
1219                     && (route.getPackageName() == null
1220                             || !discoveryPreference
1221                                     .getAllowedPackages()
1222                                     .contains(route.getPackageName()))) {
1223                 continue;
1224             }
1225             filteredRoutes.add(route);
1226         }
1227         return filteredRoutes;
1228     }
1229 
updateAllRoutesFromManager()1230     private void updateAllRoutesFromManager() {
1231         if (!isSystemRouter()) {
1232             return;
1233         }
1234         synchronized (mLock) {
1235             mRoutes.clear();
1236             for (MediaRoute2Info route : sManager.getAllRoutes()) {
1237                 mRoutes.put(route.getId(), route);
1238             }
1239             updateFilteredRoutesLocked();
1240         }
1241     }
1242 
notifyRoutesAdded(List<MediaRoute2Info> routes)1243     private void notifyRoutesAdded(List<MediaRoute2Info> routes) {
1244         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1245             List<MediaRoute2Info> filteredRoutes =
1246                     filterRoutesWithIndividualPreference(routes, record.mPreference);
1247             if (!filteredRoutes.isEmpty()) {
1248                 record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes));
1249             }
1250         }
1251     }
1252 
notifyRoutesRemoved(List<MediaRoute2Info> routes)1253     private void notifyRoutesRemoved(List<MediaRoute2Info> routes) {
1254         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1255             List<MediaRoute2Info> filteredRoutes =
1256                     filterRoutesWithIndividualPreference(routes, record.mPreference);
1257             if (!filteredRoutes.isEmpty()) {
1258                 record.mExecutor.execute(
1259                         () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes));
1260             }
1261         }
1262     }
1263 
notifyRoutesChanged(List<MediaRoute2Info> routes)1264     private void notifyRoutesChanged(List<MediaRoute2Info> routes) {
1265         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1266             List<MediaRoute2Info> filteredRoutes =
1267                     filterRoutesWithIndividualPreference(routes, record.mPreference);
1268             if (!filteredRoutes.isEmpty()) {
1269                 record.mExecutor.execute(
1270                         () -> record.mRouteCallback.onRoutesChanged(filteredRoutes));
1271             }
1272         }
1273     }
1274 
notifyRoutesUpdated(List<MediaRoute2Info> routes)1275     private void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
1276         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1277             List<MediaRoute2Info> filteredRoutes =
1278                     filterRoutesWithIndividualPreference(routes, record.mPreference);
1279             record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes));
1280         }
1281     }
1282 
notifyPreferredFeaturesChanged(List<String> features)1283     private void notifyPreferredFeaturesChanged(List<String> features) {
1284         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1285             record.mExecutor.execute(
1286                     () -> record.mRouteCallback.onPreferredFeaturesChanged(features));
1287         }
1288     }
1289 
notifyTransfer(RoutingController oldController, RoutingController newController)1290     private void notifyTransfer(RoutingController oldController, RoutingController newController) {
1291         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1292             record.mExecutor.execute(
1293                     () -> record.mTransferCallback.onTransfer(oldController, newController));
1294         }
1295     }
1296 
notifyTransferFailure(MediaRoute2Info route)1297     private void notifyTransferFailure(MediaRoute2Info route) {
1298         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1299             record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route));
1300         }
1301     }
1302 
notifyStop(RoutingController controller)1303     private void notifyStop(RoutingController controller) {
1304         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1305             record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller));
1306         }
1307     }
1308 
notifyControllerUpdated(RoutingController controller)1309     private void notifyControllerUpdated(RoutingController controller) {
1310         for (ControllerCallbackRecord record : mControllerCallbackRecords) {
1311             record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller));
1312         }
1313     }
1314 
1315     /** Callback for receiving events about media route discovery. */
1316     public abstract static class RouteCallback {
1317         /**
1318          * Called when routes are added. Whenever you register a callback, this will be invoked with
1319          * known routes.
1320          *
1321          * @param routes the list of routes that have been added. It's never empty.
1322          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1323          */
1324         @Deprecated
onRoutesAdded(@onNull List<MediaRoute2Info> routes)1325         public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}
1326 
1327         /**
1328          * Called when routes are removed.
1329          *
1330          * @param routes the list of routes that have been removed. It's never empty.
1331          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1332          */
1333         @Deprecated
onRoutesRemoved(@onNull List<MediaRoute2Info> routes)1334         public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {}
1335 
1336         /**
1337          * Called when the properties of one or more existing routes are changed. For example, it is
1338          * called when a route's name or volume have changed.
1339          *
1340          * @param routes the list of routes that have been changed. It's never empty.
1341          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1342          */
1343         @Deprecated
onRoutesChanged(@onNull List<MediaRoute2Info> routes)1344         public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}
1345 
1346         /**
1347          * Called when the route list is updated, which can happen when routes are added, removed,
1348          * or modified. It will also be called when a route callback is registered.
1349          *
1350          * @param routes the updated list of routes filtered by the callback's individual discovery
1351          *     preferences.
1352          */
onRoutesUpdated(@onNull List<MediaRoute2Info> routes)1353         public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {}
1354 
1355         /**
1356          * Called when the client app's preferred features are changed. When this is called, it is
1357          * recommended to {@link #getRoutes()} to get the routes that are currently available to the
1358          * app.
1359          *
1360          * @param preferredFeatures the new preferred features set by the application
1361          * @hide
1362          */
1363         @SystemApi
onPreferredFeaturesChanged(@onNull List<String> preferredFeatures)1364         public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {}
1365     }
1366 
1367     /** Callback for receiving events on media transfer. */
1368     public abstract static class TransferCallback {
1369         /**
1370          * Called when a media is transferred between two different routing controllers. This can
1371          * happen by calling {@link #transferTo(MediaRoute2Info)}.
1372          *
1373          * <p>Override this to start playback with {@code newController}. You may want to get the
1374          * status of the media that is being played with {@code oldController} and resume it
1375          * continuously with {@code newController}. After this is called, any callbacks with {@code
1376          * oldController} will not be invoked unless {@code oldController} is the {@link
1377          * #getSystemController() system controller}. You need to {@link RoutingController#release()
1378          * release} {@code oldController} before playing the media with {@code newController}.
1379          *
1380          * @param oldController the previous controller that controlled routing
1381          * @param newController the new controller to control routing
1382          * @see #transferTo(MediaRoute2Info)
1383          */
onTransfer( @onNull RoutingController oldController, @NonNull RoutingController newController)1384         public void onTransfer(
1385                 @NonNull RoutingController oldController,
1386                 @NonNull RoutingController newController) {}
1387 
1388         /**
1389          * Called when {@link #transferTo(MediaRoute2Info)} failed.
1390          *
1391          * @param requestedRoute the route info which was used for the transfer
1392          */
onTransferFailure(@onNull MediaRoute2Info requestedRoute)1393         public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {}
1394 
1395         /**
1396          * Called when a media routing stops. It can be stopped by a user or a provider. App should
1397          * not continue playing media locally when this method is called. The {@code controller} is
1398          * released before this method is called.
1399          *
1400          * @param controller the controller that controlled the stopped media routing
1401          */
onStop(@onNull RoutingController controller)1402         public void onStop(@NonNull RoutingController controller) {}
1403     }
1404 
1405     /**
1406      * A listener interface to send optional app-specific hints when creating a {@link
1407      * RoutingController}.
1408      */
1409     public interface OnGetControllerHintsListener {
1410         /**
1411          * Called when the {@link MediaRouter2} or the system is about to request a media route
1412          * provider service to create a controller with the given route. The {@link Bundle} returned
1413          * here will be sent to media route provider service as a hint.
1414          *
1415          * <p>Since controller creation can be requested by the {@link MediaRouter2} and the system,
1416          * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The
1417          * method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)}
1418          * or the main thread if it is requested by the system.
1419          *
1420          * @param route the route to create a controller with
1421          * @return An optional bundle of app-specific arguments to send to the provider, or {@code
1422          *     null} if none. The contents of this bundle may affect the result of controller
1423          *     creation.
1424          * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle)
1425          */
1426         @Nullable
onGetControllerHints(@onNull MediaRoute2Info route)1427         Bundle onGetControllerHints(@NonNull MediaRoute2Info route);
1428     }
1429 
1430     /** Callback for receiving {@link RoutingController} updates. */
1431     public abstract static class ControllerCallback {
1432         /**
1433          * Called when a controller is updated. (e.g., when the selected routes of the controller is
1434          * changed or when the volume of the controller is changed.)
1435          *
1436          * @param controller the updated controller. It may be the {@link #getSystemController()
1437          *     system controller}.
1438          * @see #getSystemController()
1439          */
onControllerUpdated(@onNull RoutingController controller)1440         public void onControllerUpdated(@NonNull RoutingController controller) {}
1441     }
1442 
1443     /**
1444      * A class to control media routing session in media route provider. For example,
1445      * selecting/deselecting/transferring to routes of a session can be done through this. Instances
1446      * are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
1447      * called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called.
1448      */
1449     public class RoutingController {
1450         private final Object mControllerLock = new Object();
1451 
1452         private static final int CONTROLLER_STATE_UNKNOWN = 0;
1453         private static final int CONTROLLER_STATE_ACTIVE = 1;
1454         private static final int CONTROLLER_STATE_RELEASING = 2;
1455         private static final int CONTROLLER_STATE_RELEASED = 3;
1456 
1457         @GuardedBy("mControllerLock")
1458         private RoutingSessionInfo mSessionInfo;
1459 
1460         @GuardedBy("mControllerLock")
1461         private int mState;
1462 
RoutingController(@onNull RoutingSessionInfo sessionInfo)1463         RoutingController(@NonNull RoutingSessionInfo sessionInfo) {
1464             mSessionInfo = sessionInfo;
1465             mState = CONTROLLER_STATE_ACTIVE;
1466         }
1467 
RoutingController(@onNull RoutingSessionInfo sessionInfo, int state)1468         RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) {
1469             mSessionInfo = sessionInfo;
1470             mState = state;
1471         }
1472 
1473         /**
1474          * @return the ID of the controller. It is globally unique.
1475          */
1476         @NonNull
getId()1477         public String getId() {
1478             synchronized (mControllerLock) {
1479                 return mSessionInfo.getId();
1480             }
1481         }
1482 
1483         /**
1484          * Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String,
1485          * String)}.
1486          *
1487          * @hide
1488          */
1489         @NonNull
1490         @TestApi
getOriginalId()1491         public String getOriginalId() {
1492             synchronized (mControllerLock) {
1493                 return mSessionInfo.getOriginalId();
1494             }
1495         }
1496 
1497         /**
1498          * Gets the control hints used to control routing session if available. It is set by the
1499          * media route provider.
1500          */
1501         @Nullable
getControlHints()1502         public Bundle getControlHints() {
1503             synchronized (mControllerLock) {
1504                 return mSessionInfo.getControlHints();
1505             }
1506         }
1507 
1508         /**
1509          * @return the unmodifiable list of currently selected routes
1510          */
1511         @NonNull
getSelectedRoutes()1512         public List<MediaRoute2Info> getSelectedRoutes() {
1513             List<String> selectedRouteIds;
1514             synchronized (mControllerLock) {
1515                 selectedRouteIds = mSessionInfo.getSelectedRoutes();
1516             }
1517             return getRoutesWithIds(selectedRouteIds);
1518         }
1519 
1520         /**
1521          * @return the unmodifiable list of selectable routes for the session.
1522          */
1523         @NonNull
getSelectableRoutes()1524         public List<MediaRoute2Info> getSelectableRoutes() {
1525             List<String> selectableRouteIds;
1526             synchronized (mControllerLock) {
1527                 selectableRouteIds = mSessionInfo.getSelectableRoutes();
1528             }
1529             return getRoutesWithIds(selectableRouteIds);
1530         }
1531 
1532         /**
1533          * @return the unmodifiable list of deselectable routes for the session.
1534          */
1535         @NonNull
getDeselectableRoutes()1536         public List<MediaRoute2Info> getDeselectableRoutes() {
1537             List<String> deselectableRouteIds;
1538             synchronized (mControllerLock) {
1539                 deselectableRouteIds = mSessionInfo.getDeselectableRoutes();
1540             }
1541             return getRoutesWithIds(deselectableRouteIds);
1542         }
1543 
1544         /**
1545          * Returns the current {@link RoutingSessionInfo} associated to this controller.
1546          */
1547         @NonNull
getRoutingSessionInfo()1548         public RoutingSessionInfo getRoutingSessionInfo() {
1549             synchronized (mControllerLock) {
1550                 return mSessionInfo;
1551             }
1552         }
1553 
1554         /**
1555          * Gets the information about how volume is handled on the session.
1556          *
1557          * <p>Please note that you may not control the volume of the session even when you can
1558          * control the volume of each selected route in the session.
1559          *
1560          * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link
1561          *     MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}
1562          */
1563         @MediaRoute2Info.PlaybackVolume
getVolumeHandling()1564         public int getVolumeHandling() {
1565             synchronized (mControllerLock) {
1566                 return mSessionInfo.getVolumeHandling();
1567             }
1568         }
1569 
1570         /** Gets the maximum volume of the session. */
getVolumeMax()1571         public int getVolumeMax() {
1572             synchronized (mControllerLock) {
1573                 return mSessionInfo.getVolumeMax();
1574             }
1575         }
1576 
1577         /**
1578          * Gets the current volume of the session.
1579          *
1580          * <p>When it's available, it represents the volume of routing session, which is a group of
1581          * selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route,
1582          *
1583          * @see MediaRoute2Info#getVolume()
1584          */
getVolume()1585         public int getVolume() {
1586             synchronized (mControllerLock) {
1587                 return mSessionInfo.getVolume();
1588             }
1589         }
1590 
1591         /**
1592          * Returns true if this controller is released, false otherwise. If it is released, then all
1593          * other getters from this instance may return invalid values. Also, any operations to this
1594          * instance will be ignored once released.
1595          *
1596          * @see #release
1597          */
isReleased()1598         public boolean isReleased() {
1599             synchronized (mControllerLock) {
1600                 return mState == CONTROLLER_STATE_RELEASED;
1601             }
1602         }
1603 
1604         /**
1605          * Selects a route for the remote session. After a route is selected, the media is expected
1606          * to be played to the all the selected routes. This is different from {@link
1607          * MediaRouter2#transferTo(MediaRoute2Info)} transferring to a route}, where the media is
1608          * expected to 'move' from one route to another.
1609          *
1610          * <p>The given route must satisfy all of the following conditions:
1611          *
1612          * <ul>
1613          *   <li>It should not be included in {@link #getSelectedRoutes()}
1614          *   <li>It should be included in {@link #getSelectableRoutes()}
1615          * </ul>
1616          *
1617          * If the route doesn't meet any of above conditions, it will be ignored.
1618          *
1619          * @see #deselectRoute(MediaRoute2Info)
1620          * @see #getSelectedRoutes()
1621          * @see #getSelectableRoutes()
1622          * @see ControllerCallback#onControllerUpdated
1623          */
selectRoute(@onNull MediaRoute2Info route)1624         public void selectRoute(@NonNull MediaRoute2Info route) {
1625             Objects.requireNonNull(route, "route must not be null");
1626             if (isReleased()) {
1627                 Log.w(TAG, "selectRoute: Called on released controller. Ignoring.");
1628                 return;
1629             }
1630 
1631             List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
1632             if (checkRouteListContainsRouteId(selectedRoutes, route.getId())) {
1633                 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
1634                 return;
1635             }
1636 
1637             List<MediaRoute2Info> selectableRoutes = getSelectableRoutes();
1638             if (!checkRouteListContainsRouteId(selectableRoutes, route.getId())) {
1639                 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
1640                 return;
1641             }
1642 
1643             if (isSystemRouter()) {
1644                 sManager.selectRoute(getRoutingSessionInfo(), route);
1645                 return;
1646             }
1647 
1648             MediaRouter2Stub stub;
1649             synchronized (mLock) {
1650                 stub = mStub;
1651             }
1652             if (stub != null) {
1653                 try {
1654                     mMediaRouterService.selectRouteWithRouter2(stub, getId(), route);
1655                 } catch (RemoteException ex) {
1656                     Log.e(TAG, "Unable to select route for session.", ex);
1657                 }
1658             }
1659         }
1660 
1661         /**
1662          * Deselects a route from the remote session. After a route is deselected, the media is
1663          * expected to be stopped on the deselected route.
1664          *
1665          * <p>The given route must satisfy all of the following conditions:
1666          *
1667          * <ul>
1668          *   <li>It should be included in {@link #getSelectedRoutes()}
1669          *   <li>It should be included in {@link #getDeselectableRoutes()}
1670          * </ul>
1671          *
1672          * If the route doesn't meet any of above conditions, it will be ignored.
1673          *
1674          * @see #getSelectedRoutes()
1675          * @see #getDeselectableRoutes()
1676          * @see ControllerCallback#onControllerUpdated
1677          */
deselectRoute(@onNull MediaRoute2Info route)1678         public void deselectRoute(@NonNull MediaRoute2Info route) {
1679             Objects.requireNonNull(route, "route must not be null");
1680             if (isReleased()) {
1681                 Log.w(TAG, "deselectRoute: called on released controller. Ignoring.");
1682                 return;
1683             }
1684 
1685             List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
1686             if (!checkRouteListContainsRouteId(selectedRoutes, route.getId())) {
1687                 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
1688                 return;
1689             }
1690 
1691             List<MediaRoute2Info> deselectableRoutes = getDeselectableRoutes();
1692             if (!checkRouteListContainsRouteId(deselectableRoutes, route.getId())) {
1693                 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
1694                 return;
1695             }
1696 
1697             if (isSystemRouter()) {
1698                 sManager.deselectRoute(getRoutingSessionInfo(), route);
1699                 return;
1700             }
1701 
1702             MediaRouter2Stub stub;
1703             synchronized (mLock) {
1704                 stub = mStub;
1705             }
1706             if (stub != null) {
1707                 try {
1708                     mMediaRouterService.deselectRouteWithRouter2(stub, getId(), route);
1709                 } catch (RemoteException ex) {
1710                     Log.e(TAG, "Unable to deselect route from session.", ex);
1711                 }
1712             }
1713         }
1714 
1715         /**
1716          * Transfers to a given route for the remote session. The given route must be included in
1717          * {@link RoutingSessionInfo#getTransferableRoutes()}.
1718          *
1719          * @see RoutingSessionInfo#getSelectedRoutes()
1720          * @see RoutingSessionInfo#getTransferableRoutes()
1721          * @see ControllerCallback#onControllerUpdated
1722          */
transferToRoute(@onNull MediaRoute2Info route)1723         void transferToRoute(@NonNull MediaRoute2Info route) {
1724             Objects.requireNonNull(route, "route must not be null");
1725             synchronized (mControllerLock) {
1726                 if (isReleased()) {
1727                     Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
1728                     return;
1729                 }
1730 
1731                 if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
1732                     Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
1733                     return;
1734                 }
1735             }
1736 
1737             MediaRouter2Stub stub;
1738             synchronized (mLock) {
1739                 stub = mStub;
1740             }
1741             if (stub != null) {
1742                 try {
1743                     mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route);
1744                 } catch (RemoteException ex) {
1745                     Log.e(TAG, "Unable to transfer to route for session.", ex);
1746                 }
1747             }
1748         }
1749 
1750         /**
1751          * Requests a volume change for the remote session asynchronously.
1752          *
1753          * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax}
1754          *     (inclusive).
1755          * @see #getVolume()
1756          */
setVolume(int volume)1757         public void setVolume(int volume) {
1758             if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
1759                 Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring.");
1760                 return;
1761             }
1762             if (volume < 0 || volume > getVolumeMax()) {
1763                 Log.w(TAG, "setVolume: The target volume is out of range. Ignoring");
1764                 return;
1765             }
1766 
1767             if (isReleased()) {
1768                 Log.w(TAG, "setVolume: Called on released controller. Ignoring.");
1769                 return;
1770             }
1771 
1772             if (isSystemRouter()) {
1773                 sManager.setSessionVolume(getRoutingSessionInfo(), volume);
1774                 return;
1775             }
1776 
1777             MediaRouter2Stub stub;
1778             synchronized (mLock) {
1779                 stub = mStub;
1780             }
1781             if (stub != null) {
1782                 try {
1783                     mMediaRouterService.setSessionVolumeWithRouter2(stub, getId(), volume);
1784                 } catch (RemoteException ex) {
1785                     Log.e(TAG, "setVolume: Failed to deliver request.", ex);
1786                 }
1787             }
1788         }
1789 
1790         /**
1791          * Releases this controller and the corresponding session. Any operations on this controller
1792          * after calling this method will be ignored. The devices that are playing media will stop
1793          * playing it.
1794          */
release()1795         public void release() {
1796             releaseInternal(/* shouldReleaseSession= */ true);
1797         }
1798 
1799         /**
1800          * Schedules release of the controller.
1801          *
1802          * @return {@code true} if it's successfully scheduled, {@code false} if it's already
1803          *     scheduled to be released or released.
1804          */
scheduleRelease()1805         boolean scheduleRelease() {
1806             synchronized (mControllerLock) {
1807                 if (mState != CONTROLLER_STATE_ACTIVE) {
1808                     return false;
1809                 }
1810                 mState = CONTROLLER_STATE_RELEASING;
1811             }
1812 
1813             synchronized (mLock) {
1814                 // It could happen if the controller is released by the another thread
1815                 // in between two locks
1816                 if (!mNonSystemRoutingControllers.remove(getId(), this)) {
1817                     // In that case, onStop isn't called so we return true to call onTransfer.
1818                     // It's also consistent with that the another thread acquires the lock later.
1819                     return true;
1820                 }
1821             }
1822 
1823             mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS);
1824 
1825             return true;
1826         }
1827 
releaseInternal(boolean shouldReleaseSession)1828         void releaseInternal(boolean shouldReleaseSession) {
1829             boolean shouldNotifyStop;
1830 
1831             synchronized (mControllerLock) {
1832                 if (mState == CONTROLLER_STATE_RELEASED) {
1833                     if (DEBUG) {
1834                         Log.d(TAG, "releaseInternal: Called on released controller. Ignoring.");
1835                     }
1836                     return;
1837                 }
1838                 shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE);
1839                 mState = CONTROLLER_STATE_RELEASED;
1840             }
1841 
1842             if (isSystemRouter()) {
1843                 sManager.releaseSession(getRoutingSessionInfo());
1844                 return;
1845             }
1846 
1847             synchronized (mLock) {
1848                 mNonSystemRoutingControllers.remove(getId(), this);
1849 
1850                 if (shouldReleaseSession && mStub != null) {
1851                     try {
1852                         mMediaRouterService.releaseSessionWithRouter2(mStub, getId());
1853                     } catch (RemoteException ex) {
1854                         ex.rethrowFromSystemServer();
1855                     }
1856                 }
1857 
1858                 if (shouldNotifyStop) {
1859                     mHandler.sendMessage(
1860                             obtainMessage(
1861                                     MediaRouter2::notifyStop,
1862                                     MediaRouter2.this,
1863                                     RoutingController.this));
1864                 }
1865 
1866                 if (mRouteCallbackRecords.isEmpty()
1867                         && mNonSystemRoutingControllers.isEmpty()
1868                         && mStub != null) {
1869                     try {
1870                         mMediaRouterService.unregisterRouter2(mStub);
1871                     } catch (RemoteException ex) {
1872                         ex.rethrowFromSystemServer();
1873                     }
1874                     mStub = null;
1875                 }
1876             }
1877         }
1878 
1879         @Override
toString()1880         public String toString() {
1881             // To prevent logging spam, we only print the ID of each route.
1882             List<String> selectedRoutes =
1883                     getSelectedRoutes().stream()
1884                             .map(MediaRoute2Info::getId)
1885                             .collect(Collectors.toList());
1886             List<String> selectableRoutes =
1887                     getSelectableRoutes().stream()
1888                             .map(MediaRoute2Info::getId)
1889                             .collect(Collectors.toList());
1890             List<String> deselectableRoutes =
1891                     getDeselectableRoutes().stream()
1892                             .map(MediaRoute2Info::getId)
1893                             .collect(Collectors.toList());
1894 
1895             StringBuilder result =
1896                     new StringBuilder()
1897                             .append("RoutingController{ ")
1898                             .append("id=")
1899                             .append(getId())
1900                             .append(", selectedRoutes={")
1901                             .append(selectedRoutes)
1902                             .append("}")
1903                             .append(", selectableRoutes={")
1904                             .append(selectableRoutes)
1905                             .append("}")
1906                             .append(", deselectableRoutes={")
1907                             .append(deselectableRoutes)
1908                             .append("}")
1909                             .append(" }");
1910             return result.toString();
1911         }
1912 
setRoutingSessionInfo(@onNull RoutingSessionInfo info)1913         void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) {
1914             synchronized (mControllerLock) {
1915                 mSessionInfo = info;
1916             }
1917         }
1918 
getRoutesWithIds(List<String> routeIds)1919         private List<MediaRoute2Info> getRoutesWithIds(List<String> routeIds) {
1920             if (isSystemRouter()) {
1921                 return getRoutes().stream()
1922                         .filter(r -> routeIds.contains(r.getId()))
1923                         .collect(Collectors.toList());
1924             }
1925 
1926             synchronized (mLock) {
1927                 return routeIds.stream()
1928                         .map(mRoutes::get)
1929                         .filter(Objects::nonNull)
1930                         .collect(Collectors.toList());
1931             }
1932         }
1933     }
1934 
1935     class SystemRoutingController extends RoutingController {
SystemRoutingController(@onNull RoutingSessionInfo sessionInfo)1936         SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) {
1937             super(sessionInfo);
1938         }
1939 
1940         @Override
isReleased()1941         public boolean isReleased() {
1942             // SystemRoutingController will never be released
1943             return false;
1944         }
1945 
1946         @Override
scheduleRelease()1947         boolean scheduleRelease() {
1948             // SystemRoutingController can be always transferred
1949             return true;
1950         }
1951 
1952         @Override
releaseInternal(boolean shouldReleaseSession)1953         void releaseInternal(boolean shouldReleaseSession) {
1954             // Do nothing. SystemRoutingController will never be released
1955         }
1956     }
1957 
1958     static final class RouteCallbackRecord {
1959         public final Executor mExecutor;
1960         public final RouteCallback mRouteCallback;
1961         public final RouteDiscoveryPreference mPreference;
1962 
RouteCallbackRecord( @ullable Executor executor, @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference)1963         RouteCallbackRecord(
1964                 @Nullable Executor executor,
1965                 @NonNull RouteCallback routeCallback,
1966                 @Nullable RouteDiscoveryPreference preference) {
1967             mRouteCallback = routeCallback;
1968             mExecutor = executor;
1969             mPreference = preference;
1970         }
1971 
1972         @Override
equals(Object obj)1973         public boolean equals(Object obj) {
1974             if (this == obj) {
1975                 return true;
1976             }
1977             if (!(obj instanceof RouteCallbackRecord)) {
1978                 return false;
1979             }
1980             return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback;
1981         }
1982 
1983         @Override
hashCode()1984         public int hashCode() {
1985             return mRouteCallback.hashCode();
1986         }
1987     }
1988 
1989     static final class TransferCallbackRecord {
1990         public final Executor mExecutor;
1991         public final TransferCallback mTransferCallback;
1992 
TransferCallbackRecord( @onNull Executor executor, @NonNull TransferCallback transferCallback)1993         TransferCallbackRecord(
1994                 @NonNull Executor executor, @NonNull TransferCallback transferCallback) {
1995             mTransferCallback = transferCallback;
1996             mExecutor = executor;
1997         }
1998 
1999         @Override
equals(Object obj)2000         public boolean equals(Object obj) {
2001             if (this == obj) {
2002                 return true;
2003             }
2004             if (!(obj instanceof TransferCallbackRecord)) {
2005                 return false;
2006             }
2007             return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback;
2008         }
2009 
2010         @Override
hashCode()2011         public int hashCode() {
2012             return mTransferCallback.hashCode();
2013         }
2014     }
2015 
2016     static final class ControllerCallbackRecord {
2017         public final Executor mExecutor;
2018         public final ControllerCallback mCallback;
2019 
ControllerCallbackRecord( @ullable Executor executor, @NonNull ControllerCallback callback)2020         ControllerCallbackRecord(
2021                 @Nullable Executor executor, @NonNull ControllerCallback callback) {
2022             mCallback = callback;
2023             mExecutor = executor;
2024         }
2025 
2026         @Override
equals(Object obj)2027         public boolean equals(Object obj) {
2028             if (this == obj) {
2029                 return true;
2030             }
2031             if (!(obj instanceof ControllerCallbackRecord)) {
2032                 return false;
2033             }
2034             return mCallback == ((ControllerCallbackRecord) obj).mCallback;
2035         }
2036 
2037         @Override
hashCode()2038         public int hashCode() {
2039             return mCallback.hashCode();
2040         }
2041     }
2042 
2043     static final class ControllerCreationRequest {
2044         public final int mRequestId;
2045         public final long mManagerRequestId;
2046         public final MediaRoute2Info mRoute;
2047         public final RoutingController mOldController;
2048 
ControllerCreationRequest( int requestId, long managerRequestId, @NonNull MediaRoute2Info route, @NonNull RoutingController oldController)2049         ControllerCreationRequest(
2050                 int requestId,
2051                 long managerRequestId,
2052                 @NonNull MediaRoute2Info route,
2053                 @NonNull RoutingController oldController) {
2054             mRequestId = requestId;
2055             mManagerRequestId = managerRequestId;
2056             mRoute = Objects.requireNonNull(route, "route must not be null");
2057             mOldController =
2058                     Objects.requireNonNull(oldController, "oldController must not be null");
2059         }
2060     }
2061 
2062     class MediaRouter2Stub extends IMediaRouter2.Stub {
2063         @Override
notifyRouterRegistered( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)2064         public void notifyRouterRegistered(
2065                 List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
2066             mHandler.sendMessage(
2067                     obtainMessage(
2068                             MediaRouter2::syncRoutesOnHandler,
2069                             MediaRouter2.this,
2070                             currentRoutes,
2071                             currentSystemSessionInfo));
2072         }
2073 
2074         @Override
notifyRoutesUpdated(List<MediaRoute2Info> routes)2075         public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
2076             mHandler.sendMessage(
2077                     obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));
2078         }
2079 
2080         @Override
notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo)2081         public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
2082             mHandler.sendMessage(
2083                     obtainMessage(
2084                             MediaRouter2::createControllerOnHandler,
2085                             MediaRouter2.this,
2086                             requestId,
2087                             sessionInfo));
2088         }
2089 
2090         @Override
notifySessionInfoChanged(@ullable RoutingSessionInfo sessionInfo)2091         public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) {
2092             mHandler.sendMessage(
2093                     obtainMessage(
2094                             MediaRouter2::updateControllerOnHandler,
2095                             MediaRouter2.this,
2096                             sessionInfo));
2097         }
2098 
2099         @Override
notifySessionReleased(RoutingSessionInfo sessionInfo)2100         public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
2101             mHandler.sendMessage(
2102                     obtainMessage(
2103                             MediaRouter2::releaseControllerOnHandler,
2104                             MediaRouter2.this,
2105                             sessionInfo));
2106         }
2107 
2108         @Override
requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route)2109         public void requestCreateSessionByManager(
2110                 long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
2111             mHandler.sendMessage(
2112                     obtainMessage(
2113                             MediaRouter2::onRequestCreateControllerByManagerOnHandler,
2114                             MediaRouter2.this,
2115                             oldSession,
2116                             route,
2117                             managerRequestId));
2118         }
2119     }
2120 
2121     // Note: All methods are run on main thread.
2122     class ManagerCallback implements MediaRouter2Manager.Callback {
2123 
2124         @Override
onRoutesUpdated()2125         public void onRoutesUpdated() {
2126             updateAllRoutesFromManager();
2127         }
2128 
2129         @Override
onTransferred( @onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)2130         public void onTransferred(
2131                 @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) {
2132             if (!oldSession.isSystemSession()
2133                     && !TextUtils.equals(mClientPackageName, oldSession.getClientPackageName())) {
2134                 return;
2135             }
2136 
2137             if (!newSession.isSystemSession()
2138                     && !TextUtils.equals(mClientPackageName, newSession.getClientPackageName())) {
2139                 return;
2140             }
2141 
2142             // For successful in-session transfer, onControllerUpdated() handles it.
2143             if (TextUtils.equals(oldSession.getId(), newSession.getId())) {
2144                 return;
2145             }
2146 
2147             RoutingController oldController;
2148             if (oldSession.isSystemSession()) {
2149                 mSystemController.setRoutingSessionInfo(
2150                         ensureClientPackageNameForSystemSession(oldSession));
2151                 oldController = mSystemController;
2152             } else {
2153                 oldController = new RoutingController(oldSession);
2154             }
2155 
2156             RoutingController newController;
2157             if (newSession.isSystemSession()) {
2158                 mSystemController.setRoutingSessionInfo(
2159                         ensureClientPackageNameForSystemSession(newSession));
2160                 newController = mSystemController;
2161             } else {
2162                 newController = new RoutingController(newSession);
2163             }
2164 
2165             notifyTransfer(oldController, newController);
2166         }
2167 
2168         @Override
onTransferFailed( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)2169         public void onTransferFailed(
2170                 @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
2171             if (!session.isSystemSession()
2172                     && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) {
2173                 return;
2174             }
2175             notifyTransferFailure(route);
2176         }
2177 
2178         @Override
onSessionUpdated(@onNull RoutingSessionInfo session)2179         public void onSessionUpdated(@NonNull RoutingSessionInfo session) {
2180             if (!session.isSystemSession()
2181                     && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) {
2182                 return;
2183             }
2184 
2185             RoutingController controller;
2186             if (session.isSystemSession()) {
2187                 mSystemController.setRoutingSessionInfo(
2188                         ensureClientPackageNameForSystemSession(session));
2189                 controller = mSystemController;
2190             } else {
2191                 controller = new RoutingController(session);
2192             }
2193             notifyControllerUpdated(controller);
2194         }
2195 
2196         @Override
onSessionReleased(@onNull RoutingSessionInfo session)2197         public void onSessionReleased(@NonNull RoutingSessionInfo session) {
2198             if (session.isSystemSession()) {
2199                 Log.e(TAG, "onSessionReleased: Called on system session. Ignoring.");
2200                 return;
2201             }
2202 
2203             if (!TextUtils.equals(mClientPackageName, session.getClientPackageName())) {
2204                 return;
2205             }
2206 
2207             notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED));
2208         }
2209 
2210         @Override
onDiscoveryPreferenceChanged( @onNull String packageName, @NonNull RouteDiscoveryPreference preference)2211         public void onDiscoveryPreferenceChanged(
2212                 @NonNull String packageName, @NonNull RouteDiscoveryPreference preference) {
2213             if (!TextUtils.equals(mClientPackageName, packageName)) {
2214                 return;
2215             }
2216 
2217             synchronized (mLock) {
2218                 mDiscoveryPreference = preference;
2219             }
2220             updateAllRoutesFromManager();
2221             notifyPreferredFeaturesChanged(preference.getPreferredFeatures());
2222         }
2223 
2224         @Override
onRequestFailed(int reason)2225         public void onRequestFailed(int reason) {
2226             // Does nothing.
2227         }
2228     }
2229 }
2230