• 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.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      * Gets the MediaDevice list that can be transferred to with the current media session by the
356      * media route provider.
357      *
358      * @return list of MediaDevice
359      */
360     @NonNull
getTransferableMediaDevices()361     public List<MediaDevice> getTransferableMediaDevices() {
362         return mInfoMediaManager.getTransferableMediaDevices();
363     }
364 
365     /**
366      * Get the MediaDevice list that can be removed from current media session.
367      *
368      * @return list of MediaDevice
369      */
getDeselectableMediaDevice()370     public List<MediaDevice> getDeselectableMediaDevice() {
371         return mInfoMediaManager.getDeselectableMediaDevices();
372     }
373 
374     /**
375      * Release session to stop playing media on MediaDevice.
376      */
releaseSession()377     public boolean releaseSession() {
378         return mInfoMediaManager.releaseSession();
379     }
380 
381     /**
382      * Get the MediaDevice list that has been selected to current media.
383      *
384      * @return list of MediaDevice
385      */
getSelectedMediaDevice()386     public List<MediaDevice> getSelectedMediaDevice() {
387         return mInfoMediaManager.getSelectedMediaDevices();
388     }
389 
390     /**
391      * Requests a volume change for a specific media device.
392      *
393      * This operation is different from {@link #adjustSessionVolume(String, int)}, which changes the
394      * volume of the overall session.
395      */
adjustDeviceVolume(MediaDevice device, int volume)396     public void adjustDeviceVolume(MediaDevice device, int volume) {
397         mInfoMediaManager.adjustDeviceVolume(device, volume);
398     }
399 
400     /**
401      * Adjust the volume of session.
402      *
403      * @param sessionId the value of media session id
404      * @param volume the value of volume
405      */
adjustSessionVolume(String sessionId, int volume)406     public void adjustSessionVolume(String sessionId, int volume) {
407         RoutingSessionInfo session = mInfoMediaManager.getRoutingSessionById(sessionId);
408         if (session != null) {
409             mInfoMediaManager.adjustSessionVolume(session, volume);
410         } else {
411             Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId);
412         }
413     }
414 
415     /**
416      * Adjust the volume of session.
417      *
418      * @param volume the value of volume
419      */
adjustSessionVolume(int volume)420     public void adjustSessionVolume(int volume) {
421         mInfoMediaManager.adjustSessionVolume(volume);
422     }
423 
424     /**
425      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
426      *
427      * @return  maximum volume of the session, and return -1 if not found.
428      */
getSessionVolumeMax()429     public int getSessionVolumeMax() {
430         return mInfoMediaManager.getSessionVolumeMax();
431     }
432 
433     /**
434      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
435      *
436      * @return current volume of the session, and return -1 if not found.
437      */
getSessionVolume()438     public int getSessionVolume() {
439         return mInfoMediaManager.getSessionVolume();
440     }
441 
442     /**
443      * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}.
444      *
445      * @return current name of the session, and return {@code null} if not found.
446      */
447     @Nullable
getSessionName()448     public CharSequence getSessionName() {
449         return mInfoMediaManager.getSessionName();
450     }
451 
452     /**
453      * Gets the list of remote {@link RoutingSessionInfo routing sessions} known to the system.
454      *
455      * <p>This list does not include any system routing sessions.
456      */
getRemoteRoutingSessions()457     public List<RoutingSessionInfo> getRemoteRoutingSessions() {
458         return mInfoMediaManager.getRemoteSessions();
459     }
460 
461     /**
462      * Gets the current package name.
463      *
464      * @return current package name
465      */
getPackageName()466     public String getPackageName() {
467         return mPackageName;
468     }
469 
470     /**
471      * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}.
472      */
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)473     public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
474         return mInfoMediaManager.shouldEnableVolumeSeekBar(sessionInfo);
475     }
476 
477     @VisibleForTesting
updateCurrentConnectedDevice()478     MediaDevice updateCurrentConnectedDevice() {
479         MediaDevice connectedDevice = null;
480         synchronized (mMediaDevicesLock) {
481             for (MediaDevice device : mMediaDevices) {
482                 if (device instanceof BluetoothMediaDevice) {
483                     if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice())
484                             && device.isConnected()) {
485                         return device;
486                     }
487                 } else if (device instanceof PhoneMediaDevice) {
488                     connectedDevice = device;
489                 }
490             }
491         }
492 
493         return connectedDevice;
494     }
495 
isActiveDevice(CachedBluetoothDevice device)496     private boolean isActiveDevice(CachedBluetoothDevice device) {
497         boolean isActiveDeviceA2dp = false;
498         boolean isActiveDeviceHearingAid = false;
499         boolean isActiveLeAudio = false;
500         final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile();
501         if (a2dpProfile != null) {
502             isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice());
503         }
504         if (!isActiveDeviceA2dp) {
505             final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager()
506                     .getHearingAidProfile();
507             if (hearingAidProfile != null) {
508                 isActiveDeviceHearingAid =
509                         hearingAidProfile.getActiveDevices().contains(device.getDevice());
510             }
511         }
512 
513         if (!isActiveDeviceA2dp && !isActiveDeviceHearingAid) {
514             final LeAudioProfile leAudioProfile = mLocalBluetoothManager.getProfileManager()
515                     .getLeAudioProfile();
516             if (leAudioProfile != null) {
517                 isActiveLeAudio = leAudioProfile.getActiveDevices().contains(device.getDevice());
518             }
519         }
520 
521         return isActiveDeviceA2dp || isActiveDeviceHearingAid || isActiveLeAudio;
522     }
523 
getCallbacks()524     private Collection<DeviceCallback> getCallbacks() {
525         return new CopyOnWriteArrayList<>(mCallbacks);
526     }
527 
528     class MediaDeviceCallback implements InfoMediaManager.MediaDeviceCallback {
529         @Override
onDeviceListAdded(@onNull List<MediaDevice> devices)530         public void onDeviceListAdded(@NonNull List<MediaDevice> devices) {
531             synchronized (mMediaDevicesLock) {
532                 mMediaDevices.clear();
533                 mMediaDevices.addAll(devices);
534                 // Add muting expected bluetooth devices only when phone output device is available.
535                 for (MediaDevice device : devices) {
536                     final int type = device.getDeviceType();
537                     if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE
538                             || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
539                             || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) {
540                         if (isTv()) {
541                             mMediaDevices.addAll(buildDisconnectedBluetoothDevice());
542                         } else {
543                             MediaDevice mutingExpectedDevice = getMutingExpectedDevice();
544                             if (mutingExpectedDevice != null) {
545                                 mMediaDevices.add(mutingExpectedDevice);
546                             }
547                         }
548                         break;
549                     }
550                 }
551             }
552 
553             final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice();
554             mCurrentConnectedDevice = infoMediaDevice != null
555                     ? infoMediaDevice : updateCurrentConnectedDevice();
556             dispatchDeviceListUpdate();
557             if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) {
558                 connectDevice(mOnTransferBluetoothDevice);
559                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED);
560                 dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice,
561                         MediaDeviceState.STATE_CONNECTED);
562                 mOnTransferBluetoothDevice = null;
563             }
564         }
565 
isTv()566         private boolean isTv() {
567             PackageManager pm = mContext.getPackageManager();
568             return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION)
569                     || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
570         }
571 
getMutingExpectedDevice()572         private MediaDevice getMutingExpectedDevice() {
573             if (mBluetoothAdapter == null
574                     || mAudioManager.getMutingExpectedDevice() == null) {
575                 return null;
576             }
577             final List<BluetoothDevice> bluetoothDevices =
578                     mBluetoothAdapter.getMostRecentlyConnectedDevices();
579             final CachedBluetoothDeviceManager cachedDeviceManager =
580                     mLocalBluetoothManager.getCachedDeviceManager();
581             for (BluetoothDevice device : bluetoothDevices) {
582                 final CachedBluetoothDevice cachedDevice =
583                         cachedDeviceManager.findDevice(device);
584                 if (isBondedMediaDevice(cachedDevice) && isMutingExpectedDevice(cachedDevice)) {
585                     return new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null);
586                 }
587             }
588             return null;
589         }
590 
isMutingExpectedDevice(CachedBluetoothDevice cachedDevice)591         private boolean isMutingExpectedDevice(CachedBluetoothDevice cachedDevice) {
592             AudioDeviceAttributes mutingExpectedDevice = mAudioManager.getMutingExpectedDevice();
593             if (mutingExpectedDevice == null || cachedDevice == null) {
594                 return false;
595             }
596             return cachedDevice.getAddress().equals(mutingExpectedDevice.getAddress());
597         }
598 
buildDisconnectedBluetoothDevice()599         private List<MediaDevice> buildDisconnectedBluetoothDevice() {
600             if (mBluetoothAdapter == null) {
601                 Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null");
602                 return new ArrayList<>();
603             }
604 
605             final List<BluetoothDevice> bluetoothDevices =
606                     mBluetoothAdapter.getMostRecentlyConnectedDevices();
607             final CachedBluetoothDeviceManager cachedDeviceManager =
608                     mLocalBluetoothManager.getCachedDeviceManager();
609 
610             final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>();
611             int deviceCount = 0;
612             for (BluetoothDevice device : bluetoothDevices) {
613                 final CachedBluetoothDevice cachedDevice =
614                         cachedDeviceManager.findDevice(device);
615                 if (cachedDevice != null) {
616                     if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
617                             && !cachedDevice.isConnected()
618                             && isMediaDevice(cachedDevice)) {
619                         deviceCount++;
620                         cachedBluetoothDeviceList.add(cachedDevice);
621                         if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) {
622                             break;
623                         }
624                     }
625                 }
626             }
627 
628             unRegisterDeviceAttributeChangeCallback();
629             mDisconnectedMediaDevices.clear();
630             for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) {
631                 final MediaDevice mediaDevice =
632                         new BluetoothMediaDevice(mContext, cachedDevice, null, /* item */ null);
633                 if (!mMediaDevices.contains(mediaDevice)) {
634                     cachedDevice.registerCallback(mDeviceAttributeChangeCallback);
635                     mDisconnectedMediaDevices.add(mediaDevice);
636                 }
637             }
638             return new ArrayList<>(mDisconnectedMediaDevices);
639         }
640 
isBondedMediaDevice(CachedBluetoothDevice cachedDevice)641         private boolean isBondedMediaDevice(CachedBluetoothDevice cachedDevice) {
642             return cachedDevice != null
643                     && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
644                     && !cachedDevice.isConnected()
645                     && isMediaDevice(cachedDevice);
646         }
647 
isMediaDevice(CachedBluetoothDevice device)648         private boolean isMediaDevice(CachedBluetoothDevice device) {
649             for (LocalBluetoothProfile profile : device.getUiAccessibleProfiles()) {
650                 if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile ||
651                         profile instanceof LeAudioProfile) {
652                     return true;
653                 }
654             }
655             return false;
656         }
657 
658         @Override
onDeviceListRemoved(@onNull List<MediaDevice> devices)659         public void onDeviceListRemoved(@NonNull List<MediaDevice> devices) {
660             synchronized (mMediaDevicesLock) {
661                 mMediaDevices.removeAll(devices);
662             }
663             dispatchDeviceListUpdate();
664         }
665 
666         @Override
onConnectedDeviceChanged(String id)667         public void onConnectedDeviceChanged(String id) {
668             MediaDevice connectDevice = getMediaDeviceById(id);
669             connectDevice = connectDevice != null
670                     ? connectDevice : updateCurrentConnectedDevice();
671 
672             mCurrentConnectedDevice = connectDevice;
673             if (connectDevice != null) {
674                 connectDevice.setState(MediaDeviceState.STATE_CONNECTED);
675 
676                 dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice,
677                         MediaDeviceState.STATE_CONNECTED);
678             }
679         }
680 
681         @Override
onRequestFailed(int reason)682         public void onRequestFailed(int reason) {
683             synchronized (mMediaDevicesLock) {
684                 for (MediaDevice device : mMediaDevices) {
685                     if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
686                         device.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
687                     }
688                 }
689             }
690             dispatchOnRequestFailed(reason);
691         }
692     }
693 
unRegisterDeviceAttributeChangeCallback()694     private void unRegisterDeviceAttributeChangeCallback() {
695         for (MediaDevice device : mDisconnectedMediaDevices) {
696             ((BluetoothMediaDevice) device).getCachedDevice()
697                     .unregisterCallback(mDeviceAttributeChangeCallback);
698         }
699     }
700 
701     /**
702      * Callback for notifying device information updating
703      */
704     public interface DeviceCallback {
705         /**
706          * Callback for notifying device list updated.
707          *
708          * @param devices MediaDevice list
709          */
onDeviceListUpdate(List<MediaDevice> devices)710         default void onDeviceListUpdate(List<MediaDevice> devices) {};
711 
712         /**
713          * Callback for notifying the connected device is changed.
714          *
715          * @param device the changed connected MediaDevice
716          * @param state the current MediaDevice state, the possible values are:
717          * {@link MediaDeviceState#STATE_CONNECTED},
718          * {@link MediaDeviceState#STATE_CONNECTING},
719          * {@link MediaDeviceState#STATE_DISCONNECTED}
720          */
onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)721         default void onSelectedDeviceStateChanged(MediaDevice device,
722                 @MediaDeviceState int state) {};
723 
724         /**
725          * Callback for notifying the device attributes is changed.
726          */
onDeviceAttributesChanged()727         default void onDeviceAttributesChanged() {};
728 
729         /**
730          * Callback for notifying that transferring is failed.
731          *
732          * @param reason the reason that the request has failed. Can be one of followings:
733          * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
734          * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED},
735          * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR},
736          * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
737          * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
738          */
onRequestFailed(int reason)739         default void onRequestFailed(int reason){};
740 
741         /**
742          * Callback for notifying that we have a new about-to-connect device.
743          *
744          * An about-to-connect device is a device that is not yet connected but is expected to
745          * connect imminently and should be displayed as the current device in the media player.
746          * See [AudioManager.muteAwaitConnection] for more details.
747          *
748          * The information in the most recent callback should override information from any previous
749          * callbacks.
750          *
751          * @param deviceAddress the address of the device. {@see AudioDeviceAttributes.address}.
752          *                      If present, we'll use this address to fetch the full information
753          *                      about the device (if we can find that information).
754          * @param deviceName the name of the device (displayed to the user). Used as a backup in
755          *                   case using deviceAddress doesn't work.
756          * @param deviceIcon the icon that should be used with the device. Used as a backup in case
757          *                   using deviceAddress doesn't work.
758          */
onAboutToConnectDeviceAdded( @onNull String deviceAddress, @NonNull String deviceName, @Nullable Drawable deviceIcon )759         default void onAboutToConnectDeviceAdded(
760                 @NonNull String deviceAddress,
761                 @NonNull String deviceName,
762                 @Nullable Drawable deviceIcon
763         ) {}
764 
765         /**
766          * Callback for notifying that we no longer have an about-to-connect device.
767          */
onAboutToConnectDeviceRemoved()768         default void onAboutToConnectDeviceRemoved() {}
769     }
770 
771     /**
772      * This callback is for update {@link BluetoothMediaDevice} summary when
773      * {@link CachedBluetoothDevice} connection state is changed.
774      */
775     @VisibleForTesting
776     class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
777 
778         @Override
onDeviceAttributesChanged()779         public void onDeviceAttributesChanged() {
780             if (mOnTransferBluetoothDevice != null
781                     && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice()
782                     .isBusy()
783                     && !mOnTransferBluetoothDevice.isConnected()) {
784                 // Failed to connect
785                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
786                 mOnTransferBluetoothDevice = null;
787                 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR);
788             }
789             dispatchDeviceAttributesChanged();
790         }
791     }
792 }
793