• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
20 import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
21 import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
22 import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
23 import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
24 import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
25 import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API;
26 
27 import android.Manifest;
28 import android.annotation.CallbackExecutor;
29 import android.annotation.FlaggedApi;
30 import android.annotation.IntDef;
31 import android.annotation.NonNull;
32 import android.annotation.Nullable;
33 import android.annotation.RequiresPermission;
34 import android.annotation.SystemApi;
35 import android.annotation.TestApi;
36 import android.app.AppOpsManager;
37 import android.content.Context;
38 import android.content.pm.PackageManager;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.os.Looper;
42 import android.os.Message;
43 import android.os.Process;
44 import android.os.RemoteException;
45 import android.os.ServiceManager;
46 import android.os.UserHandle;
47 import android.text.TextUtils;
48 import android.util.ArrayMap;
49 import android.util.ArraySet;
50 import android.util.Log;
51 import android.util.SparseArray;
52 
53 import com.android.internal.annotations.GuardedBy;
54 import com.android.media.flags.Flags;
55 
56 import java.lang.annotation.Retention;
57 import java.lang.annotation.RetentionPolicy;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.Comparator;
61 import java.util.HashMap;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Objects;
65 import java.util.Set;
66 import java.util.concurrent.CopyOnWriteArrayList;
67 import java.util.concurrent.Executor;
68 import java.util.concurrent.atomic.AtomicBoolean;
69 import java.util.concurrent.atomic.AtomicInteger;
70 import java.util.function.Consumer;
71 import java.util.stream.Collectors;
72 
73 /**
74  * This API is not generally intended for third party application developers. Use the
75  * <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
76  * <a href="{@docRoot}reference/androidx/mediarouter/media/package-summary.html">Media Router
77  * Library</a> for consistent behavior across all devices.
78  *
79  * <p>MediaRouter2 allows applications to control the routing of media channels and streams from
80  * the current device to remote speakers and devices.
81  */
82 // TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute)
83 //       Not only MediaRouter2, but also to service / manager / provider.
84 // TODO: ensure thread-safe and document it
85 public final class MediaRouter2 {
86 
87     /**
88      * The state of a router not requesting route scanning.
89      *
90      * @hide
91      */
92     public static final int SCANNING_STATE_NOT_SCANNING = 0;
93 
94     /**
95      * The state of a router requesting scanning only while the user interacts with its owner app.
96      *
97      * <p>The device's screen must be on and the app must be in the foreground to trigger scanning
98      * under this state.
99      *
100      * @hide
101      */
102     public static final int SCANNING_STATE_WHILE_INTERACTIVE = 1;
103 
104     /**
105      * The state of a router requesting unrestricted scanning.
106      *
107      * <p>This state triggers scanning regardless of the restrictions required for {@link
108      * #SCANNING_STATE_WHILE_INTERACTIVE}.
109      *
110      * <p>Routers requesting unrestricted scanning must hold {@link
111      * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link
112      * Manifest.permission#MEDIA_CONTENT_CONTROL}.
113      *
114      * @hide
115      */
116     public static final int SCANNING_STATE_SCANNING_FULL = 2;
117 
118     /** @hide */
119     @IntDef(
120             prefix = "SCANNING_STATE",
121             value = {
122                 SCANNING_STATE_NOT_SCANNING,
123                 SCANNING_STATE_WHILE_INTERACTIVE,
124                 SCANNING_STATE_SCANNING_FULL
125             })
126     @Retention(RetentionPolicy.SOURCE)
127     public @interface ScanningState {}
128 
129     private static final String TAG = "MR2";
130     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
131     private static final Object sSystemRouterLock = new Object();
132     private static final Object sRouterLock = new Object();
133 
134     // The maximum time for the old routing controller available after transfer.
135     private static final int TRANSFER_TIMEOUT_MS = 30_000;
136     // The manager request ID representing that no manager is involved.
137     private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE;
138 
PackageNameUserHandlePair(String packageName, UserHandle user)139     private record PackageNameUserHandlePair(String packageName, UserHandle user) {}
140 
InstanceInvalidatedCallbackRecord(Executor executor, Runnable runnable)141     private record InstanceInvalidatedCallbackRecord(Executor executor, Runnable runnable) {}
142 
143     @GuardedBy("sSystemRouterLock")
144     private static final Map<PackageNameUserHandlePair, MediaRouter2> sAppToProxyRouterMap =
145             new ArrayMap<>();
146 
147     @GuardedBy("sRouterLock")
148     private static MediaRouter2 sInstance;
149 
150     private final Context mContext;
151     private final IMediaRouterService mMediaRouterService;
152     private final Object mLock = new Object();
153     private final MediaRouter2Impl mImpl;
154 
155     private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords =
156             new CopyOnWriteArrayList<>();
157     private final CopyOnWriteArrayList<RouteListingPreferenceCallbackRecord>
158             mListingPreferenceCallbackRecords = new CopyOnWriteArrayList<>();
159     private final CopyOnWriteArrayList<TransferCallbackRecord> mTransferCallbackRecords =
160             new CopyOnWriteArrayList<>();
161     private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords =
162             new CopyOnWriteArrayList<>();
163     private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord>
164             mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>();
165 
166     private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests =
167             new CopyOnWriteArrayList<>();
168 
169     /**
170      * Stores the latest copy of all routes received from the system server, without any filtering,
171      * sorting, or deduplication.
172      *
173      * <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key.
174      */
175     @GuardedBy("mLock")
176     private final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>();
177 
178     private final RoutingController mSystemController;
179 
180     @GuardedBy("mLock")
181     private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
182 
183     @GuardedBy("mLock")
184     private int mScreenOffScanRequestCount = 0;
185 
186     @GuardedBy("mLock")
187     private int mScreenOnScanRequestCount = 0;
188 
189     private final SparseArray<ScanRequest> mScanRequestsMap = new SparseArray<>();
190     private final AtomicInteger mNextRequestId = new AtomicInteger(1);
191     private final Handler mHandler;
192 
193     @GuardedBy("mLock")
194     private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
195 
196     // TODO: Make MediaRouter2 is always connected to the MediaRouterService.
197     @GuardedBy("mLock")
198     private MediaRouter2Stub mStub;
199 
200     @GuardedBy("mLock")
201     @Nullable
202     private RouteListingPreference mRouteListingPreference;
203 
204     @GuardedBy("mLock")
205     @Nullable
206     private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>();
207 
208     /**
209      * Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback
210      * dispatch. This is only used to determine what callback a route should be assigned to (added,
211      * removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.
212      */
213     private volatile ArrayMap<String, MediaRoute2Info> mPreviousFilteredRoutes = new ArrayMap<>();
214 
215     private final Map<String, MediaRoute2Info> mPreviousUnfilteredRoutes = new ArrayMap<>();
216 
217     /**
218      * Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be
219      * accessed through {@link #getRoutes()}.
220      *
221      * <p>This list is a copy of {@link #mRoutes} which has undergone filtering, sorting, and
222      * deduplication using criteria in {@link #mDiscoveryPreference}.
223      *
224      * @see #filterRoutesWithCompositePreferenceLocked(List)
225      */
226     private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList();
227     private volatile OnGetControllerHintsListener mOnGetControllerHintsListener;
228 
229     /** Gets an instance of the media router associated with the context. */
230     @NonNull
getInstance(@onNull Context context)231     public static MediaRouter2 getInstance(@NonNull Context context) {
232         Objects.requireNonNull(context, "context must not be null");
233         synchronized (sRouterLock) {
234             if (sInstance == null) {
235                 sInstance = new MediaRouter2(context.getApplicationContext());
236             }
237             return sInstance;
238         }
239     }
240 
241     /**
242      * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
243      * specified by {@code clientPackageName}. Returns {@code null} if the specified package name
244      * does not exist.
245      *
246      * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
247      *
248      * <ul>
249      *   <li>
250      *       <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
251      *       preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when
252      *       setting a route callback.
253      *   <li>
254      *       <p>Methods returning non-system {@link RoutingController controllers} always return new
255      *       instances with the latest data. Do not attempt to compare or store them. Instead, use
256      *       {@link #getController(String)} or {@link #getControllers()} to query the most
257      *       up-to-date state.
258      *   <li>
259      *       <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
260      * </ul>
261      *
262      * <p>Callers that only hold the revocable form of {@link
263      * Manifest.permission#MEDIA_ROUTING_CONTROL} must use {@link #getInstance(Context, String,
264      * Executor, Runnable)} instead of this method.
265      *
266      * @param clientPackageName the package name of the app to control
267      * @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}.
268      * @throws IllegalStateException if the caller only holds a revocable version of {@link
269      *     Manifest.permission#MEDIA_ROUTING_CONTROL}.
270      * @hide
271      */
272     @SuppressWarnings("RequiresPermission")
273     @RequiresPermission(
274             anyOf = {
275                 Manifest.permission.MEDIA_CONTENT_CONTROL,
276                 Manifest.permission.MEDIA_ROUTING_CONTROL
277             })
278     @SystemApi
279     @Nullable
getInstance( @onNull Context context, @NonNull String clientPackageName)280     public static MediaRouter2 getInstance(
281             @NonNull Context context, @NonNull String clientPackageName) {
282         // Capturing the IAE here to not break nullability.
283         try {
284             return findOrCreateProxyInstanceForCallingUser(
285                     context,
286                     clientPackageName,
287                     context.getUser(),
288                     /* executor */ null,
289                     /* onInstanceInvalidatedListener */ null);
290         } catch (IllegalArgumentException ex) {
291             Log.e(TAG, "Failed to create proxy router for package '" + clientPackageName + "'", ex);
292             return null;
293         }
294     }
295 
296     /**
297      * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
298      * specified by {@code clientPackageName}.
299      *
300      * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
301      *
302      * <ul>
303      *   <li>
304      *       <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
305      *       preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when
306      *       setting a route callback.
307      *   <li>
308      *       <p>Methods returning non-system {@link RoutingController controllers} always return new
309      *       instances with the latest data. Do not attempt to compare or store them. Instead, use
310      *       {@link #getController(String)} or {@link #getControllers()} to query the most
311      *       up-to-date state.
312      *   <li>
313      *       <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
314      * </ul>
315      *
316      * <p>Use this method when you only hold a revocable version of {@link
317      * Manifest.permission#MEDIA_ROUTING_CONTROL} (e.g. acquired via the {@link AppOpsManager}).
318      * Otherwise, use {@link #getInstance(Context, String)}.
319      *
320      * <p>{@code onInstanceInvalidatedListener} is called when the instance is invalidated because
321      * the calling app has lost {@link Manifest.permission#MEDIA_ROUTING_CONTROL} and does not hold
322      * {@link Manifest.permission#MEDIA_CONTENT_CONTROL}. Do not use the invalidated instance after
323      * receiving this callback, as the system will ignore all operations. Call {@link
324      * #getInstance(Context, String, Executor, Runnable)} again after reacquiring the relevant
325      * permissions.
326      *
327      * @param context The {@link Context} of the caller.
328      * @param clientPackageName The package name of the app you want to control the routing of.
329      * @param executor The {@link Executor} on which to invoke {@code
330      *     onInstanceInvalidatedListener}.
331      * @param onInstanceInvalidatedListener Callback for when the {@link MediaRouter2} instance is
332      *     invalidated due to lost permissions.
333      * @throws IllegalArgumentException if {@code clientPackageName} does not exist in the calling
334      *     user.
335      */
336     @SuppressWarnings("RequiresPermission")
337     @FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
338     @RequiresPermission(
339             anyOf = {
340                 Manifest.permission.MEDIA_CONTENT_CONTROL,
341                 Manifest.permission.MEDIA_ROUTING_CONTROL
342             })
343     @NonNull
getInstance( @onNull Context context, @NonNull String clientPackageName, @NonNull Executor executor, @NonNull Runnable onInstanceInvalidatedListener)344     public static MediaRouter2 getInstance(
345             @NonNull Context context,
346             @NonNull String clientPackageName,
347             @NonNull Executor executor,
348             @NonNull Runnable onInstanceInvalidatedListener) {
349         Objects.requireNonNull(executor, "Executor must not be null");
350         Objects.requireNonNull(
351                 onInstanceInvalidatedListener, "onInstanceInvalidatedListener must not be null.");
352 
353         return findOrCreateProxyInstanceForCallingUser(
354                 context,
355                 clientPackageName,
356                 context.getUser(),
357                 executor,
358                 onInstanceInvalidatedListener);
359     }
360 
361     /**
362      * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
363      * specified by {@code clientPackageName} and {@code user}.
364      *
365      * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
366      *
367      * <ul>
368      *   <li>
369      *       <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
370      *       preference} passed by a proxy router. Use a {@link RouteDiscoveryPreference} with empty
371      *       {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features}
372      *       when setting a route callback.
373      *   <li>
374      *       <p>Methods returning non-system {@link RoutingController controllers} always return new
375      *       instances with the latest data. Do not attempt to compare or store them. Instead, use
376      *       {@link #getController(String)} or {@link #getControllers()} to query the most
377      *       up-to-date state.
378      *   <li>
379      *       <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
380      * </ul>
381      *
382      * @param context The {@link Context} of the caller.
383      * @param clientPackageName The package name of the app you want to control the routing of.
384      * @param user The {@link UserHandle} of the user running the app for which to get the proxy
385      *     router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold
386      *     {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
387      * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and
388      *     the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
389      * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}.
390      * @hide
391      */
392     @RequiresPermission(
393             anyOf = {
394                 Manifest.permission.MEDIA_CONTENT_CONTROL,
395                 Manifest.permission.MEDIA_ROUTING_CONTROL
396             })
397     @NonNull
getInstance( @onNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user)398     public static MediaRouter2 getInstance(
399             @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) {
400         return findOrCreateProxyInstanceForCallingUser(
401                 context,
402                 clientPackageName,
403                 user,
404                 /* executor */ null,
405                 /* onInstanceInvalidatedListener */ null);
406     }
407 
408     /**
409      * Returns the per-process singleton proxy router instance for the {@code clientPackageName} and
410      * {@code user} if it exists, or otherwise it creates the appropriate instance.
411      *
412      * <p>If no instance has been created previously, the method will create an instance via {@link
413      * #MediaRouter2(Context, Looper, String, UserHandle)}.
414      */
415     @NonNull
findOrCreateProxyInstanceForCallingUser( Context context, String clientPackageName, UserHandle user, @Nullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener)416     private static MediaRouter2 findOrCreateProxyInstanceForCallingUser(
417             Context context,
418             String clientPackageName,
419             UserHandle user,
420             @Nullable Executor executor,
421             @Nullable Runnable onInstanceInvalidatedListener) {
422         Objects.requireNonNull(context, "context must not be null");
423         Objects.requireNonNull(user, "user must not be null");
424 
425         if (TextUtils.isEmpty(clientPackageName)) {
426             throw new IllegalArgumentException("clientPackageName must not be null or empty");
427         }
428 
429         if (executor == null || onInstanceInvalidatedListener == null) {
430             if (checkCallerHasOnlyRevocablePermissions(context)) {
431                 throw new IllegalStateException(
432                         "Use getInstance(Context, String, Executor, Runnable) to obtain a proxy"
433                                 + " MediaRouter2 instance.");
434             }
435         }
436 
437         PackageNameUserHandlePair key = new PackageNameUserHandlePair(clientPackageName, user);
438 
439         synchronized (sSystemRouterLock) {
440             MediaRouter2 instance = sAppToProxyRouterMap.get(key);
441             if (instance == null) {
442                 instance =
443                         new MediaRouter2(context, Looper.getMainLooper(), clientPackageName, user);
444                 // Register proxy router after instantiation to avoid race condition.
445                 ((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter();
446                 sAppToProxyRouterMap.put(key, instance);
447             }
448             ((ProxyMediaRouter2Impl) instance.mImpl)
449                     .registerInstanceInvalidatedCallback(executor, onInstanceInvalidatedListener);
450             return instance;
451         }
452     }
453 
checkCallerHasOnlyRevocablePermissions(@onNull Context context)454     private static boolean checkCallerHasOnlyRevocablePermissions(@NonNull Context context) {
455         boolean hasMediaContentControl =
456                 context.checkSelfPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
457                         == PackageManager.PERMISSION_GRANTED;
458         boolean hasRegularMediaRoutingControl =
459                 context.checkSelfPermission(Manifest.permission.MEDIA_ROUTING_CONTROL)
460                         == PackageManager.PERMISSION_GRANTED;
461         AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
462         boolean hasAppOpMediaRoutingControl =
463                 appOpsManager.unsafeCheckOp(
464                                 AppOpsManager.OPSTR_MEDIA_ROUTING_CONTROL,
465                                 context.getApplicationInfo().uid,
466                                 context.getOpPackageName())
467                         == AppOpsManager.MODE_ALLOWED;
468 
469         return !hasMediaContentControl
470                 && !hasRegularMediaRoutingControl
471                 && hasAppOpMediaRoutingControl;
472     }
473 
474     /**
475      * Starts scanning remote routes.
476      *
477      * <p>Route discovery can happen even when the {@link #startScan()} is not called. This is
478      * because the scanning could be started before by other apps. Therefore, calling this method
479      * after calling {@link #stopScan()} does not necessarily mean that the routes found before are
480      * removed and added again.
481      *
482      * <p>Use {@link RouteCallback} to get the route related events.
483      *
484      * <p>Note that calling start/stopScan is applied to all system routers in the same process.
485      *
486      * <p>This will be no-op for non-system media routers.
487      *
488      * @see #stopScan()
489      * @see #getInstance(Context, String)
490      * @hide
491      */
492     @SystemApi
493     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
startScan()494     public void startScan() {
495         mImpl.startScan();
496     }
497 
498     /**
499      * Stops scanning remote routes to reduce resource consumption.
500      *
501      * <p>Route discovery can be continued even after this method is called. This is because the
502      * scanning is only turned off when all the apps stop scanning. Therefore, calling this method
503      * does not necessarily mean the routes are removed. Also, for the same reason it does not mean
504      * that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards.
505      *
506      * <p>Use {@link RouteCallback} to get the route related events.
507      *
508      * <p>Note that calling start/stopScan is applied to all system routers in the same process.
509      *
510      * <p>This will be no-op for non-system media routers.
511      *
512      * @see #startScan()
513      * @see #getInstance(Context, String)
514      * @hide
515      */
516     @SystemApi
517     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
stopScan()518     public void stopScan() {
519         mImpl.stopScan();
520     }
521 
522     /**
523      * Requests the system to actively scan for routes based on the router's {@link
524      * RouteDiscoveryPreference route discovery preference}.
525      *
526      * <p>You must call {@link #cancelScanRequest(ScanToken)} promptly to preserve system resources
527      * like battery. Avoid scanning unless there is clear intention from the user to start routing
528      * their media.
529      *
530      * <p>{@code scanRequest} specifies relevant scanning options, like whether the system should
531      * scan with the screen off. Screen off scanning requires {@link
532      * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link
533      * Manifest.permission#MEDIA_CONTENT_CONTROL}.
534      *
535      * <p>Proxy routers use the registered {@link RouteDiscoveryPreference} of their target routers.
536      *
537      * @return A unique {@link ScanToken} that identifies the scan request.
538      * @throws SecurityException If a {@link ScanRequest} with {@link
539      *     ScanRequest.Builder#setScreenOffScan} true is passed, while not holding {@link
540      *     Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link
541      *     Manifest.permission#MEDIA_CONTENT_CONTROL}.
542      */
543     @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
544     @NonNull
requestScan(@onNull ScanRequest scanRequest)545     public ScanToken requestScan(@NonNull ScanRequest scanRequest) {
546         Objects.requireNonNull(scanRequest, "scanRequest must not be null.");
547         ScanToken token = new ScanToken(mNextRequestId.getAndIncrement());
548 
549         synchronized (mLock) {
550             boolean shouldUpdate =
551                     mScreenOffScanRequestCount == 0
552                             && (scanRequest.isScreenOffScan() || mScreenOnScanRequestCount == 0);
553 
554             if (shouldUpdate) {
555                 try {
556                     mImpl.updateScanningState(
557                             scanRequest.isScreenOffScan()
558                                     ? SCANNING_STATE_SCANNING_FULL
559                                     : SCANNING_STATE_WHILE_INTERACTIVE);
560 
561                 } catch (RemoteException ex) {
562                     throw ex.rethrowFromSystemServer();
563                 }
564             }
565 
566             if (scanRequest.isScreenOffScan()) {
567                 mScreenOffScanRequestCount++;
568             } else {
569                 mScreenOnScanRequestCount++;
570             }
571 
572             mScanRequestsMap.put(token.mId, scanRequest);
573             return token;
574         }
575     }
576 
577     /**
578      * Releases the active scan request linked to the provided {@link ScanToken}.
579      *
580      * @see #requestScan(ScanRequest)
581      * @param token {@link ScanToken} of the {@link ScanRequest} to release.
582      * @throws IllegalArgumentException if the token does not match any active scan request.
583      */
584     @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
cancelScanRequest(@onNull ScanToken token)585     public void cancelScanRequest(@NonNull ScanToken token) {
586         Objects.requireNonNull(token, "token must not be null");
587 
588         synchronized (mLock) {
589             ScanRequest request = mScanRequestsMap.get(token.mId);
590 
591             if (request == null) {
592                 throw new IllegalArgumentException(
593                         "The token does not match any active scan request");
594             }
595 
596             boolean shouldUpdate =
597                     request.isScreenOffScan()
598                             ? mScreenOffScanRequestCount == 1
599                             : mScreenOnScanRequestCount == 1 && mScreenOffScanRequestCount == 0;
600 
601             if (shouldUpdate) {
602                 try {
603                     if (!request.isScreenOffScan() || mScreenOnScanRequestCount == 0) {
604                         mImpl.updateScanningState(SCANNING_STATE_NOT_SCANNING);
605                     } else {
606                         mImpl.updateScanningState(SCANNING_STATE_WHILE_INTERACTIVE);
607                     }
608 
609                 } catch (RemoteException ex) {
610                     ex.rethrowFromSystemServer();
611                 }
612             }
613 
614             if (request.isScreenOffScan()) {
615                 mScreenOffScanRequestCount--;
616             } else {
617                 mScreenOnScanRequestCount--;
618             }
619 
620             mScanRequestsMap.remove(token.mId);
621         }
622     }
623 
MediaRouter2(Context appContext)624     private MediaRouter2(Context appContext) {
625         mContext = appContext;
626         mMediaRouterService =
627                 IMediaRouterService.Stub.asInterface(
628                         ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
629         mImpl = new LocalMediaRouter2Impl(mContext.getPackageName());
630         mHandler = new Handler(Looper.getMainLooper());
631 
632         loadSystemRoutes(/* isProxyRouter */ false);
633 
634         RoutingSessionInfo currentSystemSessionInfo = mImpl.getSystemSessionInfo();
635         if (currentSystemSessionInfo == null) {
636             throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong.");
637         }
638 
639         mSystemController = new SystemRoutingController(currentSystemSessionInfo);
640     }
641 
MediaRouter2( Context context, Looper looper, String clientPackageName, UserHandle user)642     private MediaRouter2(
643             Context context, Looper looper, String clientPackageName, UserHandle user) {
644         mContext = context;
645         mHandler = new Handler(looper);
646         mMediaRouterService =
647                 IMediaRouterService.Stub.asInterface(
648                         ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
649 
650         loadSystemRoutes(/* isProxyRouter */ true);
651 
652         mSystemController =
653                 new SystemRoutingController(
654                         ProxyMediaRouter2Impl.getSystemSessionInfoImpl(
655                                 mMediaRouterService, mContext.getPackageName(), clientPackageName));
656 
657         mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user);
658     }
659 
660     @GuardedBy("mLock")
loadSystemRoutes(boolean isProxyRouter)661     private void loadSystemRoutes(boolean isProxyRouter) {
662         List<MediaRoute2Info> currentSystemRoutes = null;
663         try {
664             currentSystemRoutes = mMediaRouterService.getSystemRoutes(mContext.getPackageName(),
665                     isProxyRouter);
666         } catch (RemoteException ex) {
667             ex.rethrowFromSystemServer();
668         }
669 
670         if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) {
671             throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong.");
672         }
673 
674         for (MediaRoute2Info route : currentSystemRoutes) {
675             mRoutes.put(route.getId(), route);
676         }
677     }
678 
679     /**
680      * Gets the client package name of the app which this media router controls.
681      *
682      * <p>This will return null for non-system media routers.
683      *
684      * @see #getInstance(Context, String)
685      * @hide
686      */
687     @SystemApi
688     @Nullable
getClientPackageName()689     public String getClientPackageName() {
690         return mImpl.getClientPackageName();
691     }
692 
693     /**
694      * Registers a callback to discover routes and to receive events when they change.
695      *
696      * <p>Clients can register multiple callbacks, as long as the {@link RouteCallback} instances
697      * are different. Each callback can provide a unique {@link RouteDiscoveryPreference preference}
698      * and will only receive updates related to that set preference.
699      *
700      * <p>If the specified callback is already registered, its registration will be updated for the
701      * given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}.
702      *
703      * <p>{@link #getInstance(Context) Local routers} must register a route callback to register in
704      * the system and start receiving updates. Otherwise, all operations will be no-ops.
705      *
706      * <p>Any discovery preference passed by a {@link #getInstance(Context, String) proxy router}
707      * will be ignored and will receive route updates based on the preference set by its matching
708      * local router.
709      */
registerRouteCallback( @onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)710     public void registerRouteCallback(
711             @NonNull @CallbackExecutor Executor executor,
712             @NonNull RouteCallback routeCallback,
713             @NonNull RouteDiscoveryPreference preference) {
714         Objects.requireNonNull(executor, "executor must not be null");
715         Objects.requireNonNull(routeCallback, "callback must not be null");
716         Objects.requireNonNull(preference, "preference must not be null");
717 
718         RouteCallbackRecord record =
719                 mImpl.createRouteCallbackRecord(executor, routeCallback, preference);
720 
721         mRouteCallbackRecords.remove(record);
722         // It can fail to add the callback record if another registration with the same callback
723         // is happening but it's okay because either this or the other registration should be done.
724         mRouteCallbackRecords.addIfAbsent(record);
725 
726         mImpl.registerRouteCallback();
727     }
728 
729     /**
730      * Unregisters the given callback. The callback will no longer receive events. If the callback
731      * has not been added or been removed already, it is ignored.
732      *
733      * @param routeCallback the callback to unregister
734      * @see #registerRouteCallback
735      */
unregisterRouteCallback(@onNull RouteCallback routeCallback)736     public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) {
737         Objects.requireNonNull(routeCallback, "callback must not be null");
738 
739         if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) {
740             Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback");
741             return;
742         }
743 
744         mImpl.unregisterRouteCallback();
745     }
746 
747     /**
748      * Registers the given callback to be invoked when the {@link RouteListingPreference} of the
749      * target router changes.
750      *
751      * <p>Calls using a previously registered callback will overwrite the previous executor.
752      *
753      * @see #setRouteListingPreference(RouteListingPreference)
754      */
755     @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
registerRouteListingPreferenceUpdatedCallback( @onNull @allbackExecutor Executor executor, @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback)756     public void registerRouteListingPreferenceUpdatedCallback(
757             @NonNull @CallbackExecutor Executor executor,
758             @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) {
759         Objects.requireNonNull(executor, "executor must not be null");
760         Objects.requireNonNull(routeListingPreferenceCallback, "callback must not be null");
761 
762         RouteListingPreferenceCallbackRecord record =
763                 new RouteListingPreferenceCallbackRecord(executor, routeListingPreferenceCallback);
764 
765         mListingPreferenceCallbackRecords.remove(record);
766         mListingPreferenceCallbackRecords.add(record);
767     }
768 
769     /**
770      * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target
771      * router changes.
772      *
773      * <p>Calls using a previously registered callback will overwrite the previous executor.
774      *
775      * @hide
776      */
registerDeviceSuggestionsCallback( @onNull @allbackExecutor Executor executor, @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback)777     public void registerDeviceSuggestionsCallback(
778             @NonNull @CallbackExecutor Executor executor,
779             @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
780         Objects.requireNonNull(executor, "executor must not be null");
781         Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null");
782 
783         DeviceSuggestionsCallbackRecord record =
784                 new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback);
785 
786         mDeviceSuggestionsCallbackRecords.remove(record);
787         mDeviceSuggestionsCallbackRecords.add(record);
788     }
789 
790     /**
791      * Unregisters the given callback to not receive {@link RouteListingPreference} change events.
792      *
793      * @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer)
794      */
795     @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
unregisterRouteListingPreferenceUpdatedCallback( @onNull Consumer<RouteListingPreference> callback)796     public void unregisterRouteListingPreferenceUpdatedCallback(
797             @NonNull Consumer<RouteListingPreference> callback) {
798         Objects.requireNonNull(callback, "callback must not be null");
799 
800         if (!mListingPreferenceCallbackRecords.remove(
801                 new RouteListingPreferenceCallbackRecord(/* executor */ null, callback))) {
802             Log.w(
803                     TAG,
804                     "unregisterRouteListingPreferenceUpdatedCallback: Ignoring an unknown"
805                         + " callback");
806         }
807     }
808 
809     /**
810      * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events.
811      *
812      * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback)
813      * @hide
814      */
unregisterDeviceSuggestionsCallback(@onNull DeviceSuggestionsCallback callback)815     public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) {
816         Objects.requireNonNull(callback, "callback must not be null");
817 
818         if (!mDeviceSuggestionsCallbackRecords.remove(
819                 new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) {
820             Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback");
821         }
822     }
823 
824     /**
825      * Shows the system output switcher dialog.
826      *
827      * <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on
828      * the screen.
829      *
830      * <p>The appearance and precise behaviour of the system output switcher dialog may vary across
831      * different devices, OS versions, and form factors, but the basic functionality stays the same.
832      *
833      * <p>See <a
834      * href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">Output
835      * Switcher documentation</a> for more details.
836      *
837      * @return {@code true} if the output switcher dialog is being shown, or {@code false} if the
838      * call is ignored because the app is in the background.
839      */
showSystemOutputSwitcher()840     public boolean showSystemOutputSwitcher() {
841         return mImpl.showSystemOutputSwitcher();
842     }
843 
844     /**
845      * Sets the {@link RouteListingPreference} of the app associated to this media router.
846      *
847      * <p>Use this method to inform the system UI of the routes that you would like to list for
848      * media routing, via the Output Switcher.
849      *
850      * <p>You should call this method before {@link #registerRouteCallback registering any route
851      * callbacks} and immediately after receiving any {@link RouteCallback#onRoutesUpdated route
852      * updates} in order to keep the system UI in a consistent state. You can also call this method
853      * at any other point to update the listing preference dynamically.
854      *
855      * <p>Calling this method on a proxy router instance will throw an {@link
856      * UnsupportedOperationException}.
857      *
858      * <p>Notes:
859      *
860      * <ol>
861      *   <li>You should not include the ids of two or more routes with a match in their {@link
862      *       MediaRoute2Info#getDeduplicationIds() deduplication ids}. If you do, the system will
863      *       deduplicate them using its own criteria.
864      *   <li>You can use this method to rank routes in the output switcher, placing the more
865      *       important routes first. The system might override the proposed ranking.
866      *   <li>You can use this method to avoid listing routes using dynamic criteria. For example,
867      *       you can limit access to a specific type of device according to runtime criteria.
868      * </ol>
869      *
870      * @param routeListingPreference The {@link RouteListingPreference} for the system to use for
871      *     route listing. When null, the system uses its default listing criteria.
872      */
setRouteListingPreference(@ullable RouteListingPreference routeListingPreference)873     public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
874         mImpl.setRouteListingPreference(routeListingPreference);
875     }
876 
877     /**
878      * Sets the suggested devices.
879      *
880      * <p>Use this method to inform the system UI that this device is suggested in the Output
881      * Switcher and media controls.
882      *
883      * <p>You should pass null to this method to clear a previously set suggestion without setting a
884      * new one.
885      *
886      * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be
887      *     provided to the user.
888      * @hide
889      */
890     @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
setDeviceSuggestions(@ullable List<SuggestedDeviceInfo> suggestedDeviceInfo)891     public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
892         mImpl.setDeviceSuggestions(suggestedDeviceInfo);
893     }
894 
895     /**
896      * Gets the current suggested devices.
897      *
898      * @return the suggested devices, keyed by the package name providing each suggestion list.
899      * @hide
900      */
901     @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
902     @Nullable
getDeviceSuggestions()903     public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
904         return mImpl.getDeviceSuggestions();
905     }
906 
907     /**
908      * Returns the current {@link RouteListingPreference} of the target router.
909      *
910      * <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns
911      * the last {@link RouteListingPreference} set by the process this router was created for.
912      *
913      * @see #setRouteListingPreference(RouteListingPreference)
914      */
915     @FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
916     @Nullable
getRouteListingPreference()917     public RouteListingPreference getRouteListingPreference() {
918         synchronized (mLock) {
919             return mRouteListingPreference;
920         }
921     }
922 
923     @GuardedBy("mLock")
updateDiscoveryPreferenceIfNeededLocked()924     private boolean updateDiscoveryPreferenceIfNeededLocked() {
925         RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder(
926                 mRouteCallbackRecords.stream().map(record -> record.mPreference).collect(
927                         Collectors.toList())).build();
928 
929         if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) {
930             return false;
931         }
932         mDiscoveryPreference = newDiscoveryPreference;
933         updateFilteredRoutesLocked();
934         return true;
935     }
936 
937     /**
938      * Gets the list of all discovered routes. This list includes the routes that are not related to
939      * the client app.
940      *
941      * <p>This will return an empty list for non-system media routers.
942      *
943      * @hide
944      */
945     @SystemApi
946     @NonNull
getAllRoutes()947     public List<MediaRoute2Info> getAllRoutes() {
948         return mImpl.getAllRoutes();
949     }
950 
951     /**
952      * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media
953      * router.
954      *
955      * <p>Please note that the list can be changed before callbacks are invoked.
956      *
957      * @return the list of routes that contains at least one of the route features in discovery
958      *     preferences registered by the application
959      */
960     @NonNull
getRoutes()961     public List<MediaRoute2Info> getRoutes() {
962         synchronized (mLock) {
963             return mFilteredRoutes;
964         }
965     }
966 
967     /**
968      * Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}.
969      * If you register the same callback twice or more, it will be ignored.
970      *
971      * @param executor the executor to execute the callback on
972      * @param callback the callback to register
973      * @see #unregisterTransferCallback
974      */
registerTransferCallback( @onNull @allbackExecutor Executor executor, @NonNull TransferCallback callback)975     public void registerTransferCallback(
976             @NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) {
977         Objects.requireNonNull(executor, "executor must not be null");
978         Objects.requireNonNull(callback, "callback must not be null");
979 
980         TransferCallbackRecord record = new TransferCallbackRecord(executor, callback);
981         if (!mTransferCallbackRecords.addIfAbsent(record)) {
982             Log.w(TAG, "registerTransferCallback: Ignoring the same callback");
983         }
984     }
985 
986     /**
987      * Unregisters the given callback. The callback will no longer receive events.
988      * If the callback has not been added or been removed already, it is ignored.
989      *
990      * @param callback the callback to unregister
991      * @see #registerTransferCallback
992      */
unregisterTransferCallback(@onNull TransferCallback callback)993     public void unregisterTransferCallback(@NonNull TransferCallback callback) {
994         Objects.requireNonNull(callback, "callback must not be null");
995 
996         if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) {
997             Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback");
998         }
999     }
1000 
1001     /**
1002      * Registers a {@link ControllerCallback}. If you register the same callback twice or more, it
1003      * will be ignored.
1004      *
1005      * @see #unregisterControllerCallback(ControllerCallback)
1006      */
registerControllerCallback( @onNull @allbackExecutor Executor executor, @NonNull ControllerCallback callback)1007     public void registerControllerCallback(
1008             @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) {
1009         Objects.requireNonNull(executor, "executor must not be null");
1010         Objects.requireNonNull(callback, "callback must not be null");
1011 
1012         ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback);
1013         if (!mControllerCallbackRecords.addIfAbsent(record)) {
1014             Log.w(TAG, "registerControllerCallback: Ignoring the same callback");
1015         }
1016     }
1017 
1018     /**
1019      * Unregisters a {@link ControllerCallback}. The callback will no longer receive events.
1020      * If the callback has not been added or been removed already, it is ignored.
1021      *
1022      * @see #registerControllerCallback(Executor, ControllerCallback)
1023      */
unregisterControllerCallback(@onNull ControllerCallback callback)1024     public void unregisterControllerCallback(@NonNull ControllerCallback callback) {
1025         Objects.requireNonNull(callback, "callback must not be null");
1026 
1027         if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) {
1028             Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback");
1029         }
1030     }
1031 
1032     /**
1033      * Sets an {@link OnGetControllerHintsListener} to send hints when creating a
1034      * {@link RoutingController}. To send the hints, listener should be set <em>BEFORE</em> calling
1035      * {@link #transferTo(MediaRoute2Info)}.
1036      *
1037      * @param listener A listener to send optional app-specific hints when creating a controller.
1038      *     {@code null} for unset.
1039      */
setOnGetControllerHintsListener(@ullable OnGetControllerHintsListener listener)1040     public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) {
1041         mImpl.setOnGetControllerHintsListener(listener);
1042     }
1043 
1044     /**
1045      * Transfers the current media to the given route. If it's necessary a new
1046      * {@link RoutingController} is created or it is handled within the current routing controller.
1047      *
1048      * @param route the route you want to transfer the current media to. Pass {@code null} to
1049      *              stop routing of the current media.
1050      * @see TransferCallback#onTransfer
1051      * @see TransferCallback#onTransferFailure
1052      */
transferTo(@onNull MediaRoute2Info route)1053     public void transferTo(@NonNull MediaRoute2Info route) {
1054         mImpl.transferTo(route);
1055     }
1056 
1057     /**
1058      * Stops the current media routing. If the {@link #getSystemController() system controller}
1059      * controls the media routing, this method is a no-op.
1060      */
stop()1061     public void stop() {
1062         mImpl.stop();
1063     }
1064 
1065     /**
1066      * Transfers the media of a routing controller to the given route.
1067      *
1068      * <p>This will be no-op for non-system media routers.
1069      *
1070      * @param controller a routing controller controlling media routing.
1071      * @param route the route you want to transfer the media to.
1072      * @hide
1073      */
1074     @SystemApi
1075     @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
transfer(@onNull RoutingController controller, @NonNull MediaRoute2Info route)1076     public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
1077         mImpl.transfer(controller.getRoutingSessionInfo(), route);
1078     }
1079 
requestCreateController( @onNull RoutingController controller, @NonNull MediaRoute2Info route, long managerRequestId)1080     void requestCreateController(
1081             @NonNull RoutingController controller,
1082             @NonNull MediaRoute2Info route,
1083             long managerRequestId) {
1084 
1085         final int requestId = mNextRequestId.getAndIncrement();
1086 
1087         ControllerCreationRequest request =
1088                 new ControllerCreationRequest(requestId, managerRequestId, route, controller);
1089         mControllerCreationRequests.add(request);
1090 
1091         OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
1092         Bundle controllerHints = null;
1093         if (listener != null) {
1094             controllerHints = listener.onGetControllerHints(route);
1095             if (controllerHints != null) {
1096                 controllerHints = new Bundle(controllerHints);
1097             }
1098         }
1099 
1100         MediaRouter2Stub stub;
1101         synchronized (mLock) {
1102             stub = mStub;
1103         }
1104         if (stub != null) {
1105             try {
1106                 mMediaRouterService.requestCreateSessionWithRouter2(
1107                         stub,
1108                         requestId,
1109                         managerRequestId,
1110                         controller.getRoutingSessionInfo(),
1111                         route,
1112                         controllerHints);
1113             } catch (RemoteException ex) {
1114                 Log.e(TAG, "createControllerForTransfer: "
1115                                 + "Failed to request for creating a controller.", ex);
1116                 mControllerCreationRequests.remove(request);
1117                 if (managerRequestId == MANAGER_REQUEST_ID_NONE) {
1118                     notifyTransferFailure(route);
1119                 }
1120             }
1121         }
1122     }
1123 
1124     @NonNull
getCurrentController()1125     private RoutingController getCurrentController() {
1126         List<RoutingController> controllers = getControllers();
1127         return controllers.get(controllers.size() - 1);
1128     }
1129 
1130     /**
1131      * Gets a {@link RoutingController} which can control the routes provided by system.
1132      * e.g. Phone speaker, wired headset, Bluetooth, etc.
1133      *
1134      * <p>Note: The system controller can't be released. Calling {@link RoutingController#release()}
1135      * will be ignored.
1136      *
1137      * <p>This method always returns the same instance.
1138      */
1139     @NonNull
getSystemController()1140     public RoutingController getSystemController() {
1141         return mSystemController;
1142     }
1143 
1144     /**
1145      * Gets a {@link RoutingController} whose ID is equal to the given ID.
1146      * Returns {@code null} if there is no matching controller.
1147      */
1148     @Nullable
getController(@onNull String id)1149     public RoutingController getController(@NonNull String id) {
1150         Objects.requireNonNull(id, "id must not be null");
1151         for (RoutingController controller : getControllers()) {
1152             if (TextUtils.equals(id, controller.getId())) {
1153                 return controller;
1154             }
1155         }
1156         return null;
1157     }
1158 
1159     /**
1160      * Gets the list of currently active {@link RoutingController routing controllers} on which
1161      * media can be played.
1162      *
1163      * <p>Note: The list returned here will never be empty. The first element in the list is
1164      * always the {@link #getSystemController() system controller}.
1165      */
1166     @NonNull
getControllers()1167     public List<RoutingController> getControllers() {
1168         return mImpl.getControllers();
1169     }
1170 
1171     /**
1172      * Sets the volume for a specific route.
1173      *
1174      * <p>The call may have no effect if the route is currently not selected.
1175      *
1176      * <p>This method is only supported by {@link #getInstance(Context, String) proxy MediaRouter2
1177      * instances}. Use {@link RoutingController#setVolume(int) RoutingController#setVolume(int)}
1178      * instead for {@link #getInstance(Context) local MediaRouter2 instances}.</p>
1179      *
1180      * @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
1181      * @throws UnsupportedOperationException If called on a {@link #getInstance(Context) local
1182      * router instance}.
1183      */
1184     @FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
1185     @RequiresPermission(
1186             anyOf = {
1187                 Manifest.permission.MEDIA_CONTENT_CONTROL,
1188                 Manifest.permission.MEDIA_ROUTING_CONTROL
1189             })
setRouteVolume(@onNull MediaRoute2Info route, int volume)1190     public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
1191         Objects.requireNonNull(route, "route must not be null");
1192 
1193         mImpl.setRouteVolume(route, volume);
1194     }
1195 
syncRoutesOnHandler( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)1196     void syncRoutesOnHandler(
1197             List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
1198         if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) {
1199             Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes
1200                     + ", currentSystemSessionInfo=" + currentSystemSessionInfo);
1201             return;
1202         }
1203 
1204         updateRoutesOnHandler(currentRoutes);
1205 
1206         RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo();
1207         mSystemController.setRoutingSessionInfo(ensureClientPackageNameForSystemSession(
1208                 currentSystemSessionInfo, mContext.getPackageName()));
1209         if (!oldInfo.equals(currentSystemSessionInfo)) {
1210             notifyControllerUpdated(mSystemController);
1211         }
1212     }
1213 
dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes)1214     void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) {
1215         List<MediaRoute2Info> addedRoutes = new ArrayList<>();
1216         List<MediaRoute2Info> removedRoutes = new ArrayList<>();
1217         List<MediaRoute2Info> changedRoutes = new ArrayList<>();
1218 
1219         Set<String> newRouteIds =
1220                 newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
1221 
1222         for (MediaRoute2Info route : newRoutes) {
1223             MediaRoute2Info prevRoute = mPreviousFilteredRoutes.get(route.getId());
1224             if (prevRoute == null) {
1225                 addedRoutes.add(route);
1226             } else if (!prevRoute.equals(route)) {
1227                 changedRoutes.add(route);
1228             }
1229         }
1230 
1231         for (int i = 0; i < mPreviousFilteredRoutes.size(); i++) {
1232             if (!newRouteIds.contains(mPreviousFilteredRoutes.keyAt(i))) {
1233                 removedRoutes.add(mPreviousFilteredRoutes.valueAt(i));
1234             }
1235         }
1236 
1237         // update previous routes
1238         for (MediaRoute2Info route : removedRoutes) {
1239             mPreviousFilteredRoutes.remove(route.getId());
1240         }
1241         for (MediaRoute2Info route : addedRoutes) {
1242             mPreviousFilteredRoutes.put(route.getId(), route);
1243         }
1244         for (MediaRoute2Info route : changedRoutes) {
1245             mPreviousFilteredRoutes.put(route.getId(), route);
1246         }
1247 
1248         if (!addedRoutes.isEmpty()) {
1249             notifyRoutesAdded(addedRoutes);
1250         }
1251         if (!removedRoutes.isEmpty()) {
1252             notifyRoutesRemoved(removedRoutes);
1253         }
1254         if (!changedRoutes.isEmpty()) {
1255             notifyRoutesChanged(changedRoutes);
1256         }
1257 
1258         // Note: We don't notify clients of changes in route ordering.
1259         if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) {
1260             notifyRoutesUpdated(newRoutes);
1261         }
1262     }
1263 
dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap)1264     void dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap) {
1265         List<RoutingController> controllers = getControllers();
1266         for (RoutingController controller : controllers) {
1267 
1268             for (String selectedRoute : controller.getRoutingSessionInfo().getSelectedRoutes()) {
1269                 if (routesMap.containsKey(selectedRoute)
1270                         && mPreviousUnfilteredRoutes.containsKey(selectedRoute)) {
1271                     MediaRoute2Info currentRoute = routesMap.get(selectedRoute);
1272                     MediaRoute2Info oldRoute = mPreviousUnfilteredRoutes.get(selectedRoute);
1273                     if (!currentRoute.equals(oldRoute)) {
1274                         notifyControllerUpdated(controller);
1275                         break;
1276                     }
1277                 }
1278             }
1279         }
1280 
1281         mPreviousUnfilteredRoutes.clear();
1282         mPreviousUnfilteredRoutes.putAll(routesMap);
1283     }
1284 
updateRoutesOnHandler(List<MediaRoute2Info> newRoutes)1285     void updateRoutesOnHandler(List<MediaRoute2Info> newRoutes) {
1286         synchronized (mLock) {
1287             mRoutes.clear();
1288             for (MediaRoute2Info route : newRoutes) {
1289                 mRoutes.put(route.getId(), route);
1290             }
1291             updateFilteredRoutesLocked();
1292         }
1293     }
1294 
1295     /** Updates filtered routes and dispatch callbacks */
1296     @GuardedBy("mLock")
updateFilteredRoutesLocked()1297     void updateFilteredRoutesLocked() {
1298         mFilteredRoutes =
1299                 Collections.unmodifiableList(
1300                         filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values())));
1301         mHandler.sendMessage(
1302                 obtainMessage(
1303                         MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler,
1304                         this,
1305                         mFilteredRoutes));
1306         mHandler.sendMessage(
1307                 obtainMessage(
1308                         MediaRouter2::dispatchControllerUpdatedIfNeededOnHandler,
1309                         this,
1310                         new HashMap<>(mRoutes)));
1311     }
1312 
1313     /**
1314      * Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller
1315      * creation has failed, then it calls {@link TransferCallback#onTransferFailure}.
1316      *
1317      * <p>Pass {@code null} to sessionInfo for the failure case.
1318      */
createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo)1319     void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
1320         ControllerCreationRequest matchingRequest = null;
1321         for (ControllerCreationRequest request : mControllerCreationRequests) {
1322             if (request.mRequestId == requestId) {
1323                 matchingRequest = request;
1324                 break;
1325             }
1326         }
1327 
1328         if (matchingRequest == null) {
1329             Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request.");
1330             return;
1331         }
1332 
1333         mControllerCreationRequests.remove(matchingRequest);
1334         MediaRoute2Info requestedRoute = matchingRequest.mRoute;
1335 
1336         // TODO: Notify the reason for failure.
1337         if (sessionInfo == null) {
1338             notifyTransferFailure(requestedRoute);
1339             return;
1340         } else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
1341             Log.w(
1342                     TAG,
1343                     "The session's provider ID does not match the requested route's. "
1344                             + "(requested route's providerId="
1345                             + requestedRoute.getProviderId()
1346                             + ", actual providerId="
1347                             + sessionInfo.getProviderId()
1348                             + ")");
1349             notifyTransferFailure(requestedRoute);
1350             return;
1351         }
1352 
1353         RoutingController oldController = matchingRequest.mOldController;
1354         // When the old controller is released before transferred, treat it as a failure.
1355         // This could also happen when transfer is requested twice or more.
1356         if (!oldController.scheduleRelease()) {
1357             Log.w(
1358                     TAG,
1359                     "createControllerOnHandler: "
1360                             + "Ignoring controller creation for released old controller. "
1361                             + "oldController="
1362                             + oldController);
1363             if (!sessionInfo.isSystemSession()) {
1364                 new RoutingController(sessionInfo).release();
1365             }
1366             notifyTransferFailure(requestedRoute);
1367             return;
1368         }
1369 
1370         RoutingController newController = addRoutingController(sessionInfo);
1371         notifyTransfer(oldController, newController);
1372     }
1373 
1374     @NonNull
addRoutingController(@onNull RoutingSessionInfo session)1375     private RoutingController addRoutingController(@NonNull RoutingSessionInfo session) {
1376         RoutingController controller;
1377         if (session.isSystemSession()) {
1378             // mSystemController is never released, so we only need to update its status.
1379             mSystemController.setRoutingSessionInfo(session);
1380             controller = mSystemController;
1381         } else {
1382             controller = new RoutingController(session);
1383             synchronized (mLock) {
1384                 mNonSystemRoutingControllers.put(controller.getId(), controller);
1385             }
1386         }
1387         return controller;
1388     }
1389 
updateControllerOnHandler(RoutingSessionInfo sessionInfo)1390     void updateControllerOnHandler(RoutingSessionInfo sessionInfo) {
1391         if (sessionInfo == null) {
1392             Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo.");
1393             return;
1394         }
1395 
1396         RoutingController controller =
1397                 getMatchingController(sessionInfo, /* logPrefix */ "updateControllerOnHandler");
1398         if (controller != null) {
1399             controller.setRoutingSessionInfo(sessionInfo);
1400             notifyControllerUpdated(controller);
1401         }
1402     }
1403 
releaseControllerOnHandler(RoutingSessionInfo sessionInfo)1404     void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) {
1405         if (sessionInfo == null) {
1406             Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo.");
1407             return;
1408         }
1409 
1410         RoutingController matchingController =
1411                 getMatchingController(sessionInfo, /* logPrefix */ "releaseControllerOnHandler");
1412 
1413         if (matchingController != null) {
1414             matchingController.releaseInternal(/* shouldReleaseSession= */ false);
1415         }
1416     }
1417 
1418     @Nullable
getMatchingController( RoutingSessionInfo sessionInfo, String logPrefix)1419     private RoutingController getMatchingController(
1420             RoutingSessionInfo sessionInfo, String logPrefix) {
1421         if (sessionInfo.isSystemSession()) {
1422             return getSystemController();
1423         } else {
1424             RoutingController controller;
1425             synchronized (mLock) {
1426                 controller = mNonSystemRoutingControllers.get(sessionInfo.getId());
1427             }
1428 
1429             if (controller == null) {
1430                 Log.w(
1431                         TAG,
1432                         logPrefix
1433                                 + ": Matching controller not found. uniqueSessionId="
1434                                 + sessionInfo.getId());
1435                 return null;
1436             }
1437 
1438             RoutingSessionInfo oldInfo = controller.getRoutingSessionInfo();
1439             if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
1440                 Log.w(
1441                         TAG,
1442                         logPrefix
1443                                 + ": Provider IDs are not matched. old="
1444                                 + oldInfo.getProviderId()
1445                                 + ", new="
1446                                 + sessionInfo.getProviderId());
1447                 return null;
1448             }
1449             return controller;
1450         }
1451     }
1452 
onRequestCreateControllerByManagerOnHandler( RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId)1453     void onRequestCreateControllerByManagerOnHandler(
1454             RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) {
1455         Log.i(
1456                 TAG,
1457                 TextUtils.formatSimple(
1458                         "requestCreateSessionByManager | requestId: %d, oldSession: %s, route: %s",
1459                         managerRequestId, oldSession, route));
1460         RoutingController controller;
1461         String oldSessionId = oldSession.getId();
1462         if (oldSession.isSystemSession()) {
1463             controller = getSystemController();
1464         } else {
1465             synchronized (mLock) {
1466                 controller = mNonSystemRoutingControllers.get(oldSessionId);
1467             }
1468         }
1469         if (controller == null) {
1470             Log.w(
1471                     TAG,
1472                     TextUtils.formatSimple(
1473                             "Ignoring requestCreateSessionByManager (requestId: %d) because no"
1474                                 + " controller for old session (id: %s) was found.",
1475                             managerRequestId, oldSessionId));
1476             return;
1477         }
1478         requestCreateController(controller, route, managerRequestId);
1479     }
1480 
getSortedRoutes( List<MediaRoute2Info> routes, List<String> packageOrder)1481     private List<MediaRoute2Info> getSortedRoutes(
1482             List<MediaRoute2Info> routes, List<String> packageOrder) {
1483         if (packageOrder.isEmpty()) {
1484             return routes;
1485         }
1486         Map<String, Integer> packagePriority = new ArrayMap<>();
1487         int count = packageOrder.size();
1488         for (int i = 0; i < count; i++) {
1489             // the last package will have 1 as the priority
1490             packagePriority.put(packageOrder.get(i), count - i);
1491         }
1492         ArrayList<MediaRoute2Info> sortedRoutes = new ArrayList<>(routes);
1493         // take the negative for descending order
1494         sortedRoutes.sort(
1495                 Comparator.comparingInt(
1496                         r -> -packagePriority.getOrDefault(r.getProviderPackageName(), 0)));
1497         return sortedRoutes;
1498     }
1499 
1500     @GuardedBy("mLock")
filterRoutesWithCompositePreferenceLocked( List<MediaRoute2Info> routes)1501     private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
1502             List<MediaRoute2Info> routes) {
1503 
1504         Set<String> deduplicationIdSet = new ArraySet<>();
1505 
1506         List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
1507         for (MediaRoute2Info route :
1508                 getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) {
1509             if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) {
1510                 continue;
1511             }
1512             if (!mDiscoveryPreference.getAllowedPackages().isEmpty()
1513                     && (route.getProviderPackageName() == null
1514                             || !mDiscoveryPreference
1515                                     .getAllowedPackages()
1516                                     .contains(route.getProviderPackageName()))) {
1517                 continue;
1518             }
1519             if (mDiscoveryPreference.shouldRemoveDuplicates()) {
1520                 if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) {
1521                     continue;
1522                 }
1523                 deduplicationIdSet.addAll(route.getDeduplicationIds());
1524             }
1525             filteredRoutes.add(route);
1526         }
1527         return filteredRoutes;
1528     }
1529 
1530     @NonNull
getRoutesWithIds(@onNull List<String> routeIds)1531     private List<MediaRoute2Info> getRoutesWithIds(@NonNull List<String> routeIds) {
1532         synchronized (mLock) {
1533             return routeIds.stream()
1534                     .map(mRoutes::get)
1535                     .filter(Objects::nonNull)
1536                     .collect(Collectors.toList());
1537         }
1538     }
1539 
notifyRoutesAdded(List<MediaRoute2Info> routes)1540     private void notifyRoutesAdded(List<MediaRoute2Info> routes) {
1541         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1542             List<MediaRoute2Info> filteredRoutes =
1543                     mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
1544             if (!filteredRoutes.isEmpty()) {
1545                 record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes));
1546             }
1547         }
1548     }
1549 
notifyRoutesRemoved(List<MediaRoute2Info> routes)1550     private void notifyRoutesRemoved(List<MediaRoute2Info> routes) {
1551         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1552             List<MediaRoute2Info> filteredRoutes =
1553                     mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
1554             if (!filteredRoutes.isEmpty()) {
1555                 record.mExecutor.execute(
1556                         () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes));
1557             }
1558         }
1559     }
1560 
notifyRoutesChanged(List<MediaRoute2Info> routes)1561     private void notifyRoutesChanged(List<MediaRoute2Info> routes) {
1562         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1563             List<MediaRoute2Info> filteredRoutes =
1564                     mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
1565             if (!filteredRoutes.isEmpty()) {
1566                 record.mExecutor.execute(
1567                         () -> record.mRouteCallback.onRoutesChanged(filteredRoutes));
1568             }
1569         }
1570     }
1571 
notifyRoutesUpdated(List<MediaRoute2Info> routes)1572     private void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
1573         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1574             List<MediaRoute2Info> filteredRoutes =
1575                     mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
1576             record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes));
1577         }
1578     }
1579 
notifyPreferredFeaturesChanged(List<String> features)1580     private void notifyPreferredFeaturesChanged(List<String> features) {
1581         for (RouteCallbackRecord record : mRouteCallbackRecords) {
1582             record.mExecutor.execute(
1583                     () -> record.mRouteCallback.onPreferredFeaturesChanged(features));
1584         }
1585     }
1586 
notifyRouteListingPreferenceUpdated(@ullable RouteListingPreference preference)1587     private void notifyRouteListingPreferenceUpdated(@Nullable RouteListingPreference preference) {
1588         for (RouteListingPreferenceCallbackRecord record : mListingPreferenceCallbackRecords) {
1589             record.mExecutor.execute(
1590                     () -> record.mRouteListingPreferenceCallback.accept(preference));
1591         }
1592     }
1593 
notifyDeviceSuggestionsUpdated( @onNull String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> deviceSuggestions)1594     private void notifyDeviceSuggestionsUpdated(
1595             @NonNull String suggestingPackageName,
1596             @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
1597         for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) {
1598             record.mExecutor.execute(
1599                     () ->
1600                             record.mDeviceSuggestionsCallback.onSuggestionUpdated(
1601                                     suggestingPackageName, deviceSuggestions));
1602         }
1603     }
1604 
notifyTransfer(RoutingController oldController, RoutingController newController)1605     private void notifyTransfer(RoutingController oldController, RoutingController newController) {
1606         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1607             record.mExecutor.execute(
1608                     () -> record.mTransferCallback.onTransfer(oldController, newController));
1609         }
1610     }
1611 
notifyTransferFailure(MediaRoute2Info route)1612     private void notifyTransferFailure(MediaRoute2Info route) {
1613         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1614             record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route));
1615         }
1616     }
1617 
notifyRequestFailed(int reason)1618     private void notifyRequestFailed(int reason) {
1619         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1620             record.mExecutor.execute(() -> record.mTransferCallback.onRequestFailed(reason));
1621         }
1622     }
1623 
notifyStop(RoutingController controller)1624     private void notifyStop(RoutingController controller) {
1625         for (TransferCallbackRecord record : mTransferCallbackRecords) {
1626             record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller));
1627         }
1628     }
1629 
notifyControllerUpdated(RoutingController controller)1630     private void notifyControllerUpdated(RoutingController controller) {
1631         for (ControllerCallbackRecord record : mControllerCallbackRecords) {
1632             record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller));
1633         }
1634     }
1635 
1636     /**
1637      * Sets the routing session's {@linkplain RoutingSessionInfo#getClientPackageName() client
1638      * package name} to {@code packageName} if empty and returns the session.
1639      *
1640      * <p>This method must only be used for {@linkplain RoutingSessionInfo#isSystemSession()
1641      * system routing sessions}.
1642      */
ensureClientPackageNameForSystemSession( @onNull RoutingSessionInfo sessionInfo, @NonNull String packageName)1643     private static RoutingSessionInfo ensureClientPackageNameForSystemSession(
1644             @NonNull RoutingSessionInfo sessionInfo, @NonNull String packageName) {
1645         if (!sessionInfo.isSystemSession()
1646                 || !TextUtils.isEmpty(sessionInfo.getClientPackageName())) {
1647             return sessionInfo;
1648         }
1649 
1650         return new RoutingSessionInfo.Builder(sessionInfo)
1651                 .setClientPackageName(packageName)
1652                 .build();
1653     }
1654 
1655     /**
1656      * Callback for receiving events about device suggestions
1657      *
1658      * @hide
1659      */
1660     public interface DeviceSuggestionsCallback {
1661 
1662         /**
1663          * Called when suggestions are updated. Whenever you register a callback, this will be
1664          * invoked with the current suggestions.
1665          *
1666          * @param suggestingPackageName the package that provided the suggestions.
1667          * @param suggestedDeviceInfo the suggestions provided by the package.
1668          */
onSuggestionUpdated( @onNull String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo)1669         void onSuggestionUpdated(
1670                 @NonNull String suggestingPackageName,
1671                 @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
1672     }
1673 
1674     /** Callback for receiving events about media route discovery. */
1675     public abstract static class RouteCallback {
1676         /**
1677          * Called when routes are added. Whenever you register a callback, this will be invoked with
1678          * known routes.
1679          *
1680          * @param routes the list of routes that have been added. It's never empty.
1681          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1682          */
1683         @Deprecated
onRoutesAdded(@onNull List<MediaRoute2Info> routes)1684         public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}
1685 
1686         /**
1687          * Called when routes are removed.
1688          *
1689          * @param routes the list of routes that have been removed. It's never empty.
1690          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1691          */
1692         @Deprecated
onRoutesRemoved(@onNull List<MediaRoute2Info> routes)1693         public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {}
1694 
1695         /**
1696          * Called when the properties of one or more existing routes are changed. For example, it is
1697          * called when a route's name or volume have changed.
1698          *
1699          * @param routes the list of routes that have been changed. It's never empty.
1700          * @deprecated Use {@link #onRoutesUpdated(List)} instead.
1701          */
1702         @Deprecated
onRoutesChanged(@onNull List<MediaRoute2Info> routes)1703         public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}
1704 
1705         /**
1706          * Called when the route list is updated, which can happen when routes are added, removed,
1707          * or modified. It will also be called when a route callback is registered.
1708          *
1709          * @param routes the updated list of routes filtered by the callback's individual discovery
1710          *     preferences.
1711          */
onRoutesUpdated(@onNull List<MediaRoute2Info> routes)1712         public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {}
1713 
1714         /**
1715          * Called when the client app's preferred features are changed. When this is called, it is
1716          * recommended to {@link #getRoutes()} to get the routes that are currently available to the
1717          * app.
1718          *
1719          * @param preferredFeatures the new preferred features set by the application
1720          * @hide
1721          */
1722         @SystemApi
onPreferredFeaturesChanged(@onNull List<String> preferredFeatures)1723         public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {}
1724     }
1725 
1726     /** Callback for receiving events on media transfer. */
1727     public abstract static class TransferCallback {
1728         /**
1729          * Called when a media is transferred between two different routing controllers. This can
1730          * happen by calling {@link #transferTo(MediaRoute2Info)}.
1731          *
1732          * <p>Override this to start playback with {@code newController}. You may want to get the
1733          * status of the media that is being played with {@code oldController} and resume it
1734          * continuously with {@code newController}. After this is called, any callbacks with {@code
1735          * oldController} will not be invoked unless {@code oldController} is the {@link
1736          * #getSystemController() system controller}. You need to {@link RoutingController#release()
1737          * release} {@code oldController} before playing the media with {@code newController}.
1738          *
1739          * @param oldController the previous controller that controlled routing
1740          * @param newController the new controller to control routing
1741          * @see #transferTo(MediaRoute2Info)
1742          */
onTransfer( @onNull RoutingController oldController, @NonNull RoutingController newController)1743         public void onTransfer(
1744                 @NonNull RoutingController oldController,
1745                 @NonNull RoutingController newController) {}
1746 
1747         /**
1748          * Called when {@link #transferTo(MediaRoute2Info)} failed.
1749          *
1750          * @param requestedRoute the route info which was used for the transfer
1751          */
onTransferFailure(@onNull MediaRoute2Info requestedRoute)1752         public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {}
1753 
1754         /**
1755          * Called when a media routing stops. It can be stopped by a user or a provider. App should
1756          * not continue playing media locally when this method is called. The {@code controller} is
1757          * released before this method is called.
1758          *
1759          * @param controller the controller that controlled the stopped media routing
1760          */
onStop(@onNull RoutingController controller)1761         public void onStop(@NonNull RoutingController controller) {}
1762 
1763         /**
1764          * Called when a routing request fails.
1765          *
1766          * @param reason Reason for failure as per {@link
1767          *     android.media.MediaRoute2ProviderService.Reason}
1768          * @hide
1769          */
onRequestFailed(int reason)1770         public void onRequestFailed(int reason) {}
1771     }
1772 
1773     /**
1774      * A listener interface to send optional app-specific hints when creating a {@link
1775      * RoutingController}.
1776      */
1777     public interface OnGetControllerHintsListener {
1778         /**
1779          * Called when the {@link MediaRouter2} or the system is about to request a media route
1780          * provider service to create a controller with the given route. The {@link Bundle} returned
1781          * here will be sent to media route provider service as a hint.
1782          *
1783          * <p>Since controller creation can be requested by the {@link MediaRouter2} and the system,
1784          * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The
1785          * method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)}
1786          * or the main thread if it is requested by the system.
1787          *
1788          * @param route the route to create a controller with
1789          * @return An optional bundle of app-specific arguments to send to the provider, or {@code
1790          *     null} if none. The contents of this bundle may affect the result of controller
1791          *     creation.
1792          * @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle)
1793          */
1794         @Nullable
onGetControllerHints(@onNull MediaRoute2Info route)1795         Bundle onGetControllerHints(@NonNull MediaRoute2Info route);
1796     }
1797 
1798     /** Callback for receiving {@link RoutingController} updates. */
1799     public abstract static class ControllerCallback {
1800         /**
1801          * Called when a controller is updated. (e.g., when the selected routes of the controller is
1802          * changed or when the volume of the controller is changed.)
1803          *
1804          * @param controller the updated controller. It may be the {@link #getSystemController()
1805          *     system controller}.
1806          * @see #getSystemController()
1807          */
onControllerUpdated(@onNull RoutingController controller)1808         public void onControllerUpdated(@NonNull RoutingController controller) {}
1809     }
1810 
1811     /**
1812      * Represents an active scan request registered in the system.
1813      *
1814      * <p>See {@link #requestScan(ScanRequest)} for more information.
1815      */
1816     @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
1817     public static final class ScanToken {
1818         private final int mId;
1819 
ScanToken(int id)1820         private ScanToken(int id) {
1821             mId = id;
1822         }
1823     }
1824 
1825     /**
1826      * Represents a set of parameters for scanning requests.
1827      *
1828      * <p>See {@link #requestScan(ScanRequest)} for more details.
1829      */
1830     @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
1831     public static final class ScanRequest {
1832         private final boolean mIsScreenOffScan;
1833 
ScanRequest(boolean isScreenOffScan)1834         private ScanRequest(boolean isScreenOffScan) {
1835             mIsScreenOffScan = isScreenOffScan;
1836         }
1837 
1838         /**
1839          * Returns whether the scan request corresponds to a screen-off scan.
1840          *
1841          * @see #requestScan(ScanRequest)
1842          */
isScreenOffScan()1843         public boolean isScreenOffScan() {
1844             return mIsScreenOffScan;
1845         }
1846 
1847         /**
1848          * Builder class for {@link ScanRequest}.
1849          *
1850          * @see #requestScan(ScanRequest)
1851          */
1852         public static final class Builder {
1853             boolean mIsScreenOffScan;
1854 
1855             /**
1856              * Creates a builder for a {@link ScanRequest} instance.
1857              *
1858              * @see #requestScan(ScanRequest)
1859              */
Builder()1860             public Builder() {}
1861 
1862             /**
1863              * Sets whether the app is requesting to scan even while the screen is off, bypassing
1864              * default scanning restrictions. Only apps holding {@link
1865              * Manifest.permission#MEDIA_ROUTING_CONTROL} or {@link
1866              * Manifest.permission#MEDIA_CONTENT_CONTROL} should set this to {@code true}.
1867              *
1868              * @see #requestScan(ScanRequest)
1869              */
1870             @NonNull
setScreenOffScan(boolean isScreenOffScan)1871             public Builder setScreenOffScan(boolean isScreenOffScan) {
1872                 mIsScreenOffScan = isScreenOffScan;
1873                 return this;
1874             }
1875 
1876             /** Returns a new {@link ScanRequest} instance. */
1877             @NonNull
build()1878             public ScanRequest build() {
1879                 return new ScanRequest(mIsScreenOffScan);
1880             }
1881         }
1882     }
1883 
1884     /**
1885      * Controls a media routing session.
1886      *
1887      * <p>Routing controllers wrap a {@link RoutingSessionInfo}, taking care of mapping route ids to
1888      * {@link MediaRoute2Info} instances. You can still access the underlying session using {@link
1889      * #getRoutingSessionInfo()}, but keep in mind it can be changed by other threads. Changes to
1890      * the routing session are notified via {@link ControllerCallback}.
1891      */
1892     public class RoutingController {
1893         private final Object mControllerLock = new Object();
1894 
1895         private static final int CONTROLLER_STATE_UNKNOWN = 0;
1896         private static final int CONTROLLER_STATE_ACTIVE = 1;
1897         private static final int CONTROLLER_STATE_RELEASING = 2;
1898         private static final int CONTROLLER_STATE_RELEASED = 3;
1899 
1900         @GuardedBy("mControllerLock")
1901         private RoutingSessionInfo mSessionInfo;
1902 
1903         @GuardedBy("mControllerLock")
1904         private int mState;
1905 
RoutingController(@onNull RoutingSessionInfo sessionInfo)1906         RoutingController(@NonNull RoutingSessionInfo sessionInfo) {
1907             mSessionInfo = sessionInfo;
1908             mState = CONTROLLER_STATE_ACTIVE;
1909         }
1910 
RoutingController(@onNull RoutingSessionInfo sessionInfo, int state)1911         RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) {
1912             mSessionInfo = sessionInfo;
1913             mState = state;
1914         }
1915 
1916         /**
1917          * @return the ID of the controller. It is globally unique.
1918          */
1919         @NonNull
getId()1920         public String getId() {
1921             synchronized (mControllerLock) {
1922                 return mSessionInfo.getId();
1923             }
1924         }
1925 
1926         /**
1927          * Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String,
1928          * String)}.
1929          *
1930          * @hide
1931          */
1932         @NonNull
1933         @TestApi
getOriginalId()1934         public String getOriginalId() {
1935             synchronized (mControllerLock) {
1936                 return mSessionInfo.getOriginalId();
1937             }
1938         }
1939 
1940         /**
1941          * Gets the control hints used to control routing session if available. It is set by the
1942          * media route provider.
1943          */
1944         @Nullable
getControlHints()1945         public Bundle getControlHints() {
1946             synchronized (mControllerLock) {
1947                 return mSessionInfo.getControlHints();
1948             }
1949         }
1950 
1951         /**
1952          * Returns the unmodifiable list of currently selected routes
1953          *
1954          * @see RoutingSessionInfo#getSelectedRoutes()
1955          */
1956         @NonNull
getSelectedRoutes()1957         public List<MediaRoute2Info> getSelectedRoutes() {
1958             List<String> selectedRouteIds;
1959             synchronized (mControllerLock) {
1960                 selectedRouteIds = mSessionInfo.getSelectedRoutes();
1961             }
1962             return getRoutesWithIds(selectedRouteIds);
1963         }
1964 
1965         /**
1966          * Returns the unmodifiable list of selectable routes for the session.
1967          *
1968          * @see RoutingSessionInfo#getSelectableRoutes()
1969          */
1970         @NonNull
getSelectableRoutes()1971         public List<MediaRoute2Info> getSelectableRoutes() {
1972             List<String> selectableRouteIds;
1973             synchronized (mControllerLock) {
1974                 selectableRouteIds = mSessionInfo.getSelectableRoutes();
1975             }
1976             return getRoutesWithIds(selectableRouteIds);
1977         }
1978 
1979         /**
1980          * Returns the unmodifiable list of deselectable routes for the session.
1981          *
1982          * @see RoutingSessionInfo#getDeselectableRoutes()
1983          */
1984         @NonNull
getDeselectableRoutes()1985         public List<MediaRoute2Info> getDeselectableRoutes() {
1986             List<String> deselectableRouteIds;
1987             synchronized (mControllerLock) {
1988                 deselectableRouteIds = mSessionInfo.getDeselectableRoutes();
1989             }
1990             return getRoutesWithIds(deselectableRouteIds);
1991         }
1992 
1993         /**
1994          * Returns the unmodifiable list of transferable routes for the session.
1995          *
1996          * @see RoutingSessionInfo#getTransferableRoutes()
1997          */
1998         @FlaggedApi(FLAG_ENABLE_GET_TRANSFERABLE_ROUTES)
1999         @NonNull
getTransferableRoutes()2000         public List<MediaRoute2Info> getTransferableRoutes() {
2001             List<String> transferableRoutes;
2002             synchronized (mControllerLock) {
2003                 transferableRoutes = mSessionInfo.getTransferableRoutes();
2004             }
2005             return getRoutesWithIds(transferableRoutes);
2006         }
2007 
2008         /**
2009          * Returns whether the transfer was initiated by the calling app (as determined by comparing
2010          * {@link UserHandle} and package name).
2011          */
2012         @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
wasTransferInitiatedBySelf()2013         public boolean wasTransferInitiatedBySelf() {
2014             return mImpl.wasTransferredBySelf(getRoutingSessionInfo());
2015         }
2016 
2017         /**
2018          * Returns the current {@link RoutingSessionInfo} associated to this controller.
2019          */
2020         @NonNull
getRoutingSessionInfo()2021         public RoutingSessionInfo getRoutingSessionInfo() {
2022             synchronized (mControllerLock) {
2023                 return mSessionInfo;
2024             }
2025         }
2026 
2027         /**
2028          * Gets the information about how volume is handled on the session.
2029          *
2030          * <p>Please note that you may not control the volume of the session even when you can
2031          * control the volume of each selected route in the session.
2032          *
2033          * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link
2034          *     MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}
2035          */
2036         @MediaRoute2Info.PlaybackVolume
getVolumeHandling()2037         public int getVolumeHandling() {
2038             synchronized (mControllerLock) {
2039                 return mSessionInfo.getVolumeHandling();
2040             }
2041         }
2042 
2043         /** Gets the maximum volume of the session. */
getVolumeMax()2044         public int getVolumeMax() {
2045             synchronized (mControllerLock) {
2046                 return mSessionInfo.getVolumeMax();
2047             }
2048         }
2049 
2050         /**
2051          * Gets the current volume of the session.
2052          *
2053          * <p>When it's available, it represents the volume of routing session, which is a group of
2054          * selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route,
2055          *
2056          * @see MediaRoute2Info#getVolume()
2057          */
getVolume()2058         public int getVolume() {
2059             synchronized (mControllerLock) {
2060                 return mSessionInfo.getVolume();
2061             }
2062         }
2063 
2064         /**
2065          * Returns true if this controller is released, false otherwise. If it is released, then all
2066          * other getters from this instance may return invalid values. Also, any operations to this
2067          * instance will be ignored once released.
2068          *
2069          * @see #release
2070          */
isReleased()2071         public boolean isReleased() {
2072             synchronized (mControllerLock) {
2073                 return mState == CONTROLLER_STATE_RELEASED;
2074             }
2075         }
2076 
2077         /**
2078          * Selects a route for the remote session. After a route is selected, the media is expected
2079          * to be played to the all the selected routes. This is different from {@link
2080          * MediaRouter2#transferTo(MediaRoute2Info) transferring to a route}, where the media is
2081          * expected to 'move' from one route to another.
2082          *
2083          * <p>The given route must satisfy all of the following conditions:
2084          *
2085          * <ul>
2086          *   <li>It should not be included in {@link #getSelectedRoutes()}
2087          *   <li>It should be included in {@link #getSelectableRoutes()}
2088          * </ul>
2089          *
2090          * If the route doesn't meet any of above conditions, it will be ignored.
2091          *
2092          * @see #deselectRoute(MediaRoute2Info)
2093          * @see #getSelectedRoutes()
2094          * @see #getSelectableRoutes()
2095          * @see ControllerCallback#onControllerUpdated
2096          */
selectRoute(@onNull MediaRoute2Info route)2097         public void selectRoute(@NonNull MediaRoute2Info route) {
2098             Objects.requireNonNull(route, "route must not be null");
2099             if (isReleased()) {
2100                 Log.w(TAG, "selectRoute: Called on released controller. Ignoring.");
2101                 return;
2102             }
2103 
2104             List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
2105             if (containsRouteInfoWithId(selectedRoutes, route.getId())) {
2106                 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
2107                 return;
2108             }
2109 
2110             List<MediaRoute2Info> selectableRoutes = getSelectableRoutes();
2111             if (!containsRouteInfoWithId(selectableRoutes, route.getId())) {
2112                 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
2113                 return;
2114             }
2115 
2116             mImpl.selectRoute(route, getRoutingSessionInfo());
2117         }
2118 
2119         /**
2120          * Deselects a route from the remote session. After a route is deselected, the media is
2121          * expected to be stopped on the deselected route.
2122          *
2123          * <p>The given route must satisfy all of the following conditions:
2124          *
2125          * <ul>
2126          *   <li>It should be included in {@link #getSelectedRoutes()}
2127          *   <li>It should be included in {@link #getDeselectableRoutes()}
2128          * </ul>
2129          *
2130          * If the route doesn't meet any of above conditions, it will be ignored.
2131          *
2132          * @see #getSelectedRoutes()
2133          * @see #getDeselectableRoutes()
2134          * @see ControllerCallback#onControllerUpdated
2135          */
deselectRoute(@onNull MediaRoute2Info route)2136         public void deselectRoute(@NonNull MediaRoute2Info route) {
2137             Objects.requireNonNull(route, "route must not be null");
2138             if (isReleased()) {
2139                 Log.w(TAG, "deselectRoute: called on released controller. Ignoring.");
2140                 return;
2141             }
2142 
2143             List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
2144             if (!containsRouteInfoWithId(selectedRoutes, route.getId())) {
2145                 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
2146                 return;
2147             }
2148 
2149             List<MediaRoute2Info> deselectableRoutes = getDeselectableRoutes();
2150             if (!containsRouteInfoWithId(deselectableRoutes, route.getId())) {
2151                 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
2152                 return;
2153             }
2154 
2155             mImpl.deselectRoute(route, getRoutingSessionInfo());
2156         }
2157 
2158         /**
2159          * Attempts a transfer to a {@link RoutingSessionInfo#getTransferableRoutes() transferable
2160          * route}.
2161          *
2162          * <p>Transferring to a transferable route does not require the app to transfer the playback
2163          * state from one route to the other. The route provider completely manages the transfer. An
2164          * example of provider-managed transfers are the switches between the system's routes, like
2165          * the built-in speakers and a BT headset.
2166          *
2167          * @return True if the transfer is handled by this controller, or false if a new controller
2168          *     should be created instead.
2169          * @see RoutingSessionInfo#getSelectedRoutes()
2170          * @see RoutingSessionInfo#getTransferableRoutes()
2171          * @see ControllerCallback#onControllerUpdated
2172          */
tryTransferWithinProvider(@onNull MediaRoute2Info route)2173         boolean tryTransferWithinProvider(@NonNull MediaRoute2Info route) {
2174             Objects.requireNonNull(route, "route must not be null");
2175             synchronized (mControllerLock) {
2176                 if (isReleased()) {
2177                     Log.w(
2178                             TAG,
2179                             "tryTransferWithinProvider: Called on released controller. Ignoring.");
2180                     return true;
2181                 }
2182 
2183                 // If this call is trying to transfer to a selected system route, we let them
2184                 // through as a provider driven transfer in order to update the transfer reason and
2185                 // initiator data.
2186                 boolean isSystemRouteReselection =
2187                         Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
2188                                 && mSessionInfo.isSystemSession()
2189                                 && route.isSystemRoute()
2190                                 && mSessionInfo.getSelectedRoutes().contains(route.getId());
2191                 if (!isSystemRouteReselection
2192                         && !mSessionInfo.getTransferableRoutes().contains(route.getId())) {
2193                     Log.i(
2194                             TAG,
2195                             "Transferring to a non-transferable route="
2196                                     + route
2197                                     + " session= "
2198                                     + mSessionInfo.getId());
2199                     return false;
2200                 }
2201             }
2202 
2203             MediaRouter2Stub stub;
2204             synchronized (mLock) {
2205                 stub = mStub;
2206             }
2207             if (stub != null) {
2208                 try {
2209                     mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route);
2210                 } catch (RemoteException ex) {
2211                     Log.e(TAG, "Unable to transfer to route for session.", ex);
2212                 }
2213             }
2214             return true;
2215         }
2216 
2217         /**
2218          * Requests a volume change for the remote session asynchronously.
2219          *
2220          * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax}
2221          *     (inclusive).
2222          * @see #getVolume()
2223          */
setVolume(int volume)2224         public void setVolume(int volume) {
2225             if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
2226                 Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring.");
2227                 return;
2228             }
2229             if (volume < 0 || volume > getVolumeMax()) {
2230                 Log.w(TAG, "setVolume: The target volume is out of range. Ignoring");
2231                 return;
2232             }
2233 
2234             if (isReleased()) {
2235                 Log.w(TAG, "setVolume: Called on released controller. Ignoring.");
2236                 return;
2237             }
2238 
2239             mImpl.setSessionVolume(volume, getRoutingSessionInfo());
2240         }
2241 
2242         /**
2243          * Releases this controller and the corresponding session. Any operations on this controller
2244          * after calling this method will be ignored. The devices that are playing media will stop
2245          * playing it.
2246          */
release()2247         public void release() {
2248             releaseInternal(/* shouldReleaseSession= */ true);
2249         }
2250 
2251         /**
2252          * Schedules release of the controller.
2253          *
2254          * @return {@code true} if it's successfully scheduled, {@code false} if it's already
2255          *     scheduled to be released or released.
2256          */
scheduleRelease()2257         boolean scheduleRelease() {
2258             synchronized (mControllerLock) {
2259                 if (mState != CONTROLLER_STATE_ACTIVE) {
2260                     return false;
2261                 }
2262                 mState = CONTROLLER_STATE_RELEASING;
2263             }
2264 
2265             synchronized (mLock) {
2266                 // It could happen if the controller is released by the another thread
2267                 // in between two locks
2268                 if (!mNonSystemRoutingControllers.remove(getId(), this)) {
2269                     // In that case, onStop isn't called so we return true to call onTransfer.
2270                     // It's also consistent with that the another thread acquires the lock later.
2271                     return true;
2272                 }
2273             }
2274 
2275             mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS);
2276 
2277             return true;
2278         }
2279 
releaseInternal(boolean shouldReleaseSession)2280         void releaseInternal(boolean shouldReleaseSession) {
2281             boolean shouldNotifyStop;
2282 
2283             synchronized (mControllerLock) {
2284                 if (mState == CONTROLLER_STATE_RELEASED) {
2285                     if (DEBUG) {
2286                         Log.d(TAG, "releaseInternal: Called on released controller. Ignoring.");
2287                     }
2288                     return;
2289                 }
2290                 shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE);
2291                 mState = CONTROLLER_STATE_RELEASED;
2292             }
2293 
2294             mImpl.releaseSession(shouldReleaseSession, shouldNotifyStop, this);
2295         }
2296 
2297         @Override
toString()2298         public String toString() {
2299             // To prevent logging spam, we only print the ID of each route.
2300             List<String> selectedRoutes =
2301                     getSelectedRoutes().stream()
2302                             .map(MediaRoute2Info::getId)
2303                             .collect(Collectors.toList());
2304             List<String> selectableRoutes =
2305                     getSelectableRoutes().stream()
2306                             .map(MediaRoute2Info::getId)
2307                             .collect(Collectors.toList());
2308             List<String> deselectableRoutes =
2309                     getDeselectableRoutes().stream()
2310                             .map(MediaRoute2Info::getId)
2311                             .collect(Collectors.toList());
2312 
2313             StringBuilder result =
2314                     new StringBuilder()
2315                             .append("RoutingController{ ")
2316                             .append("id=")
2317                             .append(getId())
2318                             .append(", selectedRoutes={")
2319                             .append(selectedRoutes)
2320                             .append("}")
2321                             .append(", selectableRoutes={")
2322                             .append(selectableRoutes)
2323                             .append("}")
2324                             .append(", deselectableRoutes={")
2325                             .append(deselectableRoutes)
2326                             .append("}")
2327                             .append(" }");
2328             return result.toString();
2329         }
2330 
setRoutingSessionInfo(@onNull RoutingSessionInfo info)2331         void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) {
2332             synchronized (mControllerLock) {
2333                 mSessionInfo = info;
2334             }
2335         }
2336 
2337         /** Returns whether any route in {@code routeList} has a same unique ID with given route. */
containsRouteInfoWithId( @onNull List<MediaRoute2Info> routeList, @NonNull String routeId)2338         private static boolean containsRouteInfoWithId(
2339                 @NonNull List<MediaRoute2Info> routeList, @NonNull String routeId) {
2340             for (MediaRoute2Info info : routeList) {
2341                 if (TextUtils.equals(routeId, info.getId())) {
2342                     return true;
2343                 }
2344             }
2345             return false;
2346         }
2347     }
2348 
2349     class SystemRoutingController extends RoutingController {
SystemRoutingController(@onNull RoutingSessionInfo sessionInfo)2350         SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) {
2351             super(sessionInfo);
2352         }
2353 
2354         @Override
isReleased()2355         public boolean isReleased() {
2356             // SystemRoutingController will never be released
2357             return false;
2358         }
2359 
2360         @Override
scheduleRelease()2361         boolean scheduleRelease() {
2362             // SystemRoutingController can be always transferred
2363             return true;
2364         }
2365 
2366         @Override
releaseInternal(boolean shouldReleaseSession)2367         void releaseInternal(boolean shouldReleaseSession) {
2368             // Do nothing. SystemRoutingController will never be released
2369         }
2370     }
2371 
2372     static final class RouteCallbackRecord {
2373         public final Executor mExecutor;
2374         public final RouteCallback mRouteCallback;
2375         public final RouteDiscoveryPreference mPreference;
2376 
RouteCallbackRecord( @ullable Executor executor, @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference)2377         RouteCallbackRecord(
2378                 @Nullable Executor executor,
2379                 @NonNull RouteCallback routeCallback,
2380                 @Nullable RouteDiscoveryPreference preference) {
2381             mRouteCallback = routeCallback;
2382             mExecutor = executor;
2383             mPreference = preference;
2384         }
2385 
2386         @Override
equals(Object obj)2387         public boolean equals(Object obj) {
2388             if (this == obj) {
2389                 return true;
2390             }
2391             if (!(obj instanceof RouteCallbackRecord)) {
2392                 return false;
2393             }
2394             return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback;
2395         }
2396 
2397         @Override
hashCode()2398         public int hashCode() {
2399             return mRouteCallback.hashCode();
2400         }
2401     }
2402 
2403     private static final class RouteListingPreferenceCallbackRecord {
2404         public final Executor mExecutor;
2405         public final Consumer<RouteListingPreference> mRouteListingPreferenceCallback;
2406 
RouteListingPreferenceCallbackRecord( @onNull Executor executor, @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback)2407         /* package */ RouteListingPreferenceCallbackRecord(
2408                 @NonNull Executor executor,
2409                 @NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) {
2410             mExecutor = executor;
2411             mRouteListingPreferenceCallback = routeListingPreferenceCallback;
2412         }
2413 
2414         @Override
equals(Object obj)2415         public boolean equals(Object obj) {
2416             if (this == obj) {
2417                 return true;
2418             }
2419             if (!(obj instanceof RouteListingPreferenceCallbackRecord)) {
2420                 return false;
2421             }
2422             return mRouteListingPreferenceCallback
2423                     == ((RouteListingPreferenceCallbackRecord) obj).mRouteListingPreferenceCallback;
2424         }
2425 
2426         @Override
hashCode()2427         public int hashCode() {
2428             return mRouteListingPreferenceCallback.hashCode();
2429         }
2430     }
2431 
2432     private static final class DeviceSuggestionsCallbackRecord {
2433         public final Executor mExecutor;
2434         public final DeviceSuggestionsCallback mDeviceSuggestionsCallback;
2435 
DeviceSuggestionsCallbackRecord( @onNull Executor executor, @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback)2436         /* package */ DeviceSuggestionsCallbackRecord(
2437                 @NonNull Executor executor,
2438                 @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
2439             mExecutor = executor;
2440             mDeviceSuggestionsCallback = deviceSuggestionsCallback;
2441         }
2442 
2443         @Override
equals(Object obj)2444         public boolean equals(Object obj) {
2445             if (this == obj) {
2446                 return true;
2447             }
2448             if (!(obj instanceof DeviceSuggestionsCallbackRecord)) {
2449                 return false;
2450             }
2451             return mDeviceSuggestionsCallback
2452                     == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback;
2453         }
2454 
2455         @Override
hashCode()2456         public int hashCode() {
2457             return mDeviceSuggestionsCallback.hashCode();
2458         }
2459     }
2460 
2461     static final class TransferCallbackRecord {
2462         public final Executor mExecutor;
2463         public final TransferCallback mTransferCallback;
2464 
TransferCallbackRecord( @onNull Executor executor, @NonNull TransferCallback transferCallback)2465         TransferCallbackRecord(
2466                 @NonNull Executor executor, @NonNull TransferCallback transferCallback) {
2467             mTransferCallback = transferCallback;
2468             mExecutor = executor;
2469         }
2470 
2471         @Override
equals(Object obj)2472         public boolean equals(Object obj) {
2473             if (this == obj) {
2474                 return true;
2475             }
2476             if (!(obj instanceof TransferCallbackRecord)) {
2477                 return false;
2478             }
2479             return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback;
2480         }
2481 
2482         @Override
hashCode()2483         public int hashCode() {
2484             return mTransferCallback.hashCode();
2485         }
2486     }
2487 
2488     static final class ControllerCallbackRecord {
2489         public final Executor mExecutor;
2490         public final ControllerCallback mCallback;
2491 
ControllerCallbackRecord( @ullable Executor executor, @NonNull ControllerCallback callback)2492         ControllerCallbackRecord(
2493                 @Nullable Executor executor, @NonNull ControllerCallback callback) {
2494             mCallback = callback;
2495             mExecutor = executor;
2496         }
2497 
2498         @Override
equals(Object obj)2499         public boolean equals(Object obj) {
2500             if (this == obj) {
2501                 return true;
2502             }
2503             if (!(obj instanceof ControllerCallbackRecord)) {
2504                 return false;
2505             }
2506             return mCallback == ((ControllerCallbackRecord) obj).mCallback;
2507         }
2508 
2509         @Override
hashCode()2510         public int hashCode() {
2511             return mCallback.hashCode();
2512         }
2513     }
2514 
2515     static final class ControllerCreationRequest {
2516         public final int mRequestId;
2517         public final long mManagerRequestId;
2518         public final MediaRoute2Info mRoute;
2519         public final RoutingController mOldController;
2520 
ControllerCreationRequest( int requestId, long managerRequestId, @NonNull MediaRoute2Info route, @NonNull RoutingController oldController)2521         ControllerCreationRequest(
2522                 int requestId,
2523                 long managerRequestId,
2524                 @NonNull MediaRoute2Info route,
2525                 @NonNull RoutingController oldController) {
2526             mRequestId = requestId;
2527             mManagerRequestId = managerRequestId;
2528             mRoute = Objects.requireNonNull(route, "route must not be null");
2529             mOldController =
2530                     Objects.requireNonNull(oldController, "oldController must not be null");
2531         }
2532     }
2533 
2534     class MediaRouter2Stub extends IMediaRouter2.Stub {
2535         @Override
notifyRouterRegistered( List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo)2536         public void notifyRouterRegistered(
2537                 List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
2538             mHandler.sendMessage(
2539                     obtainMessage(
2540                             MediaRouter2::syncRoutesOnHandler,
2541                             MediaRouter2.this,
2542                             currentRoutes,
2543                             currentSystemSessionInfo));
2544         }
2545 
2546         @Override
notifyRoutesUpdated(List<MediaRoute2Info> routes)2547         public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
2548             mHandler.sendMessage(
2549                     obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));
2550         }
2551 
2552         @Override
notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo)2553         public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
2554             mHandler.sendMessage(
2555                     obtainMessage(
2556                             MediaRouter2::createControllerOnHandler,
2557                             MediaRouter2.this,
2558                             requestId,
2559                             sessionInfo));
2560         }
2561 
2562         @Override
notifySessionInfoChanged(@ullable RoutingSessionInfo sessionInfo)2563         public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) {
2564             mHandler.sendMessage(
2565                     obtainMessage(
2566                             MediaRouter2::updateControllerOnHandler,
2567                             MediaRouter2.this,
2568                             sessionInfo));
2569         }
2570 
2571         @Override
notifySessionReleased(RoutingSessionInfo sessionInfo)2572         public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
2573             mHandler.sendMessage(
2574                     obtainMessage(
2575                             MediaRouter2::releaseControllerOnHandler,
2576                             MediaRouter2.this,
2577                             sessionInfo));
2578         }
2579 
2580         @Override
notifyDeviceSuggestionsUpdated( String suggestingPackageName, List<SuggestedDeviceInfo> suggestions)2581         public void notifyDeviceSuggestionsUpdated(
2582                 String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) {
2583             mHandler.sendMessage(
2584                     obtainMessage(
2585                             MediaRouter2::notifyDeviceSuggestionsUpdated,
2586                             MediaRouter2.this,
2587                             suggestingPackageName,
2588                             suggestions));
2589         }
2590 
2591         @Override
requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route)2592         public void requestCreateSessionByManager(
2593                 long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
2594             mHandler.sendMessage(
2595                     obtainMessage(
2596                             MediaRouter2::onRequestCreateControllerByManagerOnHandler,
2597                             MediaRouter2.this,
2598                             oldSession,
2599                             route,
2600                             managerRequestId));
2601         }
2602     }
2603 
2604     /**
2605      * Provides a common interface for separating {@link LocalMediaRouter2Impl local} and {@link
2606      * ProxyMediaRouter2Impl proxy} {@link MediaRouter2} instances.
2607      */
2608     private interface MediaRouter2Impl {
2609 
updateScanningState(@canningState int scanningState)2610         void updateScanningState(@ScanningState int scanningState) throws RemoteException;
2611 
startScan()2612         void startScan();
2613 
stopScan()2614         void stopScan();
2615 
getClientPackageName()2616         String getClientPackageName();
2617 
getPackageName()2618         String getPackageName();
2619 
getSystemSessionInfo()2620         RoutingSessionInfo getSystemSessionInfo();
2621 
createRouteCallbackRecord( @onNull @allbackExecutor Executor executor, @NonNull RouteCallback routeCallback, @NonNull RouteDiscoveryPreference preference)2622         RouteCallbackRecord createRouteCallbackRecord(
2623                 @NonNull @CallbackExecutor Executor executor,
2624                 @NonNull RouteCallback routeCallback,
2625                 @NonNull RouteDiscoveryPreference preference);
2626 
registerRouteCallback()2627         void registerRouteCallback();
2628 
unregisterRouteCallback()2629         void unregisterRouteCallback();
2630 
setRouteListingPreference(@ullable RouteListingPreference preference)2631         void setRouteListingPreference(@Nullable RouteListingPreference preference);
2632 
setDeviceSuggestions(@ullable List<SuggestedDeviceInfo> suggestedDeviceInfo)2633         void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
2634 
2635         @Nullable
getDeviceSuggestions()2636         Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions();
2637 
showSystemOutputSwitcher()2638         boolean showSystemOutputSwitcher();
2639 
getAllRoutes()2640         List<MediaRoute2Info> getAllRoutes();
2641 
setOnGetControllerHintsListener(OnGetControllerHintsListener listener)2642         void setOnGetControllerHintsListener(OnGetControllerHintsListener listener);
2643 
transferTo(MediaRoute2Info route)2644         void transferTo(MediaRoute2Info route);
2645 
stop()2646         void stop();
2647 
transfer(@onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)2648         void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route);
2649 
getControllers()2650         List<RoutingController> getControllers();
2651 
setRouteVolume(MediaRoute2Info route, int volume)2652         void setRouteVolume(MediaRoute2Info route, int volume);
2653 
filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)2654         List<MediaRoute2Info> filterRoutesWithIndividualPreference(
2655                 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference);
2656 
2657         // RoutingController methods.
setSessionVolume(int volume, RoutingSessionInfo sessionInfo)2658         void setSessionVolume(int volume, RoutingSessionInfo sessionInfo);
2659 
selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2660         void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo);
2661 
deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)2662         void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo);
2663 
releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)2664         void releaseSession(
2665                 boolean shouldReleaseSession,
2666                 boolean shouldNotifyStop,
2667                 RoutingController controller);
2668 
2669         /**
2670          * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app
2671          * associated with this router.
2672          */
wasTransferredBySelf(RoutingSessionInfo sessionInfo)2673         boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo);
2674     }
2675 
2676     /**
2677      * Implements logic specific to proxy {@link MediaRouter2} instances.
2678      *
2679      * <p>A proxy {@link MediaRouter2} instance controls the routing of a different package and can
2680      * be obtained by calling {@link #getInstance(Context, String)}. This requires {@link
2681      * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission.
2682      *
2683      * <p>Proxy routers behave differently than local routers. See {@link #getInstance(Context,
2684      * String)} for more details.
2685      */
2686     private class ProxyMediaRouter2Impl implements MediaRouter2Impl {
2687         // Fields originating from MediaRouter2Manager.
2688         private final IMediaRouter2Manager.Stub mClient;
2689         private final CopyOnWriteArrayList<MediaRouter2Manager.TransferRequest>
2690                 mTransferRequests = new CopyOnWriteArrayList<>();
2691         private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
2692 
2693         // Fields originating from MediaRouter2.
2694         @NonNull private final String mClientPackageName;
2695         @NonNull private final UserHandle mClientUser;
2696         private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false);
2697 
2698         @GuardedBy("mLock")
2699         private final List<InstanceInvalidatedCallbackRecord> mInstanceInvalidatedCallbackRecords =
2700                 new ArrayList<>();
2701 
ProxyMediaRouter2Impl( @onNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user)2702         ProxyMediaRouter2Impl(
2703                 @NonNull Context context,
2704                 @NonNull String clientPackageName,
2705                 @NonNull UserHandle user) {
2706             mClientUser = user;
2707             mClientPackageName = clientPackageName;
2708             mClient = new Client();
2709             mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
2710         }
2711 
registerProxyRouter()2712         public void registerProxyRouter() {
2713             try {
2714                 mMediaRouterService.registerProxyRouter(
2715                         mClient,
2716                         mContext.getApplicationContext().getPackageName(),
2717                         mClientPackageName,
2718                         mClientUser);
2719             } catch (RemoteException ex) {
2720                 throw ex.rethrowFromSystemServer();
2721             }
2722         }
2723 
registerInstanceInvalidatedCallback( @ullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener)2724         public void registerInstanceInvalidatedCallback(
2725                 @Nullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener) {
2726             if (executor == null || onInstanceInvalidatedListener == null) {
2727                 return;
2728             }
2729 
2730             InstanceInvalidatedCallbackRecord record =
2731                     new InstanceInvalidatedCallbackRecord(executor, onInstanceInvalidatedListener);
2732             synchronized (mLock) {
2733                 if (!mInstanceInvalidatedCallbackRecords.contains(record)) {
2734                     mInstanceInvalidatedCallbackRecords.add(record);
2735                 }
2736             }
2737         }
2738 
2739         @Override
updateScanningState(int scanningState)2740         public void updateScanningState(int scanningState) throws RemoteException {
2741             mMediaRouterService.updateScanningState(mClient, scanningState);
2742         }
2743 
2744         @Override
startScan()2745         public void startScan() {
2746             if (!mIsScanning.getAndSet(true)) {
2747                 if (mScanRequestCount.getAndIncrement() == 0) {
2748                     try {
2749                         mMediaRouterService.updateScanningState(
2750                                 mClient, SCANNING_STATE_WHILE_INTERACTIVE);
2751                     } catch (RemoteException ex) {
2752                         throw ex.rethrowFromSystemServer();
2753                     }
2754                 }
2755             }
2756         }
2757 
2758         @Override
stopScan()2759         public void stopScan() {
2760             if (mIsScanning.getAndSet(false)) {
2761                 if (mScanRequestCount.updateAndGet(
2762                                 count -> {
2763                                     if (count == 0) {
2764                                         throw new IllegalStateException(
2765                                                 "No active scan requests to unregister.");
2766                                     } else {
2767                                         return --count;
2768                                     }
2769                                 })
2770                         == 0) {
2771                     try {
2772                         mMediaRouterService.updateScanningState(
2773                                 mClient, SCANNING_STATE_NOT_SCANNING);
2774                     } catch (RemoteException ex) {
2775                         throw ex.rethrowFromSystemServer();
2776                     }
2777                 }
2778             }
2779         }
2780 
2781         @Override
getClientPackageName()2782         public String getClientPackageName() {
2783             return mClientPackageName;
2784         }
2785 
2786         /**
2787          * Returns {@code null}. This refers to the package name of the caller app, which is only
2788          * relevant for local routers.
2789          */
2790         @Override
getPackageName()2791         public String getPackageName() {
2792             return null;
2793         }
2794 
2795         @Override
getSystemSessionInfo()2796         public RoutingSessionInfo getSystemSessionInfo() {
2797             return getSystemSessionInfoImpl(
2798                     mMediaRouterService, mContext.getPackageName(), mClientPackageName);
2799         }
2800 
2801         /**
2802          * {@link RouteDiscoveryPreference Discovery preferences} are ignored for proxy routers, as
2803          * their callbacks should receive events related to the media app's preferences. This is
2804          * equivalent to setting {@link RouteDiscoveryPreference#EMPTY empty preferences}.
2805          */
2806         @Override
createRouteCallbackRecord( Executor executor, RouteCallback routeCallback, RouteDiscoveryPreference preference)2807         public RouteCallbackRecord createRouteCallbackRecord(
2808                 Executor executor,
2809                 RouteCallback routeCallback,
2810                 RouteDiscoveryPreference preference) {
2811             return new RouteCallbackRecord(executor, routeCallback, RouteDiscoveryPreference.EMPTY);
2812         }
2813 
2814         /**
2815          * No-op. Only local routers communicate directly with {@link
2816          * com.android.server.media.MediaRouter2ServiceImpl MediaRouter2ServiceImpl} and modify
2817          * {@link RouteDiscoveryPreference}. Proxy routers receive callbacks from {@link
2818          * MediaRouter2Manager}.
2819          */
2820         @Override
registerRouteCallback()2821         public void registerRouteCallback() {
2822             // Do nothing.
2823         }
2824 
2825         /** No-op. See {@link ProxyMediaRouter2Impl#registerRouteCallback()}. */
2826         @Override
unregisterRouteCallback()2827         public void unregisterRouteCallback() {
2828             // Do nothing.
2829         }
2830 
2831         @Override
setRouteListingPreference(@ullable RouteListingPreference preference)2832         public void setRouteListingPreference(@Nullable RouteListingPreference preference) {
2833             throw new UnsupportedOperationException(
2834                     "RouteListingPreference cannot be set by a proxy MediaRouter2 instance.");
2835         }
2836 
2837         @Override
setDeviceSuggestions(@ullable List<SuggestedDeviceInfo> suggestedDeviceInfo)2838         public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
2839             synchronized (mLock) {
2840                 try {
2841                     mMediaRouterService.setDeviceSuggestionsWithManager(
2842                             mClient, suggestedDeviceInfo);
2843                 } catch (RemoteException ex) {
2844                     ex.rethrowFromSystemServer();
2845                 }
2846             }
2847         }
2848 
2849         @Override
getDeviceSuggestions()2850         public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
2851             synchronized (mLock) {
2852                 try {
2853                     return mMediaRouterService.getDeviceSuggestionsWithManager(mClient);
2854                 } catch (RemoteException ex) {
2855                     throw ex.rethrowFromSystemServer();
2856                 }
2857             }
2858         }
2859 
2860         @Override
showSystemOutputSwitcher()2861         public boolean showSystemOutputSwitcher() {
2862             try {
2863                 return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient);
2864             } catch (RemoteException ex) {
2865                 throw ex.rethrowFromSystemServer();
2866             }
2867         }
2868 
2869         /** Gets the list of all discovered routes. */
2870         @Override
getAllRoutes()2871         public List<MediaRoute2Info> getAllRoutes() {
2872             synchronized (mLock) {
2873                 return new ArrayList<>(mRoutes.values());
2874             }
2875         }
2876 
2877         /** No-op. Controller hints can only be provided by the media app through a local router. */
2878         @Override
setOnGetControllerHintsListener(OnGetControllerHintsListener listener)2879         public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) {
2880             // Do nothing.
2881         }
2882 
2883         /**
2884          * Transfers the current {@link RoutingSessionInfo routing session} associated with the
2885          * router's {@link #mClientPackageName client package name} to a specified {@link
2886          * MediaRoute2Info route}.
2887          *
2888          * <p>This method is equivalent to {@link #transfer(RoutingSessionInfo, MediaRoute2Info)},
2889          * except that the {@link RoutingSessionInfo routing session} is resolved based on the
2890          * router's {@link #mClientPackageName client package name}.
2891          *
2892          * @param route The route to transfer to.
2893          */
2894         @Override
transferTo(MediaRoute2Info route)2895         public void transferTo(MediaRoute2Info route) {
2896             Objects.requireNonNull(route, "route must not be null");
2897 
2898             List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
2899             RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
2900             transfer(targetSession, route);
2901         }
2902 
2903         @Override
stop()2904         public void stop() {
2905             List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
2906             RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1);
2907             releaseSession(sessionToRelease);
2908         }
2909 
2910         /**
2911          * Transfers a {@link RoutingSessionInfo routing session} to a {@link MediaRoute2Info
2912          * route}.
2913          *
2914          * <p>{@link #onTransferred} is called on success or {@link #onTransferFailed} is called if
2915          * the request fails.
2916          *
2917          * <p>This method will default for in-session transfer if the {@link MediaRoute2Info route}
2918          * is a {@link RoutingSessionInfo#getTransferableRoutes() transferable route}. Otherwise, it
2919          * will attempt an out-of-session transfer.
2920          *
2921          * @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
2922          * @param route The {@link MediaRoute2Info route} to transfer to.
2923          * @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
2924          * @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
2925          */
2926         @Override
2927         @SuppressWarnings("AndroidFrameworkRequiresPermission")
transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)2928         public void transfer(
2929                 @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
2930             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
2931             Objects.requireNonNull(route, "route must not be null");
2932 
2933             Log.v(
2934                     TAG,
2935                     "Transferring routing session. session= " + sessionInfo + ", route=" + route);
2936 
2937             boolean isUnknownRoute;
2938             synchronized (mLock) {
2939                 isUnknownRoute = !mRoutes.containsKey(route.getId());
2940             }
2941 
2942             if (isUnknownRoute) {
2943                 Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId());
2944                 this.onTransferFailed(sessionInfo, route);
2945                 return;
2946             }
2947 
2948             // If this call is trying to transfer to a selected system route, we let them
2949             // through as a provider driven transfer in order to update the transfer reason and
2950             // initiator data.
2951             boolean isSystemRouteReselection =
2952                     Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
2953                             && sessionInfo.isSystemSession()
2954                             && route.isSystemRoute()
2955                             && sessionInfo.getSelectedRoutes().contains(route.getId());
2956             if (sessionInfo.getTransferableRoutes().contains(route.getId())
2957                     || isSystemRouteReselection) {
2958                 transferToRoute(sessionInfo, route, mClientUser, mClientPackageName);
2959             } else {
2960                 requestCreateSession(sessionInfo, route);
2961             }
2962         }
2963 
2964         /**
2965          * Requests an in-session transfer of a {@link RoutingSessionInfo routing session} to a
2966          * {@link MediaRoute2Info route}.
2967          *
2968          * <p>The provided {@link MediaRoute2Info route} must be listed in the {@link
2969          * RoutingSessionInfo routing session's} {@link RoutingSessionInfo#getTransferableRoutes()
2970          * transferable routes list}. Otherwise, the request will fail.
2971          *
2972          * <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request an
2973          * out-of-session transfer.
2974          *
2975          * @param session The {@link RoutingSessionInfo routing session} to transfer.
2976          * @param route The {@link MediaRoute2Info route} to transfer to. Must be one of the {@link
2977          *     RoutingSessionInfo routing session's} {@link
2978          *     RoutingSessionInfo#getTransferableRoutes() transferable routes}.
2979          */
2980         @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
transferToRoute( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route, @NonNull UserHandle transferInitiatorUserHandle, @NonNull String transferInitiatorPackageName)2981         private void transferToRoute(
2982                 @NonNull RoutingSessionInfo session,
2983                 @NonNull MediaRoute2Info route,
2984                 @NonNull UserHandle transferInitiatorUserHandle,
2985                 @NonNull String transferInitiatorPackageName) {
2986             int requestId = createTransferRequest(session, route);
2987 
2988             try {
2989                 mMediaRouterService.transferToRouteWithManager(
2990                         mClient,
2991                         requestId,
2992                         session.getId(),
2993                         route,
2994                         transferInitiatorUserHandle,
2995                         transferInitiatorPackageName);
2996             } catch (RemoteException ex) {
2997                 throw ex.rethrowFromSystemServer();
2998             }
2999         }
3000 
3001         /**
3002          * Requests an out-of-session transfer of a {@link RoutingSessionInfo routing session} to a
3003          * {@link MediaRoute2Info route}.
3004          *
3005          * <p>This request creates a new {@link RoutingSessionInfo routing session} regardless of
3006          * whether the {@link MediaRoute2Info route} is one of the {@link RoutingSessionInfo current
3007          * session's} {@link RoutingSessionInfo#getTransferableRoutes() transferable routes}.
3008          *
3009          * <p>Use {@link #transferToRoute(RoutingSessionInfo, MediaRoute2Info)} to request an
3010          * in-session transfer.
3011          *
3012          * @param oldSession The {@link RoutingSessionInfo routing session} to transfer.
3013          * @param route The {@link MediaRoute2Info route} to transfer to.
3014          */
requestCreateSession( @onNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route)3015         private void requestCreateSession(
3016                 @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) {
3017             if (TextUtils.isEmpty(oldSession.getClientPackageName())) {
3018                 Log.w(TAG, "requestCreateSession: Can't create a session without package name.");
3019                 this.onTransferFailed(oldSession, route);
3020                 return;
3021             }
3022 
3023             int requestId = createTransferRequest(oldSession, route);
3024 
3025             try {
3026                 mMediaRouterService.requestCreateSessionWithManager(
3027                         mClient, requestId, oldSession, route);
3028             } catch (RemoteException ex) {
3029                 throw ex.rethrowFromSystemServer();
3030             }
3031         }
3032 
3033         @Override
getControllers()3034         public List<RoutingController> getControllers() {
3035             List<RoutingController> result = new ArrayList<>();
3036 
3037             /* Unlike local MediaRouter2 instances, controller instances cannot be kept because
3038             transfer events initiated from other apps will not come through manager.*/
3039             List<RoutingSessionInfo> sessions = getRoutingSessions();
3040             for (RoutingSessionInfo session : sessions) {
3041                 RoutingController controller;
3042                 if (session.isSystemSession()) {
3043                     mSystemController.setRoutingSessionInfo(session);
3044                     controller = mSystemController;
3045                 } else {
3046                     controller = new RoutingController(session);
3047                 }
3048                 result.add(controller);
3049             }
3050             return result;
3051         }
3052 
3053         /**
3054          * Requests a volume change for a {@link MediaRoute2Info route}.
3055          *
3056          * <p>It may have no effect if the {@link MediaRoute2Info route} is not currently selected.
3057          *
3058          * @param volume The desired volume value between 0 and {@link
3059          *     MediaRoute2Info#getVolumeMax()} (inclusive).
3060          */
3061         @Override
setRouteVolume(@onNull MediaRoute2Info route, int volume)3062         public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
3063             if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
3064                 Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring.");
3065                 return;
3066             }
3067             if (volume < 0 || volume > route.getVolumeMax()) {
3068                 Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring");
3069                 return;
3070             }
3071 
3072             try {
3073                 int requestId = mNextRequestId.getAndIncrement();
3074                 mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
3075             } catch (RemoteException ex) {
3076                 throw ex.rethrowFromSystemServer();
3077             }
3078         }
3079 
3080         /**
3081          * Requests a volume change for a {@link RoutingSessionInfo routing session}.
3082          *
3083          * @param volume The desired volume value between 0 and {@link
3084          *     RoutingSessionInfo#getVolumeMax()} (inclusive).
3085          */
3086         @Override
setSessionVolume(int volume, RoutingSessionInfo sessionInfo)3087         public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) {
3088             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
3089 
3090             if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
3091                 Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring.");
3092                 return;
3093             }
3094             if (volume < 0 || volume > sessionInfo.getVolumeMax()) {
3095                 Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring");
3096                 return;
3097             }
3098 
3099             try {
3100                 int requestId = mNextRequestId.getAndIncrement();
3101                 mMediaRouterService.setSessionVolumeWithManager(
3102                         mClient, requestId, sessionInfo.getId(), volume);
3103             } catch (RemoteException ex) {
3104                 throw ex.rethrowFromSystemServer();
3105             }
3106         }
3107 
3108         /**
3109          * Returns an exact copy of the routes. Individual {@link RouteDiscoveryPreference
3110          * preferences} do not apply to proxy routers.
3111          */
3112         @Override
filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)3113         public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
3114                 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
3115             // Individual discovery preferences do not apply for the system router.
3116             return new ArrayList<>(routes);
3117         }
3118 
3119         /**
3120          * Adds a {@linkplain MediaRoute2Info route} to the routing session's {@linkplain
3121          * RoutingSessionInfo#getSelectedRoutes() selected route list}.
3122          *
3123          * <p>Upon success, {@link #onSessionUpdated(RoutingSessionInfo)} is invoked. Failed
3124          * requests are silently ignored.
3125          *
3126          * <p>The {@linkplain RoutingSessionInfo#getSelectedRoutes() selected routes list} of a
3127          * routing session contains the group of devices playing media for that {@linkplain
3128          * RoutingSessionInfo session}.
3129          *
3130          * <p>The given route must not be already selected and must be listed in the session's
3131          * {@linkplain RoutingSessionInfo#getSelectableRoutes() selectable routes}. Otherwise, the
3132          * request will be ignored.
3133          *
3134          * <p>This method should not be confused with {@link #transfer(RoutingSessionInfo,
3135          * MediaRoute2Info)}.
3136          *
3137          * @see RoutingSessionInfo#getSelectedRoutes()
3138          * @see RoutingSessionInfo#getSelectableRoutes()
3139          */
3140         @Override
selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3141         public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
3142             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
3143             Objects.requireNonNull(route, "route must not be null");
3144 
3145             if (sessionInfo.getSelectedRoutes().contains(route.getId())) {
3146                 Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
3147                 return;
3148             }
3149 
3150             if (!sessionInfo.getSelectableRoutes().contains(route.getId())) {
3151                 Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
3152                 return;
3153             }
3154 
3155             try {
3156                 int requestId = mNextRequestId.getAndIncrement();
3157                 mMediaRouterService.selectRouteWithManager(
3158                         mClient, requestId, sessionInfo.getId(), route);
3159             } catch (RemoteException ex) {
3160                 throw ex.rethrowFromSystemServer();
3161             }
3162         }
3163 
3164         /**
3165          * Removes a route from a session's {@linkplain RoutingSessionInfo#getSelectedRoutes()
3166          * selected routes list}. Calls {@link #onSessionUpdated(RoutingSessionInfo)} on success.
3167          *
3168          * <p>The given route must be selected and must be listed in the session's {@linkplain
3169          * RoutingSessionInfo#getDeselectableRoutes() deselectable route list}. Otherwise, the
3170          * request will be ignored.
3171          *
3172          * @see RoutingSessionInfo#getSelectedRoutes()
3173          * @see RoutingSessionInfo#getDeselectableRoutes()
3174          */
3175         @Override
deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3176         public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
3177             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
3178             Objects.requireNonNull(route, "route must not be null");
3179 
3180             if (!sessionInfo.getSelectedRoutes().contains(route.getId())) {
3181                 Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
3182                 return;
3183             }
3184 
3185             if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) {
3186                 Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
3187                 return;
3188             }
3189 
3190             try {
3191                 int requestId = mNextRequestId.getAndIncrement();
3192                 mMediaRouterService.deselectRouteWithManager(
3193                         mClient, requestId, sessionInfo.getId(), route);
3194             } catch (RemoteException ex) {
3195                 throw ex.rethrowFromSystemServer();
3196             }
3197         }
3198 
3199         @Override
releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)3200         public void releaseSession(
3201                 boolean shouldReleaseSession,
3202                 boolean shouldNotifyStop,
3203                 RoutingController controller) {
3204             releaseSession(controller.getRoutingSessionInfo());
3205         }
3206 
3207         @Override
wasTransferredBySelf(RoutingSessionInfo sessionInfo)3208         public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
3209             UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
3210             String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
3211             return Objects.equals(mClientUser, transferInitiatorUserHandle)
3212                     && Objects.equals(mClientPackageName, transferInitiatorPackageName);
3213         }
3214 
3215         /**
3216          * Retrieves the system session info for the given package.
3217          *
3218          * <p>The returned routing session is guaranteed to have a non-null {@link
3219          * RoutingSessionInfo#getClientPackageName() client package name}.
3220          *
3221          * <p>Extracted into a static method to allow calling this from the constructor.
3222          */
getSystemSessionInfoImpl( @onNull IMediaRouterService service, @NonNull String callerPackageName, @NonNull String clientPackageName)3223         /* package */ static RoutingSessionInfo getSystemSessionInfoImpl(
3224                 @NonNull IMediaRouterService service,
3225                 @NonNull String callerPackageName,
3226                 @NonNull String clientPackageName) {
3227             try {
3228                 return service.getSystemSessionInfoForPackage(callerPackageName, clientPackageName);
3229             } catch (RemoteException ex) {
3230                 throw ex.rethrowFromSystemServer();
3231             }
3232         }
3233 
3234         /**
3235          * Requests the release of a {@linkplain RoutingSessionInfo routing session}. Calls {@link
3236          * #onSessionReleasedOnHandler(RoutingSessionInfo)} on success.
3237          *
3238          * <p>Once released, a routing session ignores incoming requests.
3239          */
releaseSession(@onNull RoutingSessionInfo sessionInfo)3240         private void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
3241             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
3242 
3243             try {
3244                 int requestId = mNextRequestId.getAndIncrement();
3245                 mMediaRouterService.releaseSessionWithManager(
3246                         mClient, requestId, sessionInfo.getId());
3247             } catch (RemoteException ex) {
3248                 throw ex.rethrowFromSystemServer();
3249             }
3250         }
3251 
createTransferRequest( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)3252         private int createTransferRequest(
3253                 @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
3254             int requestId = mNextRequestId.getAndIncrement();
3255             MediaRouter2Manager.TransferRequest transferRequest =
3256                     new MediaRouter2Manager.TransferRequest(requestId, session, route);
3257             mTransferRequests.add(transferRequest);
3258 
3259             Message timeoutMessage =
3260                     obtainMessage(
3261                             ProxyMediaRouter2Impl::handleTransferTimeout, this, transferRequest);
3262             mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS);
3263             return requestId;
3264         }
3265 
handleTransferTimeout(MediaRouter2Manager.TransferRequest request)3266         private void handleTransferTimeout(MediaRouter2Manager.TransferRequest request) {
3267             boolean removed = mTransferRequests.remove(request);
3268             if (removed) {
3269                 this.onTransferFailed(request.mOldSessionInfo, request.mTargetRoute);
3270             }
3271         }
3272 
3273         /**
3274          * Returns the {@linkplain RoutingSessionInfo routing sessions} associated with {@link
3275          * #mClientPackageName}. The first element of the returned list is the {@linkplain
3276          * #getSystemSessionInfo() system routing session}.
3277          *
3278          * @see #getSystemSessionInfo()
3279          */
3280         @NonNull
getRoutingSessions()3281         private List<RoutingSessionInfo> getRoutingSessions() {
3282             List<RoutingSessionInfo> sessions = new ArrayList<>();
3283             sessions.add(getSystemSessionInfo());
3284 
3285             List<RoutingSessionInfo> remoteSessions;
3286             try {
3287                 remoteSessions = mMediaRouterService.getRemoteSessions(mClient);
3288             } catch (RemoteException ex) {
3289                 throw ex.rethrowFromSystemServer();
3290             }
3291 
3292             for (RoutingSessionInfo sessionInfo : remoteSessions) {
3293                 if (TextUtils.equals(sessionInfo.getClientPackageName(), mClientPackageName)) {
3294                     sessions.add(sessionInfo);
3295                 }
3296             }
3297             return sessions;
3298         }
3299 
onTransferred( @onNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession)3300         private void onTransferred(
3301                 @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) {
3302             if (!isSessionRelatedToTargetPackageName(oldSession)
3303                     || !isSessionRelatedToTargetPackageName(newSession)) {
3304                 return;
3305             }
3306 
3307             RoutingController oldController;
3308             if (oldSession.isSystemSession()) {
3309                 mSystemController.setRoutingSessionInfo(
3310                         ensureClientPackageNameForSystemSession(oldSession, mClientPackageName));
3311                 oldController = mSystemController;
3312             } else {
3313                 oldController = new RoutingController(oldSession);
3314             }
3315 
3316             RoutingController newController;
3317             if (newSession.isSystemSession()) {
3318                 mSystemController.setRoutingSessionInfo(
3319                         ensureClientPackageNameForSystemSession(newSession, mClientPackageName));
3320                 newController = mSystemController;
3321             } else {
3322                 newController = new RoutingController(newSession);
3323             }
3324 
3325             notifyTransfer(oldController, newController);
3326         }
3327 
onTransferFailed( @onNull RoutingSessionInfo session, @NonNull MediaRoute2Info route)3328         private void onTransferFailed(
3329                 @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
3330             if (!isSessionRelatedToTargetPackageName(session)) {
3331                 return;
3332             }
3333             notifyTransferFailure(route);
3334         }
3335 
onSessionUpdated(@onNull RoutingSessionInfo session)3336         private void onSessionUpdated(@NonNull RoutingSessionInfo session) {
3337             if (!isSessionRelatedToTargetPackageName(session)) {
3338                 return;
3339             }
3340 
3341             RoutingController controller;
3342             if (session.isSystemSession()) {
3343                 mSystemController.setRoutingSessionInfo(
3344                         ensureClientPackageNameForSystemSession(session, mClientPackageName));
3345                 controller = mSystemController;
3346             } else {
3347                 controller = new RoutingController(session);
3348             }
3349             notifyControllerUpdated(controller);
3350         }
3351 
3352         /**
3353          * Returns {@code true} if the session is a system session or if its client package name
3354          * matches the proxy router's target package name.
3355          */
isSessionRelatedToTargetPackageName(@onNull RoutingSessionInfo session)3356         private boolean isSessionRelatedToTargetPackageName(@NonNull RoutingSessionInfo session) {
3357             return session.isSystemSession()
3358                     || TextUtils.equals(getClientPackageName(), session.getClientPackageName());
3359         }
3360 
onSessionCreatedOnHandler( int requestId, @NonNull RoutingSessionInfo sessionInfo)3361         private void onSessionCreatedOnHandler(
3362                 int requestId, @NonNull RoutingSessionInfo sessionInfo) {
3363             MediaRouter2Manager.TransferRequest matchingRequest = null;
3364             for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
3365                 if (request.mRequestId == requestId) {
3366                     matchingRequest = request;
3367                     break;
3368                 }
3369             }
3370 
3371             if (matchingRequest == null) {
3372                 return;
3373             }
3374 
3375             mTransferRequests.remove(matchingRequest);
3376 
3377             MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute;
3378 
3379             if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) {
3380                 Log.w(
3381                         TAG,
3382                         "The session does not contain the requested route. "
3383                                 + "(requestedRouteId="
3384                                 + requestedRoute.getId()
3385                                 + ", actualRoutes="
3386                                 + sessionInfo.getSelectedRoutes()
3387                                 + ")");
3388                 this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
3389             } else if (!TextUtils.equals(
3390                     requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
3391                 Log.w(
3392                         TAG,
3393                         "The session's provider ID does not match the requested route's. "
3394                                 + "(requested route's providerId="
3395                                 + requestedRoute.getProviderId()
3396                                 + ", actual providerId="
3397                                 + sessionInfo.getProviderId()
3398                                 + ")");
3399                 this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
3400             } else {
3401                 this.onTransferred(matchingRequest.mOldSessionInfo, sessionInfo);
3402             }
3403         }
3404 
onSessionUpdatedOnHandler(@onNull RoutingSessionInfo updatedSession)3405         private void onSessionUpdatedOnHandler(@NonNull RoutingSessionInfo updatedSession) {
3406             for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
3407                 String sessionId = request.mOldSessionInfo.getId();
3408                 if (!TextUtils.equals(sessionId, updatedSession.getId())) {
3409                     continue;
3410                 }
3411 
3412                 if (updatedSession.getSelectedRoutes().contains(request.mTargetRoute.getId())) {
3413                     mTransferRequests.remove(request);
3414                     break;
3415                 }
3416             }
3417             this.onSessionUpdated(updatedSession);
3418         }
3419 
onSessionReleasedOnHandler(@onNull RoutingSessionInfo session)3420         private void onSessionReleasedOnHandler(@NonNull RoutingSessionInfo session) {
3421             if (session.isSystemSession()) {
3422                 Log.e(TAG, "onSessionReleasedOnHandler: Called on system session. Ignoring.");
3423                 return;
3424             }
3425 
3426             if (!TextUtils.equals(getClientPackageName(), session.getClientPackageName())) {
3427                 return;
3428             }
3429 
3430             notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED));
3431         }
3432 
onDiscoveryPreferenceChangedOnHandler( @onNull String packageName, @Nullable RouteDiscoveryPreference preference)3433         private void onDiscoveryPreferenceChangedOnHandler(
3434                 @NonNull String packageName, @Nullable RouteDiscoveryPreference preference) {
3435             if (!TextUtils.equals(getClientPackageName(), packageName)) {
3436                 return;
3437             }
3438 
3439             if (preference == null) {
3440                 return;
3441             }
3442             synchronized (mLock) {
3443                 if (Objects.equals(preference, mDiscoveryPreference)) {
3444                     return;
3445                 }
3446                 mDiscoveryPreference = preference;
3447                 updateFilteredRoutesLocked();
3448             }
3449             notifyPreferredFeaturesChanged(preference.getPreferredFeatures());
3450         }
3451 
onRouteListingPreferenceChangedOnHandler( @onNull String packageName, @Nullable RouteListingPreference routeListingPreference)3452         private void onRouteListingPreferenceChangedOnHandler(
3453                 @NonNull String packageName,
3454                 @Nullable RouteListingPreference routeListingPreference) {
3455             if (!TextUtils.equals(getClientPackageName(), packageName)) {
3456                 return;
3457             }
3458 
3459             synchronized (mLock) {
3460                 if (Objects.equals(mRouteListingPreference, routeListingPreference)) {
3461                     return;
3462                 }
3463 
3464                 mRouteListingPreference = routeListingPreference;
3465             }
3466 
3467             notifyRouteListingPreferenceUpdated(routeListingPreference);
3468         }
3469 
onDeviceSuggestionsChangeHandler( @onNull String packageName, @NonNull String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo)3470         private void onDeviceSuggestionsChangeHandler(
3471                 @NonNull String packageName,
3472                 @NonNull String suggestingPackageName,
3473                 @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
3474             if (!TextUtils.equals(getClientPackageName(), packageName)) {
3475                 return;
3476             }
3477             synchronized (mLock) {
3478                 if (Objects.equals(
3479                         mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) {
3480                     return;
3481                 }
3482                 mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo);
3483             }
3484             notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo);
3485         }
3486 
onRequestFailedOnHandler(int requestId, int reason)3487         private void onRequestFailedOnHandler(int requestId, int reason) {
3488             MediaRouter2Manager.TransferRequest matchingRequest = null;
3489             for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
3490                 if (request.mRequestId == requestId) {
3491                     matchingRequest = request;
3492                     break;
3493                 }
3494             }
3495 
3496             if (matchingRequest != null) {
3497                 mTransferRequests.remove(matchingRequest);
3498                 onTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute);
3499             } else {
3500                 notifyRequestFailed(reason);
3501             }
3502         }
3503 
onInvalidateInstanceOnHandler()3504         private void onInvalidateInstanceOnHandler() {
3505             Log.w(
3506                     TAG,
3507                     "MEDIA_ROUTING_CONTROL has been revoked for this package. Invalidating"
3508                             + " instance.");
3509             // After this block, all following getInstance() calls should throw a SecurityException,
3510             // so no new onInstanceInvalidatedListeners can be registered to this instance.
3511             synchronized (sSystemRouterLock) {
3512                 PackageNameUserHandlePair key =
3513                         new PackageNameUserHandlePair(mClientPackageName, mClientUser);
3514                 sAppToProxyRouterMap.remove(key);
3515             }
3516 
3517             synchronized (mLock) {
3518                 for (InstanceInvalidatedCallbackRecord record :
3519                         mInstanceInvalidatedCallbackRecords) {
3520                     record.executor.execute(record.runnable);
3521                 }
3522             }
3523             mRouteCallbackRecords.clear();
3524             mControllerCallbackRecords.clear();
3525             mTransferCallbackRecords.clear();
3526         }
3527 
3528         private class Client extends IMediaRouter2Manager.Stub {
3529 
3530             @Override
notifySessionCreated(int requestId, RoutingSessionInfo routingSessionInfo)3531             public void notifySessionCreated(int requestId, RoutingSessionInfo routingSessionInfo) {
3532                 mHandler.sendMessage(
3533                         obtainMessage(
3534                                 ProxyMediaRouter2Impl::onSessionCreatedOnHandler,
3535                                 ProxyMediaRouter2Impl.this,
3536                                 requestId,
3537                                 routingSessionInfo));
3538             }
3539 
3540             @Override
notifySessionUpdated(RoutingSessionInfo routingSessionInfo)3541             public void notifySessionUpdated(RoutingSessionInfo routingSessionInfo) {
3542                 mHandler.sendMessage(
3543                         obtainMessage(
3544                                 ProxyMediaRouter2Impl::onSessionUpdatedOnHandler,
3545                                 ProxyMediaRouter2Impl.this,
3546                                 routingSessionInfo));
3547             }
3548 
3549             @Override
notifySessionReleased(RoutingSessionInfo routingSessionInfo)3550             public void notifySessionReleased(RoutingSessionInfo routingSessionInfo) {
3551                 mHandler.sendMessage(
3552                         obtainMessage(
3553                                 ProxyMediaRouter2Impl::onSessionReleasedOnHandler,
3554                                 ProxyMediaRouter2Impl.this,
3555                                 routingSessionInfo));
3556             }
3557 
3558             @Override
notifyDiscoveryPreferenceChanged( String packageName, RouteDiscoveryPreference routeDiscoveryPreference)3559             public void notifyDiscoveryPreferenceChanged(
3560                     String packageName, RouteDiscoveryPreference routeDiscoveryPreference) {
3561                 mHandler.sendMessage(
3562                         obtainMessage(
3563                                 ProxyMediaRouter2Impl::onDiscoveryPreferenceChangedOnHandler,
3564                                 ProxyMediaRouter2Impl.this,
3565                                 packageName,
3566                                 routeDiscoveryPreference));
3567             }
3568 
3569             @Override
notifyRouteListingPreferenceChange( String packageName, RouteListingPreference routeListingPreference)3570             public void notifyRouteListingPreferenceChange(
3571                     String packageName, RouteListingPreference routeListingPreference) {
3572                 mHandler.sendMessage(
3573                         obtainMessage(
3574                                 ProxyMediaRouter2Impl::onRouteListingPreferenceChangedOnHandler,
3575                                 ProxyMediaRouter2Impl.this,
3576                                 packageName,
3577                                 routeListingPreference));
3578             }
3579 
3580             @Override
notifyDeviceSuggestionsUpdated( String packageName, String suggestingPackageName, @Nullable List<SuggestedDeviceInfo> deviceSuggestions)3581             public void notifyDeviceSuggestionsUpdated(
3582                     String packageName,
3583                     String suggestingPackageName,
3584                     @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
3585                 mHandler.sendMessage(
3586                         obtainMessage(
3587                                 ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler,
3588                                 ProxyMediaRouter2Impl.this,
3589                                 packageName,
3590                                 suggestingPackageName,
3591                                 deviceSuggestions));
3592             }
3593 
3594             @Override
notifyRoutesUpdated(List<MediaRoute2Info> routes)3595             public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
3596                 mHandler.sendMessage(
3597                         obtainMessage(
3598                                 MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));
3599             }
3600 
3601             @Override
notifyRequestFailed(int requestId, int reason)3602             public void notifyRequestFailed(int requestId, int reason) {
3603                 mHandler.sendMessage(
3604                         obtainMessage(
3605                                 ProxyMediaRouter2Impl::onRequestFailedOnHandler,
3606                                 ProxyMediaRouter2Impl.this,
3607                                 requestId,
3608                                 reason));
3609             }
3610 
3611             @Override
invalidateInstance()3612             public void invalidateInstance() {
3613                 mHandler.sendMessage(
3614                         obtainMessage(
3615                                 ProxyMediaRouter2Impl::onInvalidateInstanceOnHandler,
3616                                 ProxyMediaRouter2Impl.this));
3617             }
3618         }
3619     }
3620 
3621     /**
3622      * Implements logic specific to local {@link MediaRouter2} instances.
3623      *
3624      * <p>Local routers allow an app to control its own routing without any special permissions.
3625      * Apps can obtain an instance by calling {@link #getInstance(Context)}.
3626      */
3627     private class LocalMediaRouter2Impl implements MediaRouter2Impl {
3628         private final String mPackageName;
3629 
LocalMediaRouter2Impl(@onNull String packageName)3630         LocalMediaRouter2Impl(@NonNull String packageName) {
3631             mPackageName = packageName;
3632         }
3633 
3634         /**
3635          * No-op. Local routers cannot explicitly control route scanning.
3636          *
3637          * <p>Local routers can control scanning indirectly through {@link
3638          * #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}.
3639          */
3640         @Override
startScan()3641         public void startScan() {
3642             // Do nothing.
3643         }
3644 
3645         /**
3646          * No-op. Local routers cannot explicitly control route scanning.
3647          *
3648          * <p>Local routers can control scanning indirectly through {@link
3649          * #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}.
3650          */
3651         @Override
stopScan()3652         public void stopScan() {
3653             // Do nothing.
3654         }
3655 
3656         @Override
3657         @GuardedBy("mLock")
updateScanningState(int scanningState)3658         public void updateScanningState(int scanningState) throws RemoteException {
3659             if (scanningState != SCANNING_STATE_NOT_SCANNING) {
3660                 registerRouterStubIfNeededLocked();
3661             }
3662             mMediaRouterService.updateScanningStateWithRouter2(mStub, scanningState);
3663             if (scanningState == SCANNING_STATE_NOT_SCANNING) {
3664                 unregisterRouterStubIfNeededLocked(/* isScanningStopping */ true);
3665             }
3666         }
3667 
3668         /**
3669          * Returns {@code null}. The client package name is only associated to proxy {@link
3670          * MediaRouter2} instances.
3671          */
3672         @Override
getClientPackageName()3673         public String getClientPackageName() {
3674             return null;
3675         }
3676 
3677         @Override
getPackageName()3678         public String getPackageName() {
3679             return mPackageName;
3680         }
3681 
3682         @Override
getSystemSessionInfo()3683         public RoutingSessionInfo getSystemSessionInfo() {
3684             RoutingSessionInfo currentSystemSessionInfo = null;
3685             try {
3686                 currentSystemSessionInfo = ensureClientPackageNameForSystemSession(
3687                         mMediaRouterService.getSystemSessionInfo(), mContext.getPackageName());
3688             } catch (RemoteException ex) {
3689                 ex.rethrowFromSystemServer();
3690             }
3691             return currentSystemSessionInfo;
3692         }
3693 
3694         @Override
createRouteCallbackRecord( Executor executor, RouteCallback routeCallback, RouteDiscoveryPreference preference)3695         public RouteCallbackRecord createRouteCallbackRecord(
3696                 Executor executor,
3697                 RouteCallback routeCallback,
3698                 RouteDiscoveryPreference preference) {
3699             return new RouteCallbackRecord(executor, routeCallback, preference);
3700         }
3701 
3702         @Override
registerRouteCallback()3703         public void registerRouteCallback() {
3704             synchronized (mLock) {
3705                 try {
3706                     registerRouterStubIfNeededLocked();
3707 
3708                     if (updateDiscoveryPreferenceIfNeededLocked()) {
3709                         mMediaRouterService.setDiscoveryRequestWithRouter2(
3710                                 mStub, mDiscoveryPreference);
3711                     }
3712                 } catch (RemoteException ex) {
3713                     ex.rethrowFromSystemServer();
3714                 }
3715             }
3716         }
3717 
3718         @Override
unregisterRouteCallback()3719         public void unregisterRouteCallback() {
3720             synchronized (mLock) {
3721                 if (mStub == null) {
3722                     return;
3723                 }
3724 
3725                 try {
3726                     if (updateDiscoveryPreferenceIfNeededLocked()) {
3727                         mMediaRouterService.setDiscoveryRequestWithRouter2(
3728                                 mStub, mDiscoveryPreference);
3729                     }
3730 
3731                     unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
3732 
3733                 } catch (RemoteException ex) {
3734                     Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
3735                 }
3736             }
3737         }
3738 
3739         @Override
setRouteListingPreference(@ullable RouteListingPreference preference)3740         public void setRouteListingPreference(@Nullable RouteListingPreference preference) {
3741             synchronized (mLock) {
3742                 if (Objects.equals(mRouteListingPreference, preference)) {
3743                     // Nothing changed. We return early to save a call to the system server.
3744                     return;
3745                 }
3746                 mRouteListingPreference = preference;
3747                 try {
3748                     registerRouterStubIfNeededLocked();
3749                     mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference);
3750                 } catch (RemoteException ex) {
3751                     ex.rethrowFromSystemServer();
3752                 }
3753                 notifyRouteListingPreferenceUpdated(preference);
3754             }
3755         }
3756 
3757         @Override
setDeviceSuggestions(@ullable List<SuggestedDeviceInfo> deviceSuggestions)3758         public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
3759             synchronized (mLock) {
3760                 try {
3761                     registerRouterStubIfNeededLocked();
3762                     mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions);
3763                 } catch (RemoteException ex) {
3764                     ex.rethrowFromSystemServer();
3765                 }
3766             }
3767         }
3768 
3769         @Override
3770         @Nullable
getDeviceSuggestions()3771         public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
3772             synchronized (mLock) {
3773                 try {
3774                     return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub);
3775                 } catch (RemoteException ex) {
3776                     throw ex.rethrowFromSystemServer();
3777                 }
3778             }
3779         }
3780 
3781         @Override
showSystemOutputSwitcher()3782         public boolean showSystemOutputSwitcher() {
3783             synchronized (mLock) {
3784                 try {
3785                     return mMediaRouterService.showMediaOutputSwitcherWithRouter2(mPackageName);
3786                 } catch (RemoteException ex) {
3787                     ex.rethrowFromSystemServer();
3788                 }
3789             }
3790             return false;
3791         }
3792 
3793         /**
3794          * Returns {@link Collections#emptyList()}. Local routes can only access routes related to
3795          * their {@link RouteDiscoveryPreference} through {@link #getRoutes()}.
3796          */
3797         @Override
getAllRoutes()3798         public List<MediaRoute2Info> getAllRoutes() {
3799             return Collections.emptyList();
3800         }
3801 
3802         @Override
setOnGetControllerHintsListener(OnGetControllerHintsListener listener)3803         public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) {
3804             mOnGetControllerHintsListener = listener;
3805         }
3806 
3807         @Override
transferTo(MediaRoute2Info route)3808         public void transferTo(MediaRoute2Info route) {
3809             Log.v(TAG, "Transferring to route: " + route);
3810 
3811             boolean routeFound;
3812             synchronized (mLock) {
3813                 // TODO: Check thread-safety
3814                 routeFound = mRoutes.containsKey(route.getId());
3815             }
3816             if (!routeFound) {
3817                 notifyTransferFailure(route);
3818                 return;
3819             }
3820 
3821             RoutingController controller = getCurrentController();
3822             if (!controller.tryTransferWithinProvider(route)) {
3823                 requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE);
3824             }
3825         }
3826 
3827         @Override
stop()3828         public void stop() {
3829             getCurrentController().release();
3830         }
3831 
3832         /**
3833          * No-op. Local routers cannot request transfers of specific {@link RoutingSessionInfo}.
3834          * This operation is only available to proxy routers.
3835          *
3836          * <p>Local routers can only transfer the current {@link RoutingSessionInfo} using {@link
3837          * #transferTo(MediaRoute2Info)}.
3838          */
3839         @Override
transfer( @onNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route)3840         public void transfer(
3841                 @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
3842             // Do nothing.
3843         }
3844 
3845         @Override
getControllers()3846         public List<RoutingController> getControllers() {
3847             List<RoutingController> result = new ArrayList<>();
3848 
3849             result.add(0, mSystemController);
3850             synchronized (mLock) {
3851                 result.addAll(mNonSystemRoutingControllers.values());
3852             }
3853             return result;
3854         }
3855 
3856         /** Local routers cannot modify the volume of specific routes. */
3857         @Override
setRouteVolume(MediaRoute2Info route, int volume)3858         public void setRouteVolume(MediaRoute2Info route, int volume) {
3859             throw new UnsupportedOperationException(
3860                     "setRouteVolume is only supported by proxy routers. See javadoc.");
3861             // If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2()
3862         }
3863 
3864         @Override
setSessionVolume(int volume, RoutingSessionInfo sessionInfo)3865         public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) {
3866             MediaRouter2Stub stub;
3867             synchronized (mLock) {
3868                 stub = mStub;
3869             }
3870             if (stub != null) {
3871                 try {
3872                     mMediaRouterService.setSessionVolumeWithRouter2(
3873                             stub, sessionInfo.getId(), volume);
3874                 } catch (RemoteException ex) {
3875                     Log.e(TAG, "setVolume: Failed to deliver request.", ex);
3876                 }
3877             }
3878         }
3879 
3880         @Override
filterRoutesWithIndividualPreference( List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference)3881         public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
3882                 List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
3883             List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
3884             for (MediaRoute2Info route : routes) {
3885                 if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) {
3886                     continue;
3887                 }
3888                 if (!discoveryPreference.getAllowedPackages().isEmpty()
3889                         && (route.getProviderPackageName() == null
3890                                 || !discoveryPreference
3891                                         .getAllowedPackages()
3892                                         .contains(route.getProviderPackageName()))) {
3893                     continue;
3894                 }
3895                 filteredRoutes.add(route);
3896             }
3897             return filteredRoutes;
3898         }
3899 
3900         @Override
selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3901         public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
3902             MediaRouter2Stub stub;
3903             synchronized (mLock) {
3904                 stub = mStub;
3905             }
3906             if (stub != null) {
3907                 try {
3908                     mMediaRouterService.selectRouteWithRouter2(stub, sessionInfo.getId(), route);
3909                 } catch (RemoteException ex) {
3910                     Log.e(TAG, "Unable to select route for session.", ex);
3911                 }
3912             }
3913         }
3914 
3915         @Override
deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo)3916         public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
3917             MediaRouter2Stub stub;
3918             synchronized (mLock) {
3919                 stub = mStub;
3920             }
3921             if (stub != null) {
3922                 try {
3923                     mMediaRouterService.deselectRouteWithRouter2(stub, sessionInfo.getId(), route);
3924                 } catch (RemoteException ex) {
3925                     Log.e(TAG, "Unable to deselect route from session.", ex);
3926                 }
3927             }
3928         }
3929 
3930         @Override
releaseSession( boolean shouldReleaseSession, boolean shouldNotifyStop, RoutingController controller)3931         public void releaseSession(
3932                 boolean shouldReleaseSession,
3933                 boolean shouldNotifyStop,
3934                 RoutingController controller) {
3935             synchronized (mLock) {
3936                 mNonSystemRoutingControllers.remove(controller.getId(), controller);
3937 
3938                 if (shouldReleaseSession && mStub != null) {
3939                     try {
3940                         mMediaRouterService.releaseSessionWithRouter2(mStub, controller.getId());
3941                     } catch (RemoteException ex) {
3942                         ex.rethrowFromSystemServer();
3943                     }
3944                 }
3945 
3946                 if (shouldNotifyStop) {
3947                     mHandler.sendMessage(
3948                             obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller));
3949                 }
3950 
3951                 try {
3952                     unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
3953                 } catch (RemoteException ex) {
3954                     ex.rethrowFromSystemServer();
3955                 }
3956 
3957             }
3958         }
3959 
3960         @Override
wasTransferredBySelf(RoutingSessionInfo sessionInfo)3961         public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
3962             UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
3963             String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
3964             return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
3965                     && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
3966         }
3967 
3968         @GuardedBy("mLock")
registerRouterStubIfNeededLocked()3969         private void registerRouterStubIfNeededLocked() throws RemoteException {
3970             if (mStub == null) {
3971                 MediaRouter2Stub stub = new MediaRouter2Stub();
3972                 mMediaRouterService.registerRouter2(stub, mPackageName);
3973                 mStub = stub;
3974             }
3975         }
3976 
3977         @GuardedBy("mLock")
unregisterRouterStubIfNeededLocked(boolean isScanningStopping)3978         private void unregisterRouterStubIfNeededLocked(boolean isScanningStopping)
3979                 throws RemoteException {
3980             if (mStub != null
3981                     && mRouteCallbackRecords.isEmpty()
3982                     && mNonSystemRoutingControllers.isEmpty()
3983                     && (mScanRequestsMap.size() == 0 || isScanningStopping)) {
3984                 mMediaRouterService.unregisterRouter2(mStub);
3985                 mStub = null;
3986             }
3987         }
3988     }
3989 }
3990