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