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