• 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_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_SPEAKER;
26 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
27 import static android.media.MediaRoute2Info.TYPE_UNKNOWN;
28 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY;
29 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
30 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
31 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
32 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
33 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
34 
35 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED;
36 
37 import android.annotation.TargetApi;
38 import android.app.Notification;
39 import android.bluetooth.BluetoothAdapter;
40 import android.bluetooth.BluetoothDevice;
41 import android.content.Context;
42 import android.media.MediaRoute2Info;
43 import android.media.MediaRouter2Manager;
44 import android.media.RoutingSessionInfo;
45 import android.os.Build;
46 import android.text.TextUtils;
47 import android.util.Log;
48 
49 import androidx.annotation.RequiresApi;
50 
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
53 import com.android.settingslib.bluetooth.LocalBluetoothManager;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.concurrent.Executor;
58 import java.util.concurrent.Executors;
59 
60 /**
61  * InfoMediaManager provide interface to get InfoMediaDevice list.
62  */
63 @RequiresApi(Build.VERSION_CODES.R)
64 public class InfoMediaManager extends MediaManager {
65 
66     private static final String TAG = "InfoMediaManager";
67     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
68     @VisibleForTesting
69     final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback();
70     @VisibleForTesting
71     final Executor mExecutor = Executors.newSingleThreadExecutor();
72     @VisibleForTesting
73     MediaRouter2Manager mRouterManager;
74     @VisibleForTesting
75     String mPackageName;
76     private final boolean mVolumeAdjustmentForRemoteGroupSessions;
77 
78     private MediaDevice mCurrentConnectedDevice;
79     private LocalBluetoothManager mBluetoothManager;
80 
InfoMediaManager(Context context, String packageName, Notification notification, LocalBluetoothManager localBluetoothManager)81     public InfoMediaManager(Context context, String packageName, Notification notification,
82             LocalBluetoothManager localBluetoothManager) {
83         super(context, notification);
84 
85         mRouterManager = MediaRouter2Manager.getInstance(context);
86         mBluetoothManager = localBluetoothManager;
87         if (!TextUtils.isEmpty(packageName)) {
88             mPackageName = packageName;
89         }
90 
91         mVolumeAdjustmentForRemoteGroupSessions = context.getResources().getBoolean(
92                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
93     }
94 
95     @Override
startScan()96     public void startScan() {
97         mMediaDevices.clear();
98         mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
99         mRouterManager.registerScanRequest();
100         refreshDevices();
101     }
102 
103     @Override
stopScan()104     public void stopScan() {
105         mRouterManager.unregisterCallback(mMediaRouterCallback);
106         mRouterManager.unregisterScanRequest();
107     }
108 
109     /**
110      * Get current device that played media.
111      * @return MediaDevice
112      */
getCurrentConnectedDevice()113     MediaDevice getCurrentConnectedDevice() {
114         return mCurrentConnectedDevice;
115     }
116 
117     /**
118      * Transfer MediaDevice for media without package name.
119      */
connectDeviceWithoutPackageName(MediaDevice device)120     boolean connectDeviceWithoutPackageName(MediaDevice device) {
121         boolean isConnected = false;
122         final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null);
123         if (info != null) {
124             mRouterManager.transfer(info, device.mRouteInfo);
125             isConnected = true;
126         }
127         return isConnected;
128     }
129 
130     /**
131      * Add a MediaDevice to let it play current media.
132      *
133      * @param device MediaDevice
134      * @return If add device successful return {@code true}, otherwise return {@code false}
135      */
addDeviceToPlayMedia(MediaDevice device)136     boolean addDeviceToPlayMedia(MediaDevice device) {
137         if (TextUtils.isEmpty(mPackageName)) {
138             Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!");
139             return false;
140         }
141 
142         final RoutingSessionInfo info = getRoutingSessionInfo();
143         if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) {
144             mRouterManager.selectRoute(info, device.mRouteInfo);
145             return true;
146         }
147 
148         Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : "
149                 + device.getName());
150 
151         return false;
152     }
153 
getRoutingSessionInfo()154     private RoutingSessionInfo getRoutingSessionInfo() {
155         return getRoutingSessionInfo(mPackageName);
156     }
157 
getRoutingSessionInfo(String packageName)158     private RoutingSessionInfo getRoutingSessionInfo(String packageName) {
159         final List<RoutingSessionInfo> sessionInfos =
160                 mRouterManager.getRoutingSessions(packageName);
161 
162         if (sessionInfos == null || sessionInfos.isEmpty()) {
163             return null;
164         }
165         return sessionInfos.get(sessionInfos.size() - 1);
166     }
167 
isRoutingSessionAvailableForVolumeControl()168     boolean isRoutingSessionAvailableForVolumeControl() {
169         if (mVolumeAdjustmentForRemoteGroupSessions) {
170             return true;
171         }
172         List<RoutingSessionInfo> sessions =
173                 mRouterManager.getRoutingSessions(mPackageName);
174         boolean foundNonSystemSession = false;
175         boolean isGroup = false;
176         for (RoutingSessionInfo session : sessions) {
177             if (!session.isSystemSession()) {
178                 foundNonSystemSession = true;
179                 int selectedRouteCount = session.getSelectedRoutes().size();
180                 if (selectedRouteCount > 1) {
181                     isGroup = true;
182                     break;
183                 }
184             }
185         }
186         if (!foundNonSystemSession) {
187             Log.d(TAG, "No routing session for " + mPackageName);
188             return false;
189         }
190         return !isGroup;
191     }
192 
preferRouteListingOrdering()193     boolean preferRouteListingOrdering() {
194         return false;
195     }
196 
197     /**
198      * Remove a {@code device} from current media.
199      *
200      * @param device MediaDevice
201      * @return If device stop successful return {@code true}, otherwise return {@code false}
202      */
removeDeviceFromPlayMedia(MediaDevice device)203     boolean removeDeviceFromPlayMedia(MediaDevice device) {
204         if (TextUtils.isEmpty(mPackageName)) {
205             Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!");
206             return false;
207         }
208 
209         final RoutingSessionInfo info = getRoutingSessionInfo();
210         if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) {
211             mRouterManager.deselectRoute(info, device.mRouteInfo);
212             return true;
213         }
214 
215         Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : "
216                 + device.getName());
217 
218         return false;
219     }
220 
221     /**
222      * Release session to stop playing media on MediaDevice.
223      */
releaseSession()224     boolean releaseSession() {
225         if (TextUtils.isEmpty(mPackageName)) {
226             Log.w(TAG, "releaseSession() package name is null or empty!");
227             return false;
228         }
229 
230         final RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
231 
232         if (sessionInfo != null) {
233             mRouterManager.releaseSession(sessionInfo);
234             return true;
235         }
236 
237         Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName);
238 
239         return false;
240     }
241 
242     /**
243      * Get the MediaDevice list that can be added to current media.
244      *
245      * @return list of MediaDevice
246      */
getSelectableMediaDevice()247     List<MediaDevice> getSelectableMediaDevice() {
248         final List<MediaDevice> deviceList = new ArrayList<>();
249         if (TextUtils.isEmpty(mPackageName)) {
250             Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!");
251             return deviceList;
252         }
253 
254         final RoutingSessionInfo info = getRoutingSessionInfo();
255         if (info != null) {
256             for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) {
257                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
258                         route, mPackageName));
259             }
260             return deviceList;
261         }
262 
263         Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : "
264                 + mPackageName);
265 
266         return deviceList;
267     }
268 
269     /**
270      * Get the MediaDevice list that can be removed from current media session.
271      *
272      * @return list of MediaDevice
273      */
getDeselectableMediaDevice()274     List<MediaDevice> getDeselectableMediaDevice() {
275         final List<MediaDevice> deviceList = new ArrayList<>();
276         if (TextUtils.isEmpty(mPackageName)) {
277             Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!");
278             return deviceList;
279         }
280 
281         final RoutingSessionInfo info = getRoutingSessionInfo();
282         if (info != null) {
283             for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) {
284                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
285                         route, mPackageName));
286                 Log.d(TAG, route.getName() + " is deselectable for " + mPackageName);
287             }
288             return deviceList;
289         }
290         Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : "
291                 + mPackageName);
292 
293         return deviceList;
294     }
295 
296     /**
297      * Get the MediaDevice list that has been selected to current media.
298      *
299      * @return list of MediaDevice
300      */
getSelectedMediaDevice()301     List<MediaDevice> getSelectedMediaDevice() {
302         final List<MediaDevice> deviceList = new ArrayList<>();
303         if (TextUtils.isEmpty(mPackageName)) {
304             Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!");
305             return deviceList;
306         }
307 
308         final RoutingSessionInfo info = getRoutingSessionInfo();
309         if (info != null) {
310             for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) {
311                 deviceList.add(new InfoMediaDevice(mContext, mRouterManager,
312                         route, mPackageName));
313             }
314             return deviceList;
315         }
316 
317         Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : "
318                 + mPackageName);
319 
320         return deviceList;
321     }
322 
adjustSessionVolume(RoutingSessionInfo info, int volume)323     void adjustSessionVolume(RoutingSessionInfo info, int volume) {
324         if (info == null) {
325             Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty");
326             return;
327         }
328 
329         mRouterManager.setSessionVolume(info, volume);
330     }
331 
332     /**
333      * Adjust the volume of {@link android.media.RoutingSessionInfo}.
334      *
335      * @param volume the value of volume
336      */
adjustSessionVolume(int volume)337     void adjustSessionVolume(int volume) {
338         if (TextUtils.isEmpty(mPackageName)) {
339             Log.w(TAG, "adjustSessionVolume() package name is null or empty!");
340             return;
341         }
342 
343         final RoutingSessionInfo info = getRoutingSessionInfo();
344         if (info != null) {
345             Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : "
346                     + mPackageName);
347             mRouterManager.setSessionVolume(info, volume);
348             return;
349         }
350 
351         Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : "
352                 + mPackageName);
353     }
354 
355     /**
356      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
357      *
358      * @return  maximum volume of the session, and return -1 if not found.
359      */
getSessionVolumeMax()360     public int getSessionVolumeMax() {
361         if (TextUtils.isEmpty(mPackageName)) {
362             Log.w(TAG, "getSessionVolumeMax() package name is null or empty!");
363             return -1;
364         }
365 
366         final RoutingSessionInfo info = getRoutingSessionInfo();
367         if (info != null) {
368             return info.getVolumeMax();
369         }
370 
371         Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : "
372                 + mPackageName);
373         return -1;
374     }
375 
376     /**
377      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
378      *
379      * @return current volume of the session, and return -1 if not found.
380      */
getSessionVolume()381     public int getSessionVolume() {
382         if (TextUtils.isEmpty(mPackageName)) {
383             Log.w(TAG, "getSessionVolume() package name is null or empty!");
384             return -1;
385         }
386 
387         final RoutingSessionInfo info = getRoutingSessionInfo();
388         if (info != null) {
389             return info.getVolume();
390         }
391 
392         Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : "
393                 + mPackageName);
394         return -1;
395     }
396 
getSessionName()397     CharSequence getSessionName() {
398         if (TextUtils.isEmpty(mPackageName)) {
399             Log.w(TAG, "Unable to get session name. The package name is null or empty!");
400             return null;
401         }
402 
403         final RoutingSessionInfo info = getRoutingSessionInfo();
404         if (info != null) {
405             return info.getName();
406         }
407 
408         Log.w(TAG, "Unable to get session name for package: " + mPackageName);
409         return null;
410     }
411 
shouldDisableMediaOutput(String packageName)412     boolean shouldDisableMediaOutput(String packageName) {
413         if (TextUtils.isEmpty(packageName)) {
414             Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!");
415             return true;
416         }
417 
418         // Disable when there is no transferable route
419         return mRouterManager.getTransferableRoutes(packageName).isEmpty();
420     }
421 
422     @TargetApi(Build.VERSION_CODES.R)
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)423     boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
424         return sessionInfo.isSystemSession() // System sessions are not remote
425                 || mVolumeAdjustmentForRemoteGroupSessions
426                 || sessionInfo.getSelectedRoutes().size() <= 1;
427     }
428 
refreshDevices()429     private synchronized void refreshDevices() {
430         mMediaDevices.clear();
431         mCurrentConnectedDevice = null;
432         if (TextUtils.isEmpty(mPackageName)) {
433             buildAllRoutes();
434         } else {
435             buildAvailableRoutes();
436         }
437         dispatchDeviceListAdded();
438     }
439 
buildAllRoutes()440     private void buildAllRoutes() {
441         for (MediaRoute2Info route : mRouterManager.getAllRoutes()) {
442             if (DEBUG) {
443                 Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : "
444                         + route.getVolume() + ", type : " + route.getType());
445             }
446             if (route.isSystemRoute()) {
447                 addMediaDevice(route);
448             }
449         }
450     }
451 
getActiveMediaSession()452     List<RoutingSessionInfo> getActiveMediaSession() {
453         List<RoutingSessionInfo> infos = new ArrayList<>();
454         infos.add(mRouterManager.getSystemRoutingSession(null));
455         infos.addAll(mRouterManager.getRemoteSessions());
456         return infos;
457     }
458 
buildAvailableRoutes()459     private synchronized void buildAvailableRoutes() {
460         for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) {
461             if (DEBUG) {
462                 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : "
463                         + route.getVolume() + ", type : " + route.getType());
464             }
465             addMediaDevice(route);
466         }
467     }
468 
getAvailableRoutes(String packageName)469     private synchronized List<MediaRoute2Info> getAvailableRoutes(String packageName) {
470         final List<MediaRoute2Info> infos = new ArrayList<>();
471         RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName);
472         if (routingSessionInfo != null) {
473             infos.addAll(mRouterManager.getSelectedRoutes(routingSessionInfo));
474             infos.addAll(mRouterManager.getSelectableRoutes(routingSessionInfo));
475         }
476         final List<MediaRoute2Info> transferableRoutes =
477                 mRouterManager.getTransferableRoutes(packageName);
478         for (MediaRoute2Info transferableRoute : transferableRoutes) {
479             boolean alreadyAdded = false;
480             for (MediaRoute2Info mediaRoute2Info : infos) {
481                 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) {
482                     alreadyAdded = true;
483                     break;
484                 }
485             }
486             if (!alreadyAdded) {
487                 infos.add(transferableRoute);
488             }
489         }
490         return infos;
491     }
492 
493     @VisibleForTesting
addMediaDevice(MediaRoute2Info route)494     void addMediaDevice(MediaRoute2Info route) {
495         final int deviceType = route.getType();
496         MediaDevice mediaDevice = null;
497         switch (deviceType) {
498             case TYPE_UNKNOWN:
499             case TYPE_REMOTE_TV:
500             case TYPE_REMOTE_SPEAKER:
501             case TYPE_GROUP:
502                 //TODO(b/148765806): use correct device type once api is ready.
503                 mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route,
504                         mPackageName);
505                 if (!TextUtils.isEmpty(mPackageName)
506                         && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId())) {
507                     mediaDevice.setState(STATE_SELECTED);
508                     if (mCurrentConnectedDevice == null) {
509                         mCurrentConnectedDevice = mediaDevice;
510                     }
511                 }
512                 break;
513             case TYPE_BUILTIN_SPEAKER:
514             case TYPE_USB_DEVICE:
515             case TYPE_USB_HEADSET:
516             case TYPE_USB_ACCESSORY:
517             case TYPE_DOCK:
518             case TYPE_HDMI:
519             case TYPE_WIRED_HEADSET:
520             case TYPE_WIRED_HEADPHONES:
521                 mediaDevice =
522                         new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName);
523                 break;
524             case TYPE_HEARING_AID:
525             case TYPE_BLUETOOTH_A2DP:
526             case TYPE_BLE_HEADSET:
527                 final BluetoothDevice device =
528                         BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress());
529                 final CachedBluetoothDevice cachedDevice =
530                         mBluetoothManager.getCachedDeviceManager().findDevice(device);
531                 if (cachedDevice != null) {
532                     mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager,
533                             route, mPackageName);
534                 }
535                 break;
536             default:
537                 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType);
538                 break;
539 
540         }
541 
542         if (mediaDevice != null) {
543             mMediaDevices.add(mediaDevice);
544         }
545     }
546 
547     class RouterManagerCallback implements MediaRouter2Manager.Callback {
548 
549         @Override
onRoutesAdded(List<MediaRoute2Info> routes)550         public void onRoutesAdded(List<MediaRoute2Info> routes) {
551             refreshDevices();
552         }
553 
554         @Override
onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures)555         public void onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures) {
556             if (TextUtils.equals(mPackageName, packageName)) {
557                 refreshDevices();
558             }
559         }
560 
561         @Override
onRoutesChanged(List<MediaRoute2Info> routes)562         public void onRoutesChanged(List<MediaRoute2Info> routes) {
563             refreshDevices();
564         }
565 
566         @Override
onRoutesRemoved(List<MediaRoute2Info> routes)567         public void onRoutesRemoved(List<MediaRoute2Info> routes) {
568             refreshDevices();
569         }
570 
571         @Override
onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)572         public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) {
573             if (DEBUG) {
574                 Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName()
575                         + ", newSession : " + newSession.getName());
576             }
577             mMediaDevices.clear();
578             mCurrentConnectedDevice = null;
579             if (TextUtils.isEmpty(mPackageName)) {
580                 buildAllRoutes();
581             } else {
582                 buildAvailableRoutes();
583             }
584 
585             final String id = mCurrentConnectedDevice != null
586                     ? mCurrentConnectedDevice.getId()
587                     : null;
588             dispatchConnectedDeviceChanged(id);
589         }
590 
591         @Override
onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route)592         public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) {
593             dispatchOnRequestFailed(REASON_UNKNOWN_ERROR);
594         }
595 
596         @Override
onRequestFailed(int reason)597         public void onRequestFailed(int reason) {
598             dispatchOnRequestFailed(reason);
599         }
600 
601         @Override
onSessionUpdated(RoutingSessionInfo sessionInfo)602         public void onSessionUpdated(RoutingSessionInfo sessionInfo) {
603             refreshDevices();
604         }
605     }
606 }
607