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