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