1 /*
2  * Copyright 2018 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 package com.android.settingslib.media;
17 
18 import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET;
19 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
20 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
21 import static android.media.MediaRoute2Info.TYPE_DOCK;
22 import static android.media.MediaRoute2Info.TYPE_GROUP;
23 import static android.media.MediaRoute2Info.TYPE_HDMI;
24 import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
25 import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
26 import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
27 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
28 import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR;
29 import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER;
30 import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE;
31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTPHONE;
32 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH;
33 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
34 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET;
35 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED;
36 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
37 import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
38 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY;
39 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
40 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
41 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
42 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
43 import static android.media.session.MediaController.PlaybackInfo;
44 
45 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
46 
47 import android.annotation.TargetApi;
48 import android.bluetooth.BluetoothAdapter;
49 import android.bluetooth.BluetoothDevice;
50 import android.content.ComponentName;
51 import android.content.Context;
52 import android.media.MediaRoute2Info;
53 import android.media.RouteListingPreference;
54 import android.media.RoutingSessionInfo;
55 import android.media.session.MediaController;
56 import android.media.session.MediaSession;
57 import android.os.Build;
58 import android.os.UserHandle;
59 import android.text.TextUtils;
60 import android.util.Log;
61 
62 import androidx.annotation.DoNotInline;
63 import androidx.annotation.NonNull;
64 import androidx.annotation.Nullable;
65 import androidx.annotation.RequiresApi;
66 
67 import com.android.internal.annotations.VisibleForTesting;
68 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
69 import com.android.settingslib.bluetooth.LocalBluetoothManager;
70 import com.android.settingslib.media.flags.Flags;
71 
72 import java.util.ArrayList;
73 import java.util.Collection;
74 import java.util.Collections;
75 import java.util.HashSet;
76 import java.util.LinkedHashSet;
77 import java.util.List;
78 import java.util.Map;
79 import java.util.Set;
80 import java.util.concurrent.ConcurrentHashMap;
81 import java.util.concurrent.CopyOnWriteArrayList;
82 import java.util.function.Function;
83 import java.util.stream.Collectors;
84 import java.util.stream.Stream;
85 
86 /** InfoMediaManager provide interface to get InfoMediaDevice list. */
87 @RequiresApi(Build.VERSION_CODES.R)
88 public abstract class InfoMediaManager {
89     /** Callback for notifying device is added, removed and attributes changed. */
90     public interface MediaDeviceCallback {
91 
92         /**
93          * Callback for notifying MediaDevice list is added.
94          *
95          * @param devices the MediaDevice list
96          */
onDeviceListAdded(@onNull List<MediaDevice> devices)97         void onDeviceListAdded(@NonNull List<MediaDevice> devices);
98 
99         /**
100          * Callback for notifying MediaDevice list is removed.
101          *
102          * @param devices the MediaDevice list
103          */
onDeviceListRemoved(@onNull List<MediaDevice> devices)104         void onDeviceListRemoved(@NonNull List<MediaDevice> devices);
105 
106         /**
107          * Callback for notifying connected MediaDevice is changed.
108          *
109          * @param id the id of MediaDevice
110          */
onConnectedDeviceChanged(@ullable String id)111         void onConnectedDeviceChanged(@Nullable String id);
112 
113         /**
114          * Callback for notifying that transferring is failed.
115          *
116          * @param reason the reason that the request has failed. Can be one of followings: {@link
117          *     android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, {@link
118          *     android.media.MediaRoute2ProviderService#REASON_REJECTED}, {@link
119          *     android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, {@link
120          *     android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, {@link
121          *     android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
122          */
onRequestFailed(int reason)123         void onRequestFailed(int reason);
124     }
125 
126     /** Checked exception that signals the specified package is not present in the system. */
127     public static class PackageNotAvailableException extends Exception {
PackageNotAvailableException(String message)128         public PackageNotAvailableException(String message) {
129             super(message);
130         }
131     }
132 
133     private static final String TAG = "InfoMediaManager";
134     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
135     protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
136     @NonNull protected final Context mContext;
137     @NonNull protected final String mPackageName;
138     @NonNull protected final UserHandle mUserHandle;
139     private final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
140     private MediaDevice mCurrentConnectedDevice;
141     private MediaController mMediaController;
142     private PlaybackInfo mLastKnownPlaybackInfo;
143     private final LocalBluetoothManager mBluetoothManager;
144     private final Map<String, RouteListingPreference.Item> mPreferenceItemMap =
145             new ConcurrentHashMap<>();
146 
147     private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback();
148 
InfoMediaManager( @onNull Context context, @NonNull String packageName, @NonNull UserHandle userHandle, @NonNull LocalBluetoothManager localBluetoothManager, @Nullable MediaController mediaController)149     /* package */ InfoMediaManager(
150             @NonNull Context context,
151             @NonNull String packageName,
152             @NonNull UserHandle userHandle,
153             @NonNull LocalBluetoothManager localBluetoothManager,
154             @Nullable MediaController mediaController) {
155         mContext = context;
156         mBluetoothManager = localBluetoothManager;
157         mPackageName = packageName;
158         mUserHandle = userHandle;
159         mMediaController = mediaController;
160         if (mediaController != null) {
161             mLastKnownPlaybackInfo = mediaController.getPlaybackInfo();
162         }
163     }
164 
165     /**
166      * Creates an instance of InfoMediaManager.
167      *
168      * @param context The {@link Context}.
169      * @param packageName The package name of the app for which to control routing, or null if the
170      *     caller is interested in system-level routing only (for example, headsets, built-in
171      *     speakers, as opposed to app-specific routing (for example, casting to another device).
172      * @param userHandle The {@link UserHandle} of the user on which the app to control is running,
173      *     or null if the caller does not need app-specific routing (see {@code packageName}).
174      * @param token The token of the associated {@link MediaSession} for which to do media routing.
175      */
createInstance( Context context, @Nullable String packageName, @Nullable UserHandle userHandle, LocalBluetoothManager localBluetoothManager, @Nullable MediaSession.Token token)176     public static InfoMediaManager createInstance(
177             Context context,
178             @Nullable String packageName,
179             @Nullable UserHandle userHandle,
180             LocalBluetoothManager localBluetoothManager,
181             @Nullable MediaSession.Token token) {
182         MediaController mediaController = null;
183 
184         if (Flags.usePlaybackInfoForRoutingControls() && token != null) {
185             mediaController = new MediaController(context, token);
186         }
187 
188         // The caller is only interested in system routes (headsets, built-in speakers, etc), and is
189         // not interested in a specific app's routing. The media routing APIs still require a
190         // package name, so we use the package name of the calling app.
191         if (TextUtils.isEmpty(packageName)) {
192             packageName = context.getPackageName();
193         }
194 
195         if (userHandle == null) {
196             userHandle = android.os.Process.myUserHandle();
197         }
198 
199         if (Flags.useMediaRouter2ForInfoMediaManager()) {
200             try {
201                 return new RouterInfoMediaManager(
202                         context, packageName, userHandle, localBluetoothManager, mediaController);
203             } catch (PackageNotAvailableException ex) {
204                 // TODO: b/293578081 - Propagate this exception to callers for proper handling.
205                 Log.w(TAG, "Returning a no-op InfoMediaManager for package " + packageName);
206                 return new NoOpInfoMediaManager(
207                         context, packageName, userHandle, localBluetoothManager, mediaController);
208             }
209         } else {
210             return new ManagerInfoMediaManager(
211                     context, packageName, userHandle, localBluetoothManager, mediaController);
212         }
213     }
214 
startScan()215     public void startScan() {
216         startScanOnRouter();
217     }
218 
updateRouteListingPreference()219     private void updateRouteListingPreference() {
220         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
221             RouteListingPreference routeListingPreference =
222                     getRouteListingPreference();
223             Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference,
224                     mPreferenceItemMap);
225         }
226     }
227 
stopScan()228     public final void stopScan() {
229         stopScanOnRouter();
230     }
231 
stopScanOnRouter()232     protected abstract void stopScanOnRouter();
233 
startScanOnRouter()234     protected abstract void startScanOnRouter();
235 
registerRouter()236     protected abstract void registerRouter();
237 
unregisterRouter()238     protected abstract void unregisterRouter();
239 
transferToRoute(@onNull MediaRoute2Info route)240     protected abstract void transferToRoute(@NonNull MediaRoute2Info route);
241 
selectRoute( @onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)242     protected abstract void selectRoute(
243             @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info);
244 
deselectRoute( @onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)245     protected abstract void deselectRoute(
246             @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info);
247 
releaseSession(@onNull RoutingSessionInfo sessionInfo)248     protected abstract void releaseSession(@NonNull RoutingSessionInfo sessionInfo);
249 
250     @NonNull
getSelectableRoutes(@onNull RoutingSessionInfo info)251     protected abstract List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info);
252 
253     @NonNull
getDeselectableRoutes( @onNull RoutingSessionInfo info)254     protected abstract List<MediaRoute2Info> getDeselectableRoutes(
255             @NonNull RoutingSessionInfo info);
256 
257     @NonNull
getSelectedRoutes(@onNull RoutingSessionInfo info)258     protected abstract List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info);
259 
setSessionVolume(@onNull RoutingSessionInfo info, int volume)260     protected abstract void setSessionVolume(@NonNull RoutingSessionInfo info, int volume);
261 
setRouteVolume(@onNull MediaRoute2Info route, int volume)262     protected abstract void setRouteVolume(@NonNull MediaRoute2Info route, int volume);
263 
264     @Nullable
getRouteListingPreference()265     protected abstract RouteListingPreference getRouteListingPreference();
266 
267     /**
268      * Returns the list of remote {@link RoutingSessionInfo routing sessions} known to the system.
269      */
270     @NonNull
getRemoteSessions()271     protected abstract List<RoutingSessionInfo> getRemoteSessions();
272 
273     /**
274      * Returns a non-empty list containing the routing sessions associated to the target media app.
275      *
276      * <p> The first item of the list is always the {@link RoutingSessionInfo#isSystemSession()
277      * system session}, followed other remote sessions linked to the target media app.
278      */
279     @NonNull
getRoutingSessionsForPackage()280     protected abstract List<RoutingSessionInfo> getRoutingSessionsForPackage();
281 
282     @Nullable
getRoutingSessionById(@onNull String sessionId)283     protected abstract RoutingSessionInfo getRoutingSessionById(@NonNull String sessionId);
284 
285     @NonNull
getAvailableRoutesFromRouter()286     protected abstract List<MediaRoute2Info> getAvailableRoutesFromRouter();
287 
288     @NonNull
getTransferableRoutes(@onNull String packageName)289     protected abstract List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName);
290 
rebuildDeviceList()291     protected final void rebuildDeviceList() {
292         buildAvailableRoutes();
293     }
294 
notifyCurrentConnectedDeviceChanged()295     protected final void notifyCurrentConnectedDeviceChanged() {
296         final String id = mCurrentConnectedDevice != null ? mCurrentConnectedDevice.getId() : null;
297         dispatchConnectedDeviceChanged(id);
298     }
299 
300     @RequiresApi(34)
notifyRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference)301     protected final void notifyRouteListingPreferenceUpdated(
302             RouteListingPreference routeListingPreference) {
303         Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, mPreferenceItemMap);
304     }
305 
findMediaDevice(@onNull String id)306     protected final MediaDevice findMediaDevice(@NonNull String id) {
307         for (MediaDevice mediaDevice : mMediaDevices) {
308             if (mediaDevice.getId().equals(id)) {
309                 return mediaDevice;
310             }
311         }
312         Log.e(TAG, "findMediaDevice() can't find device with id: " + id);
313         return null;
314     }
315 
316     /**
317      * Registers the specified {@code callback} to receive state updates about routing information.
318      *
319      * <p>As long as there is a registered {@link MediaDeviceCallback}, {@link InfoMediaManager}
320      * will receive state updates from the platform.
321      *
322      * <p>Call {@link #unregisterCallback(MediaDeviceCallback)} once you no longer need platform
323      * updates.
324      */
registerCallback(@onNull MediaDeviceCallback callback)325     public final void registerCallback(@NonNull MediaDeviceCallback callback) {
326         boolean wasEmpty = mCallbacks.isEmpty();
327         if (!mCallbacks.contains(callback)) {
328             mCallbacks.add(callback);
329             if (wasEmpty) {
330                 mMediaDevices.clear();
331                 registerRouter();
332                 if (mMediaController != null) {
333                     mMediaController.registerCallback(mMediaControllerCallback);
334                 }
335                 updateRouteListingPreference();
336                 refreshDevices();
337             }
338         }
339     }
340 
341     /**
342      * Unregisters the specified {@code callback}.
343      *
344      * @see #registerCallback(MediaDeviceCallback)
345      */
unregisterCallback(@onNull MediaDeviceCallback callback)346     public final void unregisterCallback(@NonNull MediaDeviceCallback callback) {
347         if (mCallbacks.remove(callback) && mCallbacks.isEmpty()) {
348             if (mMediaController != null) {
349                 mMediaController.unregisterCallback(mMediaControllerCallback);
350             }
351             unregisterRouter();
352         }
353     }
354 
dispatchDeviceListAdded(@onNull List<MediaDevice> devices)355     private void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) {
356         for (MediaDeviceCallback callback : getCallbacks()) {
357             callback.onDeviceListAdded(new ArrayList<>(devices));
358         }
359     }
360 
dispatchConnectedDeviceChanged(String id)361     private void dispatchConnectedDeviceChanged(String id) {
362         for (MediaDeviceCallback callback : getCallbacks()) {
363             callback.onConnectedDeviceChanged(id);
364         }
365     }
366 
dispatchOnRequestFailed(int reason)367     protected void dispatchOnRequestFailed(int reason) {
368         for (MediaDeviceCallback callback : getCallbacks()) {
369             callback.onRequestFailed(reason);
370         }
371     }
372 
getCallbacks()373     private Collection<MediaDeviceCallback> getCallbacks() {
374         return new CopyOnWriteArrayList<>(mCallbacks);
375     }
376 
377     /**
378      * Get current device that played media.
379      * @return MediaDevice
380      */
getCurrentConnectedDevice()381     MediaDevice getCurrentConnectedDevice() {
382         return mCurrentConnectedDevice;
383     }
384 
connectToDevice(MediaDevice device)385     /* package */ void connectToDevice(MediaDevice device) {
386         if (device.mRouteInfo == null) {
387             Log.w(TAG, "Unable to connect. RouteInfo is empty");
388             return;
389         }
390 
391         device.setConnectedRecord();
392         transferToRoute(device.mRouteInfo);
393     }
394 
395     /**
396      * Add a MediaDevice to let it play current media.
397      *
398      * @param device MediaDevice
399      * @return If add device successful return {@code true}, otherwise return {@code false}
400      */
addDeviceToPlayMedia(MediaDevice device)401     boolean addDeviceToPlayMedia(MediaDevice device) {
402         final RoutingSessionInfo info = getActiveRoutingSession();
403         if (!info.getSelectableRoutes().contains(device.mRouteInfo.getId())) {
404             Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : "
405                     + device.getName());
406             return false;
407         }
408 
409         selectRoute(device.mRouteInfo, info);
410         return true;
411     }
412 
413     @NonNull
getActiveRoutingSession()414     private RoutingSessionInfo getActiveRoutingSession() {
415         // List is never empty.
416         final List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage();
417         RoutingSessionInfo activeSession = sessions.get(sessions.size() - 1);
418 
419         // Logic from MediaRouter2Manager#getRoutingSessionForMediaController
420         if (!Flags.usePlaybackInfoForRoutingControls() || mMediaController == null) {
421             return activeSession;
422         }
423 
424         PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo();
425         if (playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
426             // Return system session.
427             return sessions.get(0);
428         }
429 
430         // For PLAYBACK_TYPE_REMOTE.
431         String volumeControlId = playbackInfo.getVolumeControlId();
432         for (RoutingSessionInfo session : sessions) {
433             if (TextUtils.equals(volumeControlId, session.getId())) {
434                 return session;
435             }
436             // Workaround for provider not being able to know the unique session ID.
437             if (TextUtils.equals(volumeControlId, session.getOriginalId())
438                     && TextUtils.equals(
439                             mMediaController.getPackageName(), session.getOwnerPackageName())) {
440                 return session;
441             }
442         }
443 
444         return activeSession;
445     }
446 
isRoutingSessionAvailableForVolumeControl()447     boolean isRoutingSessionAvailableForVolumeControl() {
448         List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage();
449 
450         for (RoutingSessionInfo session : sessions) {
451             if (!session.isSystemSession()
452                     && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
453                 return true;
454             }
455         }
456 
457         Log.d(TAG, "No routing session for " + mPackageName);
458         return false;
459     }
460 
preferRouteListingOrdering()461     boolean preferRouteListingOrdering() {
462         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
463                 && Api34Impl.preferRouteListingOrdering(getRouteListingPreference());
464     }
465 
466     @Nullable
getLinkedItemComponentName()467     ComponentName getLinkedItemComponentName() {
468         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && TextUtils.isEmpty(
469                 mPackageName)) {
470             return null;
471         }
472         return Api34Impl.getLinkedItemComponentName(getRouteListingPreference());
473     }
474 
475     /**
476      * Remove a {@code device} from current media.
477      *
478      * @param device MediaDevice
479      * @return If device stop successful return {@code true}, otherwise return {@code false}
480      */
removeDeviceFromPlayMedia(MediaDevice device)481     boolean removeDeviceFromPlayMedia(MediaDevice device) {
482         final RoutingSessionInfo info = getActiveRoutingSession();
483         if (!info.getSelectedRoutes().contains(device.mRouteInfo.getId())) {
484             Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : "
485                     + device.getName());
486             return false;
487         }
488 
489         deselectRoute(device.mRouteInfo, info);
490         return true;
491     }
492 
493     /**
494      * Release session to stop playing media on MediaDevice.
495      */
releaseSession()496     boolean releaseSession() {
497         releaseSession(getActiveRoutingSession());
498         return true;
499     }
500 
501     /**
502      * Returns the list of {@link MediaDevice media devices} that can be added to the current {@link
503      * RoutingSessionInfo routing session}.
504      */
505     @NonNull
getSelectableMediaDevices()506     List<MediaDevice> getSelectableMediaDevices() {
507         final RoutingSessionInfo info = getActiveRoutingSession();
508 
509         final List<MediaDevice> deviceList = new ArrayList<>();
510         for (MediaRoute2Info route : getSelectableRoutes(info)) {
511             deviceList.add(
512                     new InfoMediaDevice(
513                             mContext, route, mPreferenceItemMap.get(route.getId())));
514         }
515         return deviceList;
516     }
517 
518     /**
519      * Returns the list of {@link MediaDevice media devices} that can be deselected from the current
520      * {@link RoutingSessionInfo routing session}.
521      */
522     @NonNull
getDeselectableMediaDevices()523     List<MediaDevice> getDeselectableMediaDevices() {
524         final RoutingSessionInfo info = getActiveRoutingSession();
525 
526         final List<MediaDevice> deviceList = new ArrayList<>();
527         for (MediaRoute2Info route : getDeselectableRoutes(info)) {
528             deviceList.add(
529                     new InfoMediaDevice(
530                             mContext, route, mPreferenceItemMap.get(route.getId())));
531             Log.d(TAG, route.getName() + " is deselectable for " + mPackageName);
532         }
533         return deviceList;
534     }
535 
536     /**
537      * Returns the list of {@link MediaDevice media devices} that are selected in the current {@link
538      * RoutingSessionInfo routing session}.
539      */
540     @NonNull
getSelectedMediaDevices()541     List<MediaDevice> getSelectedMediaDevices() {
542         RoutingSessionInfo info = getActiveRoutingSession();
543 
544         final List<MediaDevice> deviceList = new ArrayList<>();
545         for (MediaRoute2Info route : getSelectedRoutes(info)) {
546             deviceList.add(
547                     new InfoMediaDevice(
548                             mContext, route, mPreferenceItemMap.get(route.getId())));
549         }
550         return deviceList;
551     }
552 
adjustDeviceVolume(MediaDevice device, int volume)553     /* package */ void adjustDeviceVolume(MediaDevice device, int volume) {
554         if (device.mRouteInfo == null) {
555             Log.w(TAG, "Unable to set volume. RouteInfo is empty");
556             return;
557         }
558         setRouteVolume(device.mRouteInfo, volume);
559     }
560 
adjustSessionVolume(RoutingSessionInfo info, int volume)561     void adjustSessionVolume(RoutingSessionInfo info, int volume) {
562         if (info == null) {
563             Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty");
564             return;
565         }
566 
567         setSessionVolume(info, volume);
568     }
569 
570     /**
571      * Adjust the volume of {@link android.media.RoutingSessionInfo}.
572      *
573      * @param volume the value of volume
574      */
adjustSessionVolume(int volume)575     void adjustSessionVolume(int volume) {
576         Log.d(TAG, "adjustSessionVolume() adjust volume: " + volume + ", with : " + mPackageName);
577         setSessionVolume(getActiveRoutingSession(), volume);
578     }
579 
580     /**
581      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
582      *
583      * @return  maximum volume of the session, and return -1 if not found.
584      */
getSessionVolumeMax()585     public int getSessionVolumeMax() {
586         return getActiveRoutingSession().getVolumeMax();
587     }
588 
589     /**
590      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
591      *
592      * @return current volume of the session, and return -1 if not found.
593      */
getSessionVolume()594     public int getSessionVolume() {
595         return getActiveRoutingSession().getVolume();
596     }
597 
getSessionName()598     CharSequence getSessionName() {
599         return getActiveRoutingSession().getName();
600     }
601 
602     @TargetApi(Build.VERSION_CODES.R)
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)603     boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
604         return sessionInfo.isSystemSession() // System sessions are not remote
605                 || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
606     }
607 
refreshDevices()608     protected final synchronized void refreshDevices() {
609         rebuildDeviceList();
610         dispatchDeviceListAdded(mMediaDevices);
611     }
612 
613     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
614     @SuppressWarnings("NewApi")
buildAvailableRoutes()615     private synchronized void buildAvailableRoutes() {
616         mMediaDevices.clear();
617         RoutingSessionInfo activeSession = getActiveRoutingSession();
618 
619         for (MediaRoute2Info route : getAvailableRoutes(activeSession)) {
620             if (DEBUG) {
621                 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : "
622                         + route.getVolume() + ", type : " + route.getType());
623             }
624             addMediaDevice(route, activeSession);
625         }
626 
627         // In practice, mMediaDevices should always have at least one route.
628         if (!mMediaDevices.isEmpty()) {
629             // First device on the list is always the first selected route.
630             mCurrentConnectedDevice = mMediaDevices.get(0);
631         }
632     }
633 
getAvailableRoutes( RoutingSessionInfo activeSession)634     private synchronized List<MediaRoute2Info> getAvailableRoutes(
635             RoutingSessionInfo activeSession) {
636         List<MediaRoute2Info> availableRoutes = new ArrayList<>();
637 
638         List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(activeSession);
639         availableRoutes.addAll(selectedRoutes);
640         availableRoutes.addAll(getSelectableRoutes(activeSession));
641 
642         final List<MediaRoute2Info> transferableRoutes = getTransferableRoutes(mPackageName);
643         for (MediaRoute2Info transferableRoute : transferableRoutes) {
644             boolean alreadyAdded = false;
645             for (MediaRoute2Info mediaRoute2Info : availableRoutes) {
646                 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) {
647                     alreadyAdded = true;
648                     break;
649                 }
650             }
651             if (!alreadyAdded) {
652                 availableRoutes.add(transferableRoute);
653             }
654         }
655         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
656             RouteListingPreference routeListingPreference = getRouteListingPreference();
657             if (routeListingPreference != null) {
658                 final List<RouteListingPreference.Item> preferenceRouteListing =
659                         Api34Impl.composePreferenceRouteListing(
660                                 routeListingPreference);
661                 availableRoutes = Api34Impl.arrangeRouteListByPreference(selectedRoutes,
662                         getAvailableRoutesFromRouter(),
663                                 preferenceRouteListing);
664             }
665             return Api34Impl.filterDuplicatedIds(availableRoutes);
666         } else {
667             return availableRoutes;
668         }
669     }
670 
671     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
672     @SuppressWarnings("NewApi")
673     @VisibleForTesting
addMediaDevice(@onNull MediaRoute2Info route, @NonNull RoutingSessionInfo activeSession)674     void addMediaDevice(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo activeSession) {
675         final int deviceType = route.getType();
676         MediaDevice mediaDevice = null;
677         switch (deviceType) {
678             case TYPE_UNKNOWN:
679             case TYPE_REMOTE_TV:
680             case TYPE_REMOTE_SPEAKER:
681             case TYPE_GROUP:
682             case TYPE_REMOTE_TABLET:
683             case TYPE_REMOTE_TABLET_DOCKED:
684             case TYPE_REMOTE_COMPUTER:
685             case TYPE_REMOTE_GAME_CONSOLE:
686             case TYPE_REMOTE_CAR:
687             case TYPE_REMOTE_SMARTWATCH:
688             case TYPE_REMOTE_SMARTPHONE:
689                 mediaDevice =
690                         new InfoMediaDevice(
691                                 mContext,
692                                 route,
693                                 mPreferenceItemMap.get(route.getId()));
694                 break;
695             case TYPE_BUILTIN_SPEAKER:
696             case TYPE_USB_DEVICE:
697             case TYPE_USB_HEADSET:
698             case TYPE_USB_ACCESSORY:
699             case TYPE_DOCK:
700             case TYPE_HDMI:
701             case TYPE_HDMI_ARC:
702             case TYPE_HDMI_EARC:
703             case TYPE_WIRED_HEADSET:
704             case TYPE_WIRED_HEADPHONES:
705                 mediaDevice =
706                         new PhoneMediaDevice(
707                                 mContext,
708                                 route,
709                                 mPreferenceItemMap.getOrDefault(route.getId(), null));
710                 break;
711             case TYPE_HEARING_AID:
712             case TYPE_BLUETOOTH_A2DP:
713             case TYPE_BLE_HEADSET:
714                 if (route.getAddress() == null) {
715                     Log.e(TAG, "Ignoring bluetooth route with no set address: " + route);
716                     break;
717                 }
718                 final BluetoothDevice device =
719                         BluetoothAdapter.getDefaultAdapter()
720                                 .getRemoteDevice(route.getAddress());
721                 final CachedBluetoothDevice cachedDevice =
722                         mBluetoothManager.getCachedDeviceManager().findDevice(device);
723                 if (cachedDevice != null) {
724                     mediaDevice =
725                             new BluetoothMediaDevice(
726                                     mContext,
727                                     cachedDevice,
728                                     route,
729                                     mPreferenceItemMap.getOrDefault(route.getId(), null));
730                 }
731                 break;
732             case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER:
733                 mediaDevice =
734                         new ComplexMediaDevice(
735                                 mContext,
736                                 route,
737                                 mPreferenceItemMap.get(route.getId()));
738                 break;
739             default:
740                 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType);
741                 break;
742         }
743 
744         if (mediaDevice != null) {
745             if (activeSession.getSelectedRoutes().contains(route.getId())) {
746                 mediaDevice.setState(STATE_SELECTED);
747             }
748             mMediaDevices.add(mediaDevice);
749         }
750     }
751 
752     @RequiresApi(34)
753     static class Api34Impl {
754         @DoNotInline
composePreferenceRouteListing( RouteListingPreference routeListingPreference)755         static List<RouteListingPreference.Item> composePreferenceRouteListing(
756                 RouteListingPreference routeListingPreference) {
757             List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
758             List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
759             for (RouteListingPreference.Item item : itemList) {
760                 // Put suggested devices on the top first before further organization
761                 if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) {
762                     finalizedItemList.add(0, item);
763                 } else {
764                     finalizedItemList.add(item);
765                 }
766             }
767             return finalizedItemList;
768         }
769 
770         @DoNotInline
filterDuplicatedIds(List<MediaRoute2Info> infos)771         static synchronized List<MediaRoute2Info> filterDuplicatedIds(List<MediaRoute2Info> infos) {
772             List<MediaRoute2Info> filteredInfos = new ArrayList<>();
773             Set<String> foundDeduplicationIds = new HashSet<>();
774             for (MediaRoute2Info mediaRoute2Info : infos) {
775                 if (!Collections.disjoint(mediaRoute2Info.getDeduplicationIds(),
776                         foundDeduplicationIds)) {
777                     continue;
778                 }
779                 filteredInfos.add(mediaRoute2Info);
780                 foundDeduplicationIds.addAll(mediaRoute2Info.getDeduplicationIds());
781             }
782             return filteredInfos;
783         }
784 
785         /**
786          * Returns an ordered list of available devices based on the provided {@code
787          * routeListingPreferenceItems}.
788          *
789          * <p>The result has the following order:
790          *
791          * <ol>
792          *   <li>Selected routes.
793          *   <li>Not-selected system routes.
794          *   <li>Not-selected, non-system, available routes sorted by route listing preference.
795          * </ol>
796          *
797          * @param selectedRoutes List of currently selected routes.
798          * @param availableRoutes List of available routes that match the app's requested route
799          *     features.
800          * @param routeListingPreferenceItems Ordered list of {@link RouteListingPreference.Item} to
801          *     sort routes with.
802          */
803         @DoNotInline
arrangeRouteListByPreference( List<MediaRoute2Info> selectedRoutes, List<MediaRoute2Info> availableRoutes, List<RouteListingPreference.Item> routeListingPreferenceItems)804         static List<MediaRoute2Info> arrangeRouteListByPreference(
805                 List<MediaRoute2Info> selectedRoutes,
806                 List<MediaRoute2Info> availableRoutes,
807                 List<RouteListingPreference.Item> routeListingPreferenceItems) {
808             Set<String> sortedRouteIds = new LinkedHashSet<>();
809 
810             // Add selected routes first.
811             for (MediaRoute2Info selectedRoute : selectedRoutes) {
812                 sortedRouteIds.add(selectedRoute.getId());
813             }
814 
815             // Add not-yet-added system routes.
816             for (MediaRoute2Info availableRoute : availableRoutes) {
817                 if (availableRoute.isSystemRoute()) {
818                     sortedRouteIds.add(availableRoute.getId());
819                 }
820             }
821 
822             // Create a mapping from id to route to avoid a quadratic search.
823             Map<String, MediaRoute2Info> idToRouteMap =
824                     Stream.concat(selectedRoutes.stream(), availableRoutes.stream())
825                             .collect(
826                                     Collectors.toMap(
827                                             MediaRoute2Info::getId,
828                                             Function.identity(),
829                                             (route1, route2) -> route1));
830 
831             // Add not-selected routes that match RLP items. All system routes have already been
832             // added at this point.
833             for (RouteListingPreference.Item item : routeListingPreferenceItems) {
834                 MediaRoute2Info route = idToRouteMap.get(item.getRouteId());
835                 if (route != null) {
836                     sortedRouteIds.add(route.getId());
837                 }
838             }
839 
840             return sortedRouteIds.stream().map(idToRouteMap::get).collect(Collectors.toList());
841         }
842 
843         @DoNotInline
preferRouteListingOrdering(RouteListingPreference routeListingPreference)844         static boolean preferRouteListingOrdering(RouteListingPreference routeListingPreference) {
845             return routeListingPreference != null
846                     && !routeListingPreference.getUseSystemOrdering();
847         }
848 
849         @DoNotInline
850         @Nullable
getLinkedItemComponentName( RouteListingPreference routeListingPreference)851         static ComponentName getLinkedItemComponentName(
852                 RouteListingPreference routeListingPreference) {
853             return routeListingPreference == null ? null
854                     : routeListingPreference.getLinkedItemComponentName();
855         }
856 
857         @DoNotInline
onRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap)858         static void onRouteListingPreferenceUpdated(
859                 RouteListingPreference routeListingPreference,
860                 Map<String, RouteListingPreference.Item> preferenceItemMap) {
861             preferenceItemMap.clear();
862             if (routeListingPreference != null) {
863                 routeListingPreference.getItems().forEach((item) ->
864                         preferenceItemMap.put(item.getRouteId(), item));
865             }
866         }
867     }
868 
869     private final class MediaControllerCallback extends MediaController.Callback {
870         @Override
onSessionDestroyed()871         public void onSessionDestroyed() {
872             mMediaController = null;
873             refreshDevices();
874         }
875 
876         @Override
onAudioInfoChanged(@onNull PlaybackInfo info)877         public void onAudioInfoChanged(@NonNull PlaybackInfo info) {
878             if (info.getPlaybackType() != mLastKnownPlaybackInfo.getPlaybackType()
879                     || !TextUtils.equals(
880                             info.getVolumeControlId(),
881                             mLastKnownPlaybackInfo.getVolumeControlId())) {
882                 refreshDevices();
883             }
884             mLastKnownPlaybackInfo = info;
885         }
886     }
887 }
888