• 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.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
19 
20 import android.app.Notification;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.Context;
24 import android.graphics.drawable.Drawable;
25 import android.media.AudioDeviceAttributes;
26 import android.media.AudioManager;
27 import android.media.RoutingSessionInfo;
28 import android.os.Build;
29 import android.text.TextUtils;
30 import android.util.Log;
31 
32 import androidx.annotation.IntDef;
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.RequiresApi;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 import com.android.settingslib.bluetooth.A2dpProfile;
39 import com.android.settingslib.bluetooth.BluetoothCallback;
40 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
41 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
42 import com.android.settingslib.bluetooth.HearingAidProfile;
43 import com.android.settingslib.bluetooth.LeAudioProfile;
44 import com.android.settingslib.bluetooth.LocalBluetoothManager;
45 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
46 
47 import java.lang.annotation.Retention;
48 import java.lang.annotation.RetentionPolicy;
49 import java.util.ArrayList;
50 import java.util.Collection;
51 import java.util.List;
52 import java.util.concurrent.CopyOnWriteArrayList;
53 
54 /**
55  * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice.
56  */
57 @RequiresApi(Build.VERSION_CODES.R)
58 public class LocalMediaManager implements BluetoothCallback {
59     private static final String TAG = "LocalMediaManager";
60     private static final int MAX_DISCONNECTED_DEVICE_NUM = 5;
61 
62     @Retention(RetentionPolicy.SOURCE)
63     @IntDef({MediaDeviceState.STATE_CONNECTED,
64             MediaDeviceState.STATE_CONNECTING,
65             MediaDeviceState.STATE_DISCONNECTED,
66             MediaDeviceState.STATE_CONNECTING_FAILED,
67             MediaDeviceState.STATE_SELECTED,
68             MediaDeviceState.STATE_GROUPING})
69     public @interface MediaDeviceState {
70         int STATE_CONNECTED = 0;
71         int STATE_CONNECTING = 1;
72         int STATE_DISCONNECTED = 2;
73         int STATE_CONNECTING_FAILED = 3;
74         int STATE_SELECTED = 4;
75         int STATE_GROUPING = 5;
76     }
77 
78     private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
79     private final Object mMediaDevicesLock = new Object();
80     @VisibleForTesting
81     final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback();
82 
83     private Context mContext;
84     private LocalBluetoothManager mLocalBluetoothManager;
85     private InfoMediaManager mInfoMediaManager;
86     private String mPackageName;
87     private MediaDevice mOnTransferBluetoothDevice;
88     @VisibleForTesting
89     AudioManager mAudioManager;
90 
91     @VisibleForTesting
92     List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
93     @VisibleForTesting
94     List<MediaDevice> mDisconnectedMediaDevices = new CopyOnWriteArrayList<>();
95     @VisibleForTesting
96     MediaDevice mPhoneDevice;
97     @VisibleForTesting
98     MediaDevice mCurrentConnectedDevice;
99     @VisibleForTesting
100     DeviceAttributeChangeCallback mDeviceAttributeChangeCallback =
101             new DeviceAttributeChangeCallback();
102     @VisibleForTesting
103     BluetoothAdapter mBluetoothAdapter;
104 
105     /**
106      * Register to start receiving callbacks for MediaDevice events.
107      */
registerCallback(DeviceCallback callback)108     public void registerCallback(DeviceCallback callback) {
109         mCallbacks.add(callback);
110     }
111 
112     /**
113      * Unregister to stop receiving callbacks for MediaDevice events
114      */
unregisterCallback(DeviceCallback callback)115     public void unregisterCallback(DeviceCallback callback) {
116         mCallbacks.remove(callback);
117     }
118 
119     /**
120      * Creates a LocalMediaManager with references to given managers.
121      *
122      * It will obtain a {@link LocalBluetoothManager} by calling
123      * {@link LocalBluetoothManager#getInstance} and create an {@link InfoMediaManager} passing
124      * that bluetooth manager.
125      *
126      * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter.
127      */
LocalMediaManager(Context context, String packageName, Notification notification)128     public LocalMediaManager(Context context, String packageName, Notification notification) {
129         mContext = context;
130         mPackageName = packageName;
131         mLocalBluetoothManager =
132                 LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null);
133         mAudioManager = context.getSystemService(AudioManager.class);
134         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
135         if (mLocalBluetoothManager == null) {
136             Log.e(TAG, "Bluetooth is not supported on this device");
137             return;
138         }
139 
140         mInfoMediaManager =
141                 new InfoMediaManager(context, packageName, notification, mLocalBluetoothManager);
142     }
143 
144     /**
145      * Creates a LocalMediaManager with references to given managers.
146      *
147      * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter.
148      */
LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, InfoMediaManager infoMediaManager, String packageName)149     public LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager,
150             InfoMediaManager infoMediaManager, String packageName) {
151         mContext = context;
152         mLocalBluetoothManager = localBluetoothManager;
153         mInfoMediaManager = infoMediaManager;
154         mPackageName = packageName;
155         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
156         mAudioManager = context.getSystemService(AudioManager.class);
157     }
158 
159     /**
160      * Connect the MediaDevice to transfer media
161      * @param connectDevice the MediaDevice
162      * @return {@code true} if successfully call, otherwise return {@code false}
163      */
connectDevice(MediaDevice connectDevice)164     public boolean connectDevice(MediaDevice connectDevice) {
165         MediaDevice device = null;
166         synchronized (mMediaDevicesLock) {
167             device = getMediaDeviceById(mMediaDevices, connectDevice.getId());
168         }
169         if (device == null) {
170             Log.w(TAG, "connectDevice() connectDevice not in the list!");
171             return false;
172         }
173         if (device instanceof BluetoothMediaDevice) {
174             final CachedBluetoothDevice cachedDevice =
175                     ((BluetoothMediaDevice) device).getCachedDevice();
176             if (!cachedDevice.isConnected() && !cachedDevice.isBusy()) {
177                 mOnTransferBluetoothDevice = connectDevice;
178                 device.setState(MediaDeviceState.STATE_CONNECTING);
179                 cachedDevice.connect();
180                 return true;
181             }
182         }
183 
184         if (device.equals(mCurrentConnectedDevice)) {
185             Log.d(TAG, "connectDevice() this device is already connected! : " + device.getName());
186             return false;
187         }
188 
189         if (mCurrentConnectedDevice != null) {
190             mCurrentConnectedDevice.disconnect();
191         }
192 
193         device.setState(MediaDeviceState.STATE_CONNECTING);
194         if (TextUtils.isEmpty(mPackageName)) {
195             mInfoMediaManager.connectDeviceWithoutPackageName(device);
196         } else {
197             device.connect();
198         }
199         return true;
200     }
201 
dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)202     void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
203         for (DeviceCallback callback : getCallbacks()) {
204             callback.onSelectedDeviceStateChanged(device, state);
205         }
206     }
207 
208     /**
209      * Returns if the media session is available for volume control.
210      * @return True if this media session is available for colume control, false otherwise.
211      */
isMediaSessionAvailableForVolumeControl()212     public boolean isMediaSessionAvailableForVolumeControl() {
213         return mInfoMediaManager.isRoutingSessionAvailableForVolumeControl();
214     }
215 
216     /**
217      * Returns if media app establishes a preferred route listing order.
218      *
219      * @return True if route list ordering exist and not using system ordering, false otherwise.
220      */
isPreferenceRouteListingExist()221     public boolean isPreferenceRouteListingExist() {
222         return mInfoMediaManager.preferRouteListingOrdering();
223     }
224 
225     /**
226      * Start scan connected MediaDevice
227      */
startScan()228     public void startScan() {
229         synchronized (mMediaDevicesLock) {
230             mMediaDevices.clear();
231         }
232         mInfoMediaManager.registerCallback(mMediaDeviceCallback);
233         mInfoMediaManager.startScan();
234     }
235 
dispatchDeviceListUpdate()236     void dispatchDeviceListUpdate() {
237         final List<MediaDevice> mediaDevices = new ArrayList<>(mMediaDevices);
238         for (DeviceCallback callback : getCallbacks()) {
239             callback.onDeviceListUpdate(mediaDevices);
240         }
241     }
242 
dispatchDeviceAttributesChanged()243     void dispatchDeviceAttributesChanged() {
244         for (DeviceCallback callback : getCallbacks()) {
245             callback.onDeviceAttributesChanged();
246         }
247     }
248 
dispatchOnRequestFailed(int reason)249     void dispatchOnRequestFailed(int reason) {
250         for (DeviceCallback callback : getCallbacks()) {
251             callback.onRequestFailed(reason);
252         }
253     }
254 
255     /**
256      * Dispatch a change in the about-to-connect device. See
257      * {@link DeviceCallback#onAboutToConnectDeviceAdded} for more information.
258      */
dispatchAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon)259     public void dispatchAboutToConnectDeviceAdded(
260             @NonNull String deviceAddress,
261             @NonNull String deviceName,
262             @Nullable Drawable deviceIcon) {
263         for (DeviceCallback callback : getCallbacks()) {
264             callback.onAboutToConnectDeviceAdded(deviceAddress, deviceName, deviceIcon);
265         }
266     }
267 
268     /**
269      * Dispatch a change in the about-to-connect device. See
270      * {@link DeviceCallback#onAboutToConnectDeviceRemoved} for more information.
271      */
dispatchAboutToConnectDeviceRemoved()272     public void dispatchAboutToConnectDeviceRemoved() {
273         for (DeviceCallback callback : getCallbacks()) {
274             callback.onAboutToConnectDeviceRemoved();
275         }
276     }
277 
278     /**
279      * Stop scan MediaDevice
280      */
stopScan()281     public void stopScan() {
282         mInfoMediaManager.unregisterCallback(mMediaDeviceCallback);
283         mInfoMediaManager.stopScan();
284         unRegisterDeviceAttributeChangeCallback();
285     }
286 
287     /**
288      * Find the MediaDevice through id.
289      *
290      * @param devices the list of MediaDevice
291      * @param id the unique id of MediaDevice
292      * @return MediaDevice
293      */
getMediaDeviceById(List<MediaDevice> devices, String id)294     public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) {
295         for (MediaDevice mediaDevice : devices) {
296             if (TextUtils.equals(mediaDevice.getId(), id)) {
297                 return mediaDevice;
298             }
299         }
300         Log.i(TAG, "getMediaDeviceById() can't found device");
301         return null;
302     }
303 
304     /**
305      * Find the MediaDevice from all media devices by id.
306      *
307      * @param id the unique id of MediaDevice
308      * @return MediaDevice
309      */
getMediaDeviceById(String id)310     public MediaDevice getMediaDeviceById(String id) {
311         synchronized (mMediaDevicesLock) {
312             for (MediaDevice mediaDevice : mMediaDevices) {
313                 if (TextUtils.equals(mediaDevice.getId(), id)) {
314                     return mediaDevice;
315                 }
316             }
317         }
318         Log.i(TAG, "Unable to find device " + id);
319         return null;
320     }
321 
322     /**
323      * Find the current connected MediaDevice.
324      *
325      * @return MediaDevice
326      */
327     @Nullable
getCurrentConnectedDevice()328     public MediaDevice getCurrentConnectedDevice() {
329         return mCurrentConnectedDevice;
330     }
331 
332     /**
333      * Add a MediaDevice to let it play current media.
334      *
335      * @param device MediaDevice
336      * @return If add device successful return {@code true}, otherwise return {@code false}
337      */
addDeviceToPlayMedia(MediaDevice device)338     public boolean addDeviceToPlayMedia(MediaDevice device) {
339         device.setState(MediaDeviceState.STATE_GROUPING);
340         return mInfoMediaManager.addDeviceToPlayMedia(device);
341     }
342 
343     /**
344      * Remove a {@code device} from current media.
345      *
346      * @param device MediaDevice
347      * @return If device stop successful return {@code true}, otherwise return {@code false}
348      */
removeDeviceFromPlayMedia(MediaDevice device)349     public boolean removeDeviceFromPlayMedia(MediaDevice device) {
350         device.setState(MediaDeviceState.STATE_GROUPING);
351         return mInfoMediaManager.removeDeviceFromPlayMedia(device);
352     }
353 
354     /**
355      * Get the MediaDevice list that can be added to current media.
356      *
357      * @return list of MediaDevice
358      */
getSelectableMediaDevice()359     public List<MediaDevice> getSelectableMediaDevice() {
360         return mInfoMediaManager.getSelectableMediaDevice();
361     }
362 
363     /**
364      * Get the MediaDevice list that can be removed from current media session.
365      *
366      * @return list of MediaDevice
367      */
getDeselectableMediaDevice()368     public List<MediaDevice> getDeselectableMediaDevice() {
369         return mInfoMediaManager.getDeselectableMediaDevice();
370     }
371 
372     /**
373      * Release session to stop playing media on MediaDevice.
374      */
releaseSession()375     public boolean releaseSession() {
376         return mInfoMediaManager.releaseSession();
377     }
378 
379     /**
380      * Get the MediaDevice list that has been selected to current media.
381      *
382      * @return list of MediaDevice
383      */
getSelectedMediaDevice()384     public List<MediaDevice> getSelectedMediaDevice() {
385         return mInfoMediaManager.getSelectedMediaDevice();
386     }
387 
388     /**
389      * Adjust the volume of session.
390      *
391      * @param sessionId the value of media session id
392      * @param volume the value of volume
393      */
adjustSessionVolume(String sessionId, int volume)394     public void adjustSessionVolume(String sessionId, int volume) {
395         final List<RoutingSessionInfo> infos = getActiveMediaSession();
396         for (RoutingSessionInfo info : infos) {
397             if (TextUtils.equals(sessionId, info.getId())) {
398                 mInfoMediaManager.adjustSessionVolume(info, volume);
399                 return;
400             }
401         }
402         Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId);
403     }
404 
405     /**
406      * Adjust the volume of session.
407      *
408      * @param volume the value of volume
409      */
adjustSessionVolume(int volume)410     public void adjustSessionVolume(int volume) {
411         mInfoMediaManager.adjustSessionVolume(volume);
412     }
413 
414     /**
415      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
416      *
417      * @return  maximum volume of the session, and return -1 if not found.
418      */
getSessionVolumeMax()419     public int getSessionVolumeMax() {
420         return mInfoMediaManager.getSessionVolumeMax();
421     }
422 
423     /**
424      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
425      *
426      * @return current volume of the session, and return -1 if not found.
427      */
getSessionVolume()428     public int getSessionVolume() {
429         return mInfoMediaManager.getSessionVolume();
430     }
431 
432     /**
433      * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}.
434      *
435      * @return current name of the session, and return {@code null} if not found.
436      */
getSessionName()437     public CharSequence getSessionName() {
438         return mInfoMediaManager.getSessionName();
439     }
440 
441     /**
442      * Gets the current active session.
443      *
444      * @return current active session list{@link android.media.RoutingSessionInfo}
445      */
getActiveMediaSession()446     public List<RoutingSessionInfo> getActiveMediaSession() {
447         return mInfoMediaManager.getActiveMediaSession();
448     }
449 
450     /**
451      * Gets the current package name.
452      *
453      * @return current package name
454      */
getPackageName()455     public String getPackageName() {
456         return mPackageName;
457     }
458 
459     /**
460      * Returns {@code true} if needed to disable media output, otherwise returns {@code false}.
461      */
shouldDisableMediaOutput(String packageName)462     public boolean shouldDisableMediaOutput(String packageName) {
463         return mInfoMediaManager.shouldDisableMediaOutput(packageName);
464     }
465 
466     /**
467      * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}.
468      */
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)469     public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
470         return mInfoMediaManager.shouldEnableVolumeSeekBar(sessionInfo);
471     }
472 
473     @VisibleForTesting
updateCurrentConnectedDevice()474     MediaDevice updateCurrentConnectedDevice() {
475         MediaDevice connectedDevice = null;
476         synchronized (mMediaDevicesLock) {
477             for (MediaDevice device : mMediaDevices) {
478                 if (device instanceof BluetoothMediaDevice) {
479                     if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice())
480                             && device.isConnected()) {
481                         return device;
482                     }
483                 } else if (device instanceof PhoneMediaDevice) {
484                     connectedDevice = device;
485                 }
486             }
487         }
488 
489         return connectedDevice;
490     }
491 
isActiveDevice(CachedBluetoothDevice device)492     private boolean isActiveDevice(CachedBluetoothDevice device) {
493         boolean isActiveDeviceA2dp = false;
494         boolean isActiveDeviceHearingAid = false;
495         boolean isActiveLeAudio = false;
496         final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile();
497         if (a2dpProfile != null) {
498             isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice());
499         }
500         if (!isActiveDeviceA2dp) {
501             final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager()
502                     .getHearingAidProfile();
503             if (hearingAidProfile != null) {
504                 isActiveDeviceHearingAid =
505                         hearingAidProfile.getActiveDevices().contains(device.getDevice());
506             }
507         }
508 
509         if (!isActiveDeviceA2dp && !isActiveDeviceHearingAid) {
510             final LeAudioProfile leAudioProfile = mLocalBluetoothManager.getProfileManager()
511                     .getLeAudioProfile();
512             if (leAudioProfile != null) {
513                 isActiveLeAudio = leAudioProfile.getActiveDevices().contains(device.getDevice());
514             }
515         }
516 
517         return isActiveDeviceA2dp || isActiveDeviceHearingAid || isActiveLeAudio;
518     }
519 
getCallbacks()520     private Collection<DeviceCallback> getCallbacks() {
521         return new CopyOnWriteArrayList<>(mCallbacks);
522     }
523 
524     class MediaDeviceCallback implements MediaManager.MediaDeviceCallback {
525         @Override
onDeviceAdded(MediaDevice device)526         public void onDeviceAdded(MediaDevice device) {
527             boolean isAdded = false;
528             synchronized (mMediaDevicesLock) {
529                 if (!mMediaDevices.contains(device)) {
530                     mMediaDevices.add(device);
531                     isAdded = true;
532                 }
533             }
534 
535             if (isAdded) {
536                 dispatchDeviceListUpdate();
537             }
538         }
539 
540         @Override
onDeviceListAdded(List<MediaDevice> devices)541         public void onDeviceListAdded(List<MediaDevice> devices) {
542             synchronized (mMediaDevicesLock) {
543                 mMediaDevices.clear();
544                 mMediaDevices.addAll(devices);
545                 // Add muting expected bluetooth devices only when phone output device is available.
546                 for (MediaDevice device : devices) {
547                     final int type = device.getDeviceType();
548                     if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE
549                             || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
550                             || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) {
551                         MediaDevice mutingExpectedDevice = getMutingExpectedDevice();
552                         if (mutingExpectedDevice != null) {
553                             mMediaDevices.add(mutingExpectedDevice);
554                         }
555                         break;
556                     }
557                 }
558             }
559 
560             final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice();
561             mCurrentConnectedDevice = infoMediaDevice != null
562                     ? infoMediaDevice : updateCurrentConnectedDevice();
563             dispatchDeviceListUpdate();
564             if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) {
565                 connectDevice(mOnTransferBluetoothDevice);
566                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED);
567                 dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice,
568                         MediaDeviceState.STATE_CONNECTED);
569                 mOnTransferBluetoothDevice = null;
570             }
571         }
572 
getMutingExpectedDevice()573         private MediaDevice getMutingExpectedDevice() {
574             if (mBluetoothAdapter == null
575                     || mAudioManager.getMutingExpectedDevice() == null) {
576                 Log.w(TAG, "BluetoothAdapter is null or muting expected device not exist");
577                 return null;
578             }
579             final List<BluetoothDevice> bluetoothDevices =
580                     mBluetoothAdapter.getMostRecentlyConnectedDevices();
581             final CachedBluetoothDeviceManager cachedDeviceManager =
582                     mLocalBluetoothManager.getCachedDeviceManager();
583             for (BluetoothDevice device : bluetoothDevices) {
584                 final CachedBluetoothDevice cachedDevice =
585                         cachedDeviceManager.findDevice(device);
586                 if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) {
587                     return new BluetoothMediaDevice(mContext,
588                             cachedDevice,
589                             null, null, mPackageName);
590                 }
591             }
592             return null;
593         }
594 
isMutingExpectedDevice(CachedBluetoothDevice cachedDevice)595         private boolean isMutingExpectedDevice(CachedBluetoothDevice cachedDevice) {
596             AudioDeviceAttributes mutingExpectedDevice = mAudioManager.getMutingExpectedDevice();
597             if (mutingExpectedDevice == null || cachedDevice == null) {
598                 return false;
599             }
600             return cachedDevice.getAddress().equals(mutingExpectedDevice.getAddress());
601         }
602 
buildDisconnectedBluetoothDevice()603         private List<MediaDevice> buildDisconnectedBluetoothDevice() {
604             if (mBluetoothAdapter == null) {
605                 Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null");
606                 return new ArrayList<>();
607             }
608 
609             final List<BluetoothDevice> bluetoothDevices =
610                     mBluetoothAdapter.getMostRecentlyConnectedDevices();
611             final CachedBluetoothDeviceManager cachedDeviceManager =
612                     mLocalBluetoothManager.getCachedDeviceManager();
613 
614             final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>();
615             int deviceCount = 0;
616             for (BluetoothDevice device : bluetoothDevices) {
617                 final CachedBluetoothDevice cachedDevice =
618                         cachedDeviceManager.findDevice(device);
619                 if (cachedDevice != null) {
620                     if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
621                             && !cachedDevice.isConnected()
622                             && isMediaDevice(cachedDevice)) {
623                         deviceCount++;
624                         cachedBluetoothDeviceList.add(cachedDevice);
625                         if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) {
626                             break;
627                         }
628                     }
629                 }
630             }
631 
632             unRegisterDeviceAttributeChangeCallback();
633             mDisconnectedMediaDevices.clear();
634             for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) {
635                 final MediaDevice mediaDevice = new BluetoothMediaDevice(mContext,
636                         cachedDevice,
637                         null, null, mPackageName);
638                 if (!mMediaDevices.contains(mediaDevice)) {
639                     cachedDevice.registerCallback(mDeviceAttributeChangeCallback);
640                     mDisconnectedMediaDevices.add(mediaDevice);
641                 }
642             }
643             return new ArrayList<>(mDisconnectedMediaDevices);
644         }
645 
isBondedMediaDevice(CachedBluetoothDevice cachedDevice)646         private boolean isBondedMediaDevice(CachedBluetoothDevice cachedDevice) {
647             return cachedDevice != null
648                     && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
649                     && !cachedDevice.isConnected()
650                     && isMediaDevice(cachedDevice);
651         }
652 
isMediaDevice(CachedBluetoothDevice device)653         private boolean isMediaDevice(CachedBluetoothDevice device) {
654             for (LocalBluetoothProfile profile : device.getConnectableProfiles()) {
655                 if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile ||
656                         profile instanceof LeAudioProfile) {
657                     return true;
658                 }
659             }
660             return false;
661         }
662 
663         @Override
onDeviceRemoved(MediaDevice device)664         public void onDeviceRemoved(MediaDevice device) {
665             boolean isRemoved = false;
666             synchronized (mMediaDevicesLock) {
667                 if (mMediaDevices.contains(device)) {
668                     mMediaDevices.remove(device);
669                     isRemoved = true;
670                 }
671             }
672             if (isRemoved) {
673                 dispatchDeviceListUpdate();
674             }
675         }
676 
677         @Override
onDeviceListRemoved(List<MediaDevice> devices)678         public void onDeviceListRemoved(List<MediaDevice> devices) {
679             synchronized (mMediaDevicesLock) {
680                 mMediaDevices.removeAll(devices);
681             }
682             dispatchDeviceListUpdate();
683         }
684 
685         @Override
onConnectedDeviceChanged(String id)686         public void onConnectedDeviceChanged(String id) {
687             MediaDevice connectDevice = null;
688             synchronized (mMediaDevicesLock) {
689                 connectDevice = getMediaDeviceById(mMediaDevices, id);
690             }
691             connectDevice = connectDevice != null
692                     ? connectDevice : updateCurrentConnectedDevice();
693 
694             mCurrentConnectedDevice = connectDevice;
695             if (connectDevice != null) {
696                 connectDevice.setState(MediaDeviceState.STATE_CONNECTED);
697 
698                 dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice,
699                         MediaDeviceState.STATE_CONNECTED);
700             }
701         }
702 
703         @Override
onDeviceAttributesChanged()704         public void onDeviceAttributesChanged() {
705             dispatchDeviceAttributesChanged();
706         }
707 
708         @Override
onRequestFailed(int reason)709         public void onRequestFailed(int reason) {
710             synchronized (mMediaDevicesLock) {
711                 for (MediaDevice device : mMediaDevices) {
712                     if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
713                         device.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
714                     }
715                 }
716             }
717             dispatchOnRequestFailed(reason);
718         }
719     }
720 
unRegisterDeviceAttributeChangeCallback()721     private void unRegisterDeviceAttributeChangeCallback() {
722         for (MediaDevice device : mDisconnectedMediaDevices) {
723             ((BluetoothMediaDevice) device).getCachedDevice()
724                     .unregisterCallback(mDeviceAttributeChangeCallback);
725         }
726     }
727 
728     /**
729      * Callback for notifying device information updating
730      */
731     public interface DeviceCallback {
732         /**
733          * Callback for notifying device list updated.
734          *
735          * @param devices MediaDevice list
736          */
onDeviceListUpdate(List<MediaDevice> devices)737         default void onDeviceListUpdate(List<MediaDevice> devices) {};
738 
739         /**
740          * Callback for notifying the connected device is changed.
741          *
742          * @param device the changed connected MediaDevice
743          * @param state the current MediaDevice state, the possible values are:
744          * {@link MediaDeviceState#STATE_CONNECTED},
745          * {@link MediaDeviceState#STATE_CONNECTING},
746          * {@link MediaDeviceState#STATE_DISCONNECTED}
747          */
onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)748         default void onSelectedDeviceStateChanged(MediaDevice device,
749                 @MediaDeviceState int state) {};
750 
751         /**
752          * Callback for notifying the device attributes is changed.
753          */
onDeviceAttributesChanged()754         default void onDeviceAttributesChanged() {};
755 
756         /**
757          * Callback for notifying that transferring is failed.
758          *
759          * @param reason the reason that the request has failed. Can be one of followings:
760          * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
761          * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED},
762          * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR},
763          * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
764          * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
765          */
onRequestFailed(int reason)766         default void onRequestFailed(int reason){};
767 
768         /**
769          * Callback for notifying that we have a new about-to-connect device.
770          *
771          * An about-to-connect device is a device that is not yet connected but is expected to
772          * connect imminently and should be displayed as the current device in the media player.
773          * See [AudioManager.muteAwaitConnection] for more details.
774          *
775          * The information in the most recent callback should override information from any previous
776          * callbacks.
777          *
778          * @param deviceAddress the address of the device. {@see AudioDeviceAttributes.address}.
779          *                      If present, we'll use this address to fetch the full information
780          *                      about the device (if we can find that information).
781          * @param deviceName the name of the device (displayed to the user). Used as a backup in
782          *                   case using deviceAddress doesn't work.
783          * @param deviceIcon the icon that should be used with the device. Used as a backup in case
784          *                   using deviceAddress doesn't work.
785          */
onAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon )786         default void onAboutToConnectDeviceAdded(
787                 @NonNull String deviceAddress,
788                 @NonNull String deviceName,
789                 @Nullable Drawable deviceIcon
790         ) {}
791 
792         /**
793          * Callback for notifying that we no longer have an about-to-connect device.
794          */
onAboutToConnectDeviceRemoved()795         default void onAboutToConnectDeviceRemoved() {}
796     }
797 
798     /**
799      * This callback is for update {@link BluetoothMediaDevice} summary when
800      * {@link CachedBluetoothDevice} connection state is changed.
801      */
802     @VisibleForTesting
803     class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
804 
805         @Override
onDeviceAttributesChanged()806         public void onDeviceAttributesChanged() {
807             if (mOnTransferBluetoothDevice != null
808                     && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice()
809                     .isBusy()
810                     && !mOnTransferBluetoothDevice.isConnected()) {
811                 // Failed to connect
812                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
813                 mOnTransferBluetoothDevice = null;
814                 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR);
815             }
816             dispatchDeviceAttributesChanged();
817         }
818     }
819 }
820