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_HEARING_AID;
25 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
26 import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR;
27 import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER;
28 import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE;
29 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTPHONE;
30 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH;
31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
32 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET;
33 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED;
34 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
35 import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
36 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY;
37 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
38 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
39 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
40 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
41 
42 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
43 
44 import android.annotation.Nullable;
45 import android.annotation.TargetApi;
46 import android.app.Notification;
47 import android.bluetooth.BluetoothAdapter;
48 import android.bluetooth.BluetoothDevice;
49 import android.content.ComponentName;
50 import android.content.Context;
51 import android.media.MediaRoute2Info;
52 import android.media.MediaRouter2Manager;
53 import android.media.RouteListingPreference;
54 import android.media.RoutingSessionInfo;
55 import android.os.Build;
56 import android.text.TextUtils;
57 import android.util.Log;
58 
59 import androidx.annotation.DoNotInline;
60 import androidx.annotation.NonNull;
61 import androidx.annotation.RequiresApi;
62 
63 import com.android.internal.annotations.VisibleForTesting;
64 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
65 import com.android.settingslib.bluetooth.LocalBluetoothManager;
66 
67 import java.util.ArrayList;
68 import java.util.Collections;
69 import java.util.HashSet;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Set;
73 import java.util.concurrent.ConcurrentHashMap;
74 import java.util.concurrent.Executor;
75 import java.util.concurrent.Executors;
76 import java.util.stream.Collectors;
77 
78 /**
79  * InfoMediaManager provide interface to get InfoMediaDevice list.
80  */
81 @RequiresApi(Build.VERSION_CODES.R)
82 public class InfoMediaManager extends MediaManager {
83 
84     private static final String TAG = "InfoMediaManager";
85     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
86     @VisibleForTesting
87     final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback();
88     @VisibleForTesting
89     final Executor mExecutor = Executors.newSingleThreadExecutor();
90     @VisibleForTesting
91     MediaRouter2Manager mRouterManager;
92     @VisibleForTesting
93     String mPackageName;
94     boolean mIsScanning = false;
95 
96     private MediaDevice mCurrentConnectedDevice;
97     private LocalBluetoothManager mBluetoothManager;
98     private final Map<String, RouteListingPreference.Item> mPreferenceItemMap =
99             new ConcurrentHashMap<>();
100 
InfoMediaManager(Context context, String packageName, Notification notification, LocalBluetoothManager localBluetoothManager)101     public InfoMediaManager(Context context, String packageName, Notification notification,
102             LocalBluetoothManager localBluetoothManager) {
103         super(context, notification);
104 
105         mRouterManager = MediaRouter2Manager.getInstance(context);
106         mBluetoothManager = localBluetoothManager;
107         if (!TextUtils.isEmpty(packageName)) {
108             mPackageName = packageName;
109         }
110     }
111 
112     @Override
startScan()113     public void startScan() {
114         if (!mIsScanning) {
115             mMediaDevices.clear();
116             mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
117             mRouterManager.registerScanRequest();
118             mIsScanning = true;
119             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
120                     && !TextUtils.isEmpty(mPackageName)) {
121                 RouteListingPreference routeListingPreference =
122                         mRouterManager.getRouteListingPreference(mPackageName);
123                 Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference,
124                         mPreferenceItemMap);
125             }
126             refreshDevices();
127         }
128     }
129 
130     @Override
stopScan()131     public void stopScan() {
132         if (mIsScanning) {
133             mRouterManager.unregisterCallback(mMediaRouterCallback);
134             mRouterManager.unregisterScanRequest();
135             mIsScanning = false;
136         }
137     }
138 
139     /**
140      * Get current device that played media.
141      * @return MediaDevice
142      */
getCurrentConnectedDevice()143     MediaDevice getCurrentConnectedDevice() {
144         return mCurrentConnectedDevice;
145     }
146 
147     /**
148      * Transfer MediaDevice for media without package name.
149      */
connectDeviceWithoutPackageName(MediaDevice device)150     boolean connectDeviceWithoutPackageName(MediaDevice device) {
151         boolean isConnected = false;
152         final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null);
153         if (info != null) {
154             mRouterManager.transfer(info, device.mRouteInfo);
155             isConnected = true;
156         }
157         return isConnected;
158     }
159 
160     /**
161      * Add a MediaDevice to let it play current media.
162      *
163      * @param device MediaDevice
164      * @return If add device successful return {@code true}, otherwise return {@code false}
165      */
addDeviceToPlayMedia(MediaDevice device)166     boolean addDeviceToPlayMedia(MediaDevice device) {
167         if (TextUtils.isEmpty(mPackageName)) {
168             Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!");
169             return false;
170         }
171 
172         final RoutingSessionInfo info = getRoutingSessionInfo();
173         if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) {
174             mRouterManager.selectRoute(info, device.mRouteInfo);
175             return true;
176         }
177 
178         Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : "
179                 + device.getName());
180 
181         return false;
182     }
183 
getRoutingSessionInfo()184     private RoutingSessionInfo getRoutingSessionInfo() {
185         return getRoutingSessionInfo(mPackageName);
186     }
187 
getRoutingSessionInfo(String packageName)188     private RoutingSessionInfo getRoutingSessionInfo(String packageName) {
189         final List<RoutingSessionInfo> sessionInfos =
190                 mRouterManager.getRoutingSessions(packageName);
191 
192         if (sessionInfos == null || sessionInfos.isEmpty()) {
193             return null;
194         }
195         return sessionInfos.get(sessionInfos.size() - 1);
196     }
197 
isRoutingSessionAvailableForVolumeControl()198     boolean isRoutingSessionAvailableForVolumeControl() {
199         List<RoutingSessionInfo> sessions =
200                 mRouterManager.getRoutingSessions(mPackageName);
201 
202         for (RoutingSessionInfo session : sessions) {
203             if (!session.isSystemSession()
204                     && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
205                 return true;
206             }
207         }
208 
209         Log.d(TAG, "No routing session for " + mPackageName);
210         return false;
211     }
212 
preferRouteListingOrdering()213     boolean preferRouteListingOrdering() {
214         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
215                 && Api34Impl.preferRouteListingOrdering(mRouterManager, mPackageName);
216     }
217 
218     @Nullable
getLinkedItemComponentName()219     ComponentName getLinkedItemComponentName() {
220         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
221             return null;
222         }
223         return Api34Impl.getLinkedItemComponentName(mRouterManager, mPackageName);
224     }
225 
226     /**
227      * Remove a {@code device} from current media.
228      *
229      * @param device MediaDevice
230      * @return If device stop successful return {@code true}, otherwise return {@code false}
231      */
removeDeviceFromPlayMedia(MediaDevice device)232     boolean removeDeviceFromPlayMedia(MediaDevice device) {
233         if (TextUtils.isEmpty(mPackageName)) {
234             Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!");
235             return false;
236         }
237 
238         final RoutingSessionInfo info = getRoutingSessionInfo();
239         if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) {
240             mRouterManager.deselectRoute(info, device.mRouteInfo);
241             return true;
242         }
243 
244         Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : "
245                 + device.getName());
246 
247         return false;
248     }
249 
250     /**
251      * Release session to stop playing media on MediaDevice.
252      */
releaseSession()253     boolean releaseSession() {
254         if (TextUtils.isEmpty(mPackageName)) {
255             Log.w(TAG, "releaseSession() package name is null or empty!");
256             return false;
257         }
258 
259         final RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
260 
261         if (sessionInfo != null) {
262             mRouterManager.releaseSession(sessionInfo);
263             return true;
264         }
265 
266         Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName);
267 
268         return false;
269     }
270 
271     /**
272      * Get the MediaDevice list that can be added to current media.
273      *
274      * @return list of MediaDevice
275      */
getSelectableMediaDevice()276     List<MediaDevice> getSelectableMediaDevice() {
277         final List<MediaDevice> deviceList = new ArrayList<>();
278         if (TextUtils.isEmpty(mPackageName)) {
279             Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!");
280             return deviceList;
281         }
282 
283         final RoutingSessionInfo info = getRoutingSessionInfo();
284         if (info != null) {
285             for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) {
286                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
287                         route, mPackageName, mPreferenceItemMap.get(route.getId())));
288             }
289             return deviceList;
290         }
291 
292         Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : "
293                 + mPackageName);
294 
295         return deviceList;
296     }
297 
298     /**
299      * Get the MediaDevice list that can be removed from current media session.
300      *
301      * @return list of MediaDevice
302      */
getDeselectableMediaDevice()303     List<MediaDevice> getDeselectableMediaDevice() {
304         final List<MediaDevice> deviceList = new ArrayList<>();
305         if (TextUtils.isEmpty(mPackageName)) {
306             Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!");
307             return deviceList;
308         }
309 
310         final RoutingSessionInfo info = getRoutingSessionInfo();
311         if (info != null) {
312             for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) {
313                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
314                         route, mPackageName, mPreferenceItemMap.get(route.getId())));
315                 Log.d(TAG, route.getName() + " is deselectable for " + mPackageName);
316             }
317             return deviceList;
318         }
319         Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : "
320                 + mPackageName);
321 
322         return deviceList;
323     }
324 
325     /**
326      * Get the MediaDevice list that has been selected to current media.
327      *
328      * @return list of MediaDevice
329      */
getSelectedMediaDevice()330     List<MediaDevice> getSelectedMediaDevice() {
331         final List<MediaDevice> deviceList = new ArrayList<>();
332         if (TextUtils.isEmpty(mPackageName)) {
333             Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!");
334             return deviceList;
335         }
336 
337         final RoutingSessionInfo info = getRoutingSessionInfo();
338         if (info != null) {
339             for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) {
340                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
341                         route, mPackageName, mPreferenceItemMap.get(route.getId())));
342             }
343             return deviceList;
344         }
345 
346         Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : "
347                 + mPackageName);
348 
349         return deviceList;
350     }
351 
adjustSessionVolume(RoutingSessionInfo info, int volume)352     void adjustSessionVolume(RoutingSessionInfo info, int volume) {
353         if (info == null) {
354             Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty");
355             return;
356         }
357 
358         mRouterManager.setSessionVolume(info, volume);
359     }
360 
361     /**
362      * Adjust the volume of {@link android.media.RoutingSessionInfo}.
363      *
364      * @param volume the value of volume
365      */
adjustSessionVolume(int volume)366     void adjustSessionVolume(int volume) {
367         if (TextUtils.isEmpty(mPackageName)) {
368             Log.w(TAG, "adjustSessionVolume() package name is null or empty!");
369             return;
370         }
371 
372         final RoutingSessionInfo info = getRoutingSessionInfo();
373         if (info != null) {
374             Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : "
375                     + mPackageName);
376             mRouterManager.setSessionVolume(info, volume);
377             return;
378         }
379 
380         Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : "
381                 + mPackageName);
382     }
383 
384     /**
385      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
386      *
387      * @return  maximum volume of the session, and return -1 if not found.
388      */
getSessionVolumeMax()389     public int getSessionVolumeMax() {
390         if (TextUtils.isEmpty(mPackageName)) {
391             Log.w(TAG, "getSessionVolumeMax() package name is null or empty!");
392             return -1;
393         }
394 
395         final RoutingSessionInfo info = getRoutingSessionInfo();
396         if (info != null) {
397             return info.getVolumeMax();
398         }
399 
400         Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : "
401                 + mPackageName);
402         return -1;
403     }
404 
405     /**
406      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
407      *
408      * @return current volume of the session, and return -1 if not found.
409      */
getSessionVolume()410     public int getSessionVolume() {
411         if (TextUtils.isEmpty(mPackageName)) {
412             Log.w(TAG, "getSessionVolume() package name is null or empty!");
413             return -1;
414         }
415 
416         final RoutingSessionInfo info = getRoutingSessionInfo();
417         if (info != null) {
418             return info.getVolume();
419         }
420 
421         Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : "
422                 + mPackageName);
423         return -1;
424     }
425 
getSessionName()426     CharSequence getSessionName() {
427         if (TextUtils.isEmpty(mPackageName)) {
428             Log.w(TAG, "Unable to get session name. The package name is null or empty!");
429             return null;
430         }
431 
432         final RoutingSessionInfo info = getRoutingSessionInfo();
433         if (info != null) {
434             return info.getName();
435         }
436 
437         Log.w(TAG, "Unable to get session name for package: " + mPackageName);
438         return null;
439     }
440 
shouldDisableMediaOutput(String packageName)441     boolean shouldDisableMediaOutput(String packageName) {
442         if (TextUtils.isEmpty(packageName)) {
443             Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!");
444             return true;
445         }
446 
447         // Disable when there is no transferable route
448         return mRouterManager.getTransferableRoutes(packageName).isEmpty();
449     }
450 
451     @TargetApi(Build.VERSION_CODES.R)
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)452     boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
453         return sessionInfo.isSystemSession() // System sessions are not remote
454                 || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
455     }
456 
refreshDevices()457     private synchronized void refreshDevices() {
458         mMediaDevices.clear();
459         mCurrentConnectedDevice = null;
460         if (TextUtils.isEmpty(mPackageName)) {
461             buildAllRoutes();
462         } else {
463             buildAvailableRoutes();
464         }
465         dispatchDeviceListAdded();
466     }
467 
468     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
469     @SuppressWarnings("NewApi")
buildAllRoutes()470     private void buildAllRoutes() {
471         for (MediaRoute2Info route : mRouterManager.getAllRoutes()) {
472             if (DEBUG) {
473                 Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : "
474                         + route.getVolume() + ", type : " + route.getType());
475             }
476             if (route.isSystemRoute()) {
477                 addMediaDevice(route);
478             }
479         }
480     }
481 
getActiveMediaSession()482     List<RoutingSessionInfo> getActiveMediaSession() {
483         List<RoutingSessionInfo> infos = new ArrayList<>();
484         infos.add(mRouterManager.getSystemRoutingSession(null));
485         infos.addAll(mRouterManager.getRemoteSessions());
486         return infos;
487     }
488 
489     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
490     @SuppressWarnings("NewApi")
buildAvailableRoutes()491     private synchronized void buildAvailableRoutes() {
492         for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) {
493             if (DEBUG) {
494                 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : "
495                         + route.getVolume() + ", type : " + route.getType());
496             }
497             addMediaDevice(route);
498         }
499     }
500 
getAvailableRoutes(String packageName)501     private synchronized List<MediaRoute2Info> getAvailableRoutes(String packageName) {
502         List<MediaRoute2Info> infos = new ArrayList<>();
503         RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName);
504         List<MediaRoute2Info> selectedRouteInfos = new ArrayList<>();
505         if (routingSessionInfo != null) {
506             selectedRouteInfos = mRouterManager.getSelectedRoutes(routingSessionInfo);
507             infos.addAll(selectedRouteInfos);
508             infos.addAll(mRouterManager.getSelectableRoutes(routingSessionInfo));
509         }
510         final List<MediaRoute2Info> transferableRoutes =
511                 mRouterManager.getTransferableRoutes(packageName);
512         for (MediaRoute2Info transferableRoute : transferableRoutes) {
513             boolean alreadyAdded = false;
514             for (MediaRoute2Info mediaRoute2Info : infos) {
515                 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) {
516                     alreadyAdded = true;
517                     break;
518                 }
519             }
520             if (!alreadyAdded) {
521                 infos.add(transferableRoute);
522             }
523         }
524         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
525                 && !TextUtils.isEmpty(mPackageName)) {
526             RouteListingPreference routeListingPreference =
527                     mRouterManager.getRouteListingPreference(mPackageName);
528             if (routeListingPreference != null) {
529                 final List<RouteListingPreference.Item> preferenceRouteListing =
530                         Api34Impl.composePreferenceRouteListing(
531                                 routeListingPreference);
532                 infos = Api34Impl.arrangeRouteListByPreference(selectedRouteInfos,
533                                 mRouterManager.getAvailableRoutes(packageName),
534                                 preferenceRouteListing);
535             }
536             return Api34Impl.filterDuplicatedIds(infos);
537         } else {
538             return infos;
539         }
540     }
541 
542     // MediaRoute2Info.getType was made public on API 34, but exists since API 30.
543     @SuppressWarnings("NewApi")
544     @VisibleForTesting
addMediaDevice(MediaRoute2Info route)545     void addMediaDevice(MediaRoute2Info route) {
546         final int deviceType = route.getType();
547         MediaDevice mediaDevice = null;
548         switch (deviceType) {
549             case TYPE_UNKNOWN:
550             case TYPE_REMOTE_TV:
551             case TYPE_REMOTE_SPEAKER:
552             case TYPE_GROUP:
553             case TYPE_REMOTE_TABLET:
554             case TYPE_REMOTE_TABLET_DOCKED:
555             case TYPE_REMOTE_COMPUTER:
556             case TYPE_REMOTE_GAME_CONSOLE:
557             case TYPE_REMOTE_CAR:
558             case TYPE_REMOTE_SMARTWATCH:
559             case TYPE_REMOTE_SMARTPHONE:
560                 mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route,
561                         mPackageName, mPreferenceItemMap.get(route.getId()));
562                 break;
563             case TYPE_BUILTIN_SPEAKER:
564             case TYPE_USB_DEVICE:
565             case TYPE_USB_HEADSET:
566             case TYPE_USB_ACCESSORY:
567             case TYPE_DOCK:
568             case TYPE_HDMI:
569             case TYPE_WIRED_HEADSET:
570             case TYPE_WIRED_HEADPHONES:
571                 mediaDevice = mPreferenceItemMap.containsKey(route.getId()) ? new PhoneMediaDevice(
572                         mContext, mRouterManager, route, mPackageName,
573                         mPreferenceItemMap.get(route.getId())) : new PhoneMediaDevice(mContext,
574                         mRouterManager, route, mPackageName);
575                 break;
576             case TYPE_HEARING_AID:
577             case TYPE_BLUETOOTH_A2DP:
578             case TYPE_BLE_HEADSET:
579                 final BluetoothDevice device =
580                         BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress());
581                 final CachedBluetoothDevice cachedDevice =
582                         mBluetoothManager.getCachedDeviceManager().findDevice(device);
583                 if (cachedDevice != null) {
584                     mediaDevice = mPreferenceItemMap.containsKey(route.getId())
585                             ? new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager,
586                             route, mPackageName, mPreferenceItemMap.get(route.getId()))
587                             : new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager,
588                                     route, mPackageName);
589                 }
590                 break;
591             case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER:
592                 mediaDevice = new ComplexMediaDevice(mContext, mRouterManager, route,
593                         mPackageName, mPreferenceItemMap.get(route.getId()));
594             default:
595                 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType);
596                 break;
597 
598         }
599         if (mediaDevice != null && !TextUtils.isEmpty(mPackageName)
600                 && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId())) {
601             mediaDevice.setState(STATE_SELECTED);
602             if (mCurrentConnectedDevice == null) {
603                 mCurrentConnectedDevice = mediaDevice;
604             }
605         }
606         if (mediaDevice != null) {
607             mMediaDevices.add(mediaDevice);
608         }
609     }
610 
611     class RouterManagerCallback implements MediaRouter2Manager.Callback {
612 
613         @Override
onRoutesUpdated()614         public void onRoutesUpdated() {
615             refreshDevices();
616         }
617 
618         @Override
onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures)619         public void onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures) {
620             if (TextUtils.equals(mPackageName, packageName)) {
621                 refreshDevices();
622             }
623         }
624 
625         @Override
onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)626         public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) {
627             if (DEBUG) {
628                 Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName()
629                         + ", newSession : " + newSession.getName());
630             }
631             mMediaDevices.clear();
632             mCurrentConnectedDevice = null;
633             if (TextUtils.isEmpty(mPackageName)) {
634                 buildAllRoutes();
635             } else {
636                 buildAvailableRoutes();
637             }
638 
639             final String id = mCurrentConnectedDevice != null
640                     ? mCurrentConnectedDevice.getId()
641                     : null;
642             dispatchConnectedDeviceChanged(id);
643         }
644 
645         /**
646          * Ignore callback here since we'll also receive {@link #onRequestFailed} with reason code.
647          */
648         @Override
onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route)649         public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
650         }
651 
652         @Override
onRequestFailed(int reason)653         public void onRequestFailed(int reason) {
654             dispatchOnRequestFailed(reason);
655         }
656 
657         @Override
onSessionUpdated(RoutingSessionInfo sessionInfo)658         public void onSessionUpdated(RoutingSessionInfo sessionInfo) {
659             refreshDevices();
660         }
661 
662         @Override
onSessionReleased(@onNull RoutingSessionInfo session)663         public void onSessionReleased(@NonNull RoutingSessionInfo session) {
664             refreshDevices();
665         }
666 
667         @Override
onRouteListingPreferenceUpdated( String packageName, RouteListingPreference routeListingPreference)668         public void onRouteListingPreferenceUpdated(
669                 String packageName,
670                 RouteListingPreference routeListingPreference) {
671             if (TextUtils.equals(mPackageName, packageName)) {
672                 Api34Impl.onRouteListingPreferenceUpdated(
673                         routeListingPreference, mPreferenceItemMap);
674                 refreshDevices();
675             }
676         }
677     }
678 
679     @RequiresApi(34)
680     private static class Api34Impl {
681         @DoNotInline
composePreferenceRouteListing( RouteListingPreference routeListingPreference)682         static List<RouteListingPreference.Item> composePreferenceRouteListing(
683                 RouteListingPreference routeListingPreference) {
684             List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
685             List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
686             for (RouteListingPreference.Item item : itemList) {
687                 // Put suggested devices on the top first before further organization
688                 if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) {
689                     finalizedItemList.add(0, item);
690                 } else {
691                     finalizedItemList.add(item);
692                 }
693             }
694             return finalizedItemList;
695         }
696 
697         @DoNotInline
filterDuplicatedIds(List<MediaRoute2Info> infos)698         static synchronized List<MediaRoute2Info> filterDuplicatedIds(List<MediaRoute2Info> infos) {
699             List<MediaRoute2Info> filteredInfos = new ArrayList<>();
700             Set<String> foundDeduplicationIds = new HashSet<>();
701             for (MediaRoute2Info mediaRoute2Info : infos) {
702                 if (!Collections.disjoint(mediaRoute2Info.getDeduplicationIds(),
703                         foundDeduplicationIds)) {
704                     continue;
705                 }
706                 filteredInfos.add(mediaRoute2Info);
707                 foundDeduplicationIds.addAll(mediaRoute2Info.getDeduplicationIds());
708             }
709             return filteredInfos;
710         }
711 
712         @DoNotInline
arrangeRouteListByPreference( List<MediaRoute2Info> selectedRouteInfos, List<MediaRoute2Info> infolist, List<RouteListingPreference.Item> preferenceRouteListing)713         static List<MediaRoute2Info> arrangeRouteListByPreference(
714                 List<MediaRoute2Info> selectedRouteInfos, List<MediaRoute2Info> infolist,
715                 List<RouteListingPreference.Item> preferenceRouteListing) {
716             final List<MediaRoute2Info> sortedInfoList = new ArrayList<>(selectedRouteInfos);
717             infolist.removeAll(selectedRouteInfos);
718             sortedInfoList.addAll(infolist.stream().filter(
719                     MediaRoute2Info::isSystemRoute).collect(Collectors.toList()));
720             for (RouteListingPreference.Item item : preferenceRouteListing) {
721                 for (MediaRoute2Info info : infolist) {
722                     if (item.getRouteId().equals(info.getId())
723                             && !selectedRouteInfos.contains(info)
724                             && !info.isSystemRoute()) {
725                         sortedInfoList.add(info);
726                         break;
727                     }
728                 }
729             }
730             return sortedInfoList;
731         }
732 
733         @DoNotInline
preferRouteListingOrdering(MediaRouter2Manager mediaRouter2Manager, String packageName)734         static boolean preferRouteListingOrdering(MediaRouter2Manager mediaRouter2Manager,
735                 String packageName) {
736             if (TextUtils.isEmpty(packageName)) {
737                 return false;
738             }
739             RouteListingPreference routeListingPreference =
740                     mediaRouter2Manager.getRouteListingPreference(packageName);
741             return routeListingPreference != null
742                     && !routeListingPreference.getUseSystemOrdering();
743         }
744 
745         @DoNotInline
746         @Nullable
getLinkedItemComponentName( MediaRouter2Manager mediaRouter2Manager, String packageName)747         static ComponentName getLinkedItemComponentName(
748                 MediaRouter2Manager mediaRouter2Manager, String packageName) {
749             if (TextUtils.isEmpty(packageName)) {
750                 return null;
751             }
752             RouteListingPreference routeListingPreference =
753                     mediaRouter2Manager.getRouteListingPreference(packageName);
754             return routeListingPreference == null ? null
755                     : routeListingPreference.getLinkedItemComponentName();
756         }
757 
758         @DoNotInline
onRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap)759         static void onRouteListingPreferenceUpdated(
760                 RouteListingPreference routeListingPreference,
761                 Map<String, RouteListingPreference.Item> preferenceItemMap) {
762             preferenceItemMap.clear();
763             if (routeListingPreference != null) {
764                 routeListingPreference.getItems().forEach((item) -> {
765                     preferenceItemMap.put(item.getRouteId(), item);
766                 });
767             }
768         }
769     }
770 }
771