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