1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settingslib.media;
17 
18 import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
19 
20 import android.app.Notification;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.content.Context;
24 import android.media.RoutingSessionInfo;
25 import android.os.Build;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import androidx.annotation.IntDef;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.RequiresApi;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.settingslib.bluetooth.A2dpProfile;
35 import com.android.settingslib.bluetooth.BluetoothCallback;
36 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
37 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
38 import com.android.settingslib.bluetooth.HearingAidProfile;
39 import com.android.settingslib.bluetooth.LocalBluetoothManager;
40 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
41 
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 import java.util.ArrayList;
45 import java.util.Collection;
46 import java.util.Collections;
47 import java.util.Comparator;
48 import java.util.List;
49 import java.util.concurrent.CopyOnWriteArrayList;
50 
51 /**
52  * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice.
53  */
54 @RequiresApi(Build.VERSION_CODES.R)
55 public class LocalMediaManager implements BluetoothCallback {
56     private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder();
57     private static final String TAG = "LocalMediaManager";
58     private static final int MAX_DISCONNECTED_DEVICE_NUM = 5;
59 
60     @Retention(RetentionPolicy.SOURCE)
61     @IntDef({MediaDeviceState.STATE_CONNECTED,
62             MediaDeviceState.STATE_CONNECTING,
63             MediaDeviceState.STATE_DISCONNECTED,
64             MediaDeviceState.STATE_CONNECTING_FAILED})
65     public @interface MediaDeviceState {
66         int STATE_CONNECTED = 0;
67         int STATE_CONNECTING = 1;
68         int STATE_DISCONNECTED = 2;
69         int STATE_CONNECTING_FAILED = 3;
70     }
71 
72     private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
73     private final Object mMediaDevicesLock = new Object();
74     @VisibleForTesting
75     final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback();
76 
77     private Context mContext;
78     private LocalBluetoothManager mLocalBluetoothManager;
79     private InfoMediaManager mInfoMediaManager;
80     private String mPackageName;
81     private MediaDevice mOnTransferBluetoothDevice;
82 
83     @VisibleForTesting
84     List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
85     @VisibleForTesting
86     List<MediaDevice> mDisconnectedMediaDevices = new CopyOnWriteArrayList<>();
87     @VisibleForTesting
88     MediaDevice mPhoneDevice;
89     @VisibleForTesting
90     MediaDevice mCurrentConnectedDevice;
91     @VisibleForTesting
92     DeviceAttributeChangeCallback mDeviceAttributeChangeCallback =
93             new DeviceAttributeChangeCallback();
94     @VisibleForTesting
95     BluetoothAdapter mBluetoothAdapter;
96 
97     /**
98      * Register to start receiving callbacks for MediaDevice events.
99      */
registerCallback(DeviceCallback callback)100     public void registerCallback(DeviceCallback callback) {
101         mCallbacks.add(callback);
102     }
103 
104     /**
105      * Unregister to stop receiving callbacks for MediaDevice events
106      */
unregisterCallback(DeviceCallback callback)107     public void unregisterCallback(DeviceCallback callback) {
108         mCallbacks.remove(callback);
109     }
110 
111     /**
112      * Creates a LocalMediaManager with references to given managers.
113      *
114      * It will obtain a {@link LocalBluetoothManager} by calling
115      * {@link LocalBluetoothManager#getInstance} and create an {@link InfoMediaManager} passing
116      * that bluetooth manager.
117      *
118      * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter.
119      */
LocalMediaManager(Context context, String packageName, Notification notification)120     public LocalMediaManager(Context context, String packageName, Notification notification) {
121         mContext = context;
122         mPackageName = packageName;
123         mLocalBluetoothManager =
124                 LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null);
125         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
126         if (mLocalBluetoothManager == null) {
127             Log.e(TAG, "Bluetooth is not supported on this device");
128             return;
129         }
130 
131         mInfoMediaManager =
132                 new InfoMediaManager(context, packageName, notification, mLocalBluetoothManager);
133     }
134 
135     /**
136      * Creates a LocalMediaManager with references to given managers.
137      *
138      * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter.
139      */
LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, InfoMediaManager infoMediaManager, String packageName)140     public LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager,
141             InfoMediaManager infoMediaManager, String packageName) {
142         mContext = context;
143         mLocalBluetoothManager = localBluetoothManager;
144         mInfoMediaManager = infoMediaManager;
145         mPackageName = packageName;
146         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
147     }
148 
149     /**
150      * Connect the MediaDevice to transfer media
151      * @param connectDevice the MediaDevice
152      * @return {@code true} if successfully call, otherwise return {@code false}
153      */
connectDevice(MediaDevice connectDevice)154     public boolean connectDevice(MediaDevice connectDevice) {
155         MediaDevice device = null;
156         synchronized (mMediaDevicesLock) {
157             device = getMediaDeviceById(mMediaDevices, connectDevice.getId());
158         }
159         if (device == null) {
160             Log.w(TAG, "connectDevice() connectDevice not in the list!");
161             return false;
162         }
163         if (device instanceof BluetoothMediaDevice) {
164             final CachedBluetoothDevice cachedDevice =
165                     ((BluetoothMediaDevice) device).getCachedDevice();
166             if (!cachedDevice.isConnected() && !cachedDevice.isBusy()) {
167                 mOnTransferBluetoothDevice = connectDevice;
168                 device.setState(MediaDeviceState.STATE_CONNECTING);
169                 cachedDevice.connect();
170                 return true;
171             }
172         }
173 
174         if (device == mCurrentConnectedDevice) {
175             Log.d(TAG, "connectDevice() this device all ready connected! : " + device.getName());
176             return false;
177         }
178 
179         if (mCurrentConnectedDevice != null) {
180             mCurrentConnectedDevice.disconnect();
181         }
182 
183         device.setState(MediaDeviceState.STATE_CONNECTING);
184         if (TextUtils.isEmpty(mPackageName)) {
185             mInfoMediaManager.connectDeviceWithoutPackageName(device);
186         } else {
187             device.connect();
188         }
189         return true;
190     }
191 
dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)192     void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) {
193         for (DeviceCallback callback : getCallbacks()) {
194             callback.onSelectedDeviceStateChanged(device, state);
195         }
196     }
197 
198     /**
199      * Start scan connected MediaDevice
200      */
startScan()201     public void startScan() {
202         synchronized (mMediaDevicesLock) {
203             mMediaDevices.clear();
204         }
205         mInfoMediaManager.registerCallback(mMediaDeviceCallback);
206         mInfoMediaManager.startScan();
207     }
208 
dispatchDeviceListUpdate()209     void dispatchDeviceListUpdate() {
210         final List<MediaDevice> mediaDevices = new ArrayList<>(mMediaDevices);
211         for (DeviceCallback callback : getCallbacks()) {
212             callback.onDeviceListUpdate(mediaDevices);
213         }
214     }
215 
dispatchDeviceAttributesChanged()216     void dispatchDeviceAttributesChanged() {
217         for (DeviceCallback callback : getCallbacks()) {
218             callback.onDeviceAttributesChanged();
219         }
220     }
221 
dispatchOnRequestFailed(int reason)222     void dispatchOnRequestFailed(int reason) {
223         for (DeviceCallback callback : getCallbacks()) {
224             callback.onRequestFailed(reason);
225         }
226     }
227 
228     /**
229      * Stop scan MediaDevice
230      */
stopScan()231     public void stopScan() {
232         mInfoMediaManager.unregisterCallback(mMediaDeviceCallback);
233         mInfoMediaManager.stopScan();
234         unRegisterDeviceAttributeChangeCallback();
235     }
236 
237     /**
238      * Find the MediaDevice through id.
239      *
240      * @param devices the list of MediaDevice
241      * @param id the unique id of MediaDevice
242      * @return MediaDevice
243      */
getMediaDeviceById(List<MediaDevice> devices, String id)244     public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) {
245         for (MediaDevice mediaDevice : devices) {
246             if (TextUtils.equals(mediaDevice.getId(), id)) {
247                 return mediaDevice;
248             }
249         }
250         Log.i(TAG, "getMediaDeviceById() can't found device");
251         return null;
252     }
253 
254     /**
255      * Find the MediaDevice from all media devices by id.
256      *
257      * @param id the unique id of MediaDevice
258      * @return MediaDevice
259      */
getMediaDeviceById(String id)260     public MediaDevice getMediaDeviceById(String id) {
261         synchronized (mMediaDevicesLock) {
262             for (MediaDevice mediaDevice : mMediaDevices) {
263                 if (TextUtils.equals(mediaDevice.getId(), id)) {
264                     return mediaDevice;
265                 }
266             }
267         }
268         Log.i(TAG, "Unable to find device " + id);
269         return null;
270     }
271 
272     /**
273      * Find the current connected MediaDevice.
274      *
275      * @return MediaDevice
276      */
277     @Nullable
getCurrentConnectedDevice()278     public MediaDevice getCurrentConnectedDevice() {
279         return mCurrentConnectedDevice;
280     }
281 
282     /**
283      * Add a MediaDevice to let it play current media.
284      *
285      * @param device MediaDevice
286      * @return If add device successful return {@code true}, otherwise return {@code false}
287      */
addDeviceToPlayMedia(MediaDevice device)288     public boolean addDeviceToPlayMedia(MediaDevice device) {
289         return mInfoMediaManager.addDeviceToPlayMedia(device);
290     }
291 
292     /**
293      * Remove a {@code device} from current media.
294      *
295      * @param device MediaDevice
296      * @return If device stop successful return {@code true}, otherwise return {@code false}
297      */
removeDeviceFromPlayMedia(MediaDevice device)298     public boolean removeDeviceFromPlayMedia(MediaDevice device) {
299         return mInfoMediaManager.removeDeviceFromPlayMedia(device);
300     }
301 
302     /**
303      * Get the MediaDevice list that can be added to current media.
304      *
305      * @return list of MediaDevice
306      */
getSelectableMediaDevice()307     public List<MediaDevice> getSelectableMediaDevice() {
308         return mInfoMediaManager.getSelectableMediaDevice();
309     }
310 
311     /**
312      * Get the MediaDevice list that can be removed from current media session.
313      *
314      * @return list of MediaDevice
315      */
getDeselectableMediaDevice()316     public List<MediaDevice> getDeselectableMediaDevice() {
317         return mInfoMediaManager.getDeselectableMediaDevice();
318     }
319 
320     /**
321      * Release session to stop playing media on MediaDevice.
322      */
releaseSession()323     public boolean releaseSession() {
324         return mInfoMediaManager.releaseSession();
325     }
326 
327     /**
328      * Get the MediaDevice list that has been selected to current media.
329      *
330      * @return list of MediaDevice
331      */
getSelectedMediaDevice()332     public List<MediaDevice> getSelectedMediaDevice() {
333         return mInfoMediaManager.getSelectedMediaDevice();
334     }
335 
336     /**
337      * Adjust the volume of session.
338      *
339      * @param sessionId the value of media session id
340      * @param volume the value of volume
341      */
adjustSessionVolume(String sessionId, int volume)342     public void adjustSessionVolume(String sessionId, int volume) {
343         final List<RoutingSessionInfo> infos = getActiveMediaSession();
344         for (RoutingSessionInfo info : infos) {
345             if (TextUtils.equals(sessionId, info.getId())) {
346                 mInfoMediaManager.adjustSessionVolume(info, volume);
347                 return;
348             }
349         }
350         Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId);
351     }
352 
353     /**
354      * Adjust the volume of session.
355      *
356      * @param volume the value of volume
357      */
adjustSessionVolume(int volume)358     public void adjustSessionVolume(int volume) {
359         mInfoMediaManager.adjustSessionVolume(volume);
360     }
361 
362     /**
363      * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}.
364      *
365      * @return  maximum volume of the session, and return -1 if not found.
366      */
getSessionVolumeMax()367     public int getSessionVolumeMax() {
368         return mInfoMediaManager.getSessionVolumeMax();
369     }
370 
371     /**
372      * Gets the current volume of the {@link android.media.RoutingSessionInfo}.
373      *
374      * @return current volume of the session, and return -1 if not found.
375      */
getSessionVolume()376     public int getSessionVolume() {
377         return mInfoMediaManager.getSessionVolume();
378     }
379 
380     /**
381      * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}.
382      *
383      * @return current name of the session, and return {@code null} if not found.
384      */
getSessionName()385     public CharSequence getSessionName() {
386         return mInfoMediaManager.getSessionName();
387     }
388 
389     /**
390      * Gets the current active session.
391      *
392      * @return current active session list{@link android.media.RoutingSessionInfo}
393      */
getActiveMediaSession()394     public List<RoutingSessionInfo> getActiveMediaSession() {
395         return mInfoMediaManager.getActiveMediaSession();
396     }
397 
398     /**
399      * Gets the current package name.
400      *
401      * @return current package name
402      */
getPackageName()403     public String getPackageName() {
404         return mPackageName;
405     }
406 
407     /**
408      * Returns {@code true} if needed to disable media output, otherwise returns {@code false}.
409      */
shouldDisableMediaOutput(String packageName)410     public boolean shouldDisableMediaOutput(String packageName) {
411         return mInfoMediaManager.shouldDisableMediaOutput(packageName);
412     }
413 
414     /**
415      * Returns {@code true} if needed to enable volume seekbar, otherwise returns {@code false}.
416      */
shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)417     public boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) {
418         return mInfoMediaManager.shouldEnableVolumeSeekBar(sessionInfo);
419     }
420 
421     @VisibleForTesting
updateCurrentConnectedDevice()422     MediaDevice updateCurrentConnectedDevice() {
423         MediaDevice connectedDevice = null;
424         synchronized (mMediaDevicesLock) {
425             for (MediaDevice device : mMediaDevices) {
426                 if (device instanceof BluetoothMediaDevice) {
427                     if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice())
428                             && device.isConnected()) {
429                         return device;
430                     }
431                 } else if (device instanceof PhoneMediaDevice) {
432                     connectedDevice = device;
433                 }
434             }
435         }
436 
437         return connectedDevice;
438     }
439 
isActiveDevice(CachedBluetoothDevice device)440     private boolean isActiveDevice(CachedBluetoothDevice device) {
441         boolean isActiveDeviceA2dp = false;
442         boolean isActiveDeviceHearingAid = false;
443         final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile();
444         if (a2dpProfile != null) {
445             isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice());
446         }
447         if (!isActiveDeviceA2dp) {
448             final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager()
449                     .getHearingAidProfile();
450             if (hearingAidProfile != null) {
451                 isActiveDeviceHearingAid =
452                         hearingAidProfile.getActiveDevices().contains(device.getDevice());
453             }
454         }
455 
456         return isActiveDeviceA2dp || isActiveDeviceHearingAid;
457     }
458 
getCallbacks()459     private Collection<DeviceCallback> getCallbacks() {
460         return new CopyOnWriteArrayList<>(mCallbacks);
461     }
462 
463     class MediaDeviceCallback implements MediaManager.MediaDeviceCallback {
464         @Override
onDeviceAdded(MediaDevice device)465         public void onDeviceAdded(MediaDevice device) {
466             boolean isAdded = false;
467             synchronized (mMediaDevicesLock) {
468                 if (!mMediaDevices.contains(device)) {
469                     mMediaDevices.add(device);
470                     isAdded = true;
471                 }
472             }
473 
474             if (isAdded) {
475                 dispatchDeviceListUpdate();
476             }
477         }
478 
479         @Override
onDeviceListAdded(List<MediaDevice> devices)480         public void onDeviceListAdded(List<MediaDevice> devices) {
481             synchronized (mMediaDevicesLock) {
482                 mMediaDevices.clear();
483                 mMediaDevices.addAll(devices);
484                 Collections.sort(devices, COMPARATOR);
485                 // Add disconnected bluetooth devices only when phone output device is available.
486                 for (MediaDevice device : devices) {
487                     final int type = device.getDeviceType();
488                     if (type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE
489                             || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
490                             || type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE) {
491                         mMediaDevices.addAll(buildDisconnectedBluetoothDevice());
492                         break;
493                     }
494                 }
495             }
496 
497             final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice();
498             mCurrentConnectedDevice = infoMediaDevice != null
499                     ? infoMediaDevice : updateCurrentConnectedDevice();
500             dispatchDeviceListUpdate();
501             if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) {
502                 connectDevice(mOnTransferBluetoothDevice);
503                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED);
504                 dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice,
505                         MediaDeviceState.STATE_CONNECTED);
506                 mOnTransferBluetoothDevice = null;
507             }
508         }
509 
buildDisconnectedBluetoothDevice()510         private List<MediaDevice> buildDisconnectedBluetoothDevice() {
511             if (mBluetoothAdapter == null) {
512                 Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null");
513                 return new ArrayList<>();
514             }
515 
516             final List<BluetoothDevice> bluetoothDevices =
517                     mBluetoothAdapter.getMostRecentlyConnectedDevices();
518             final CachedBluetoothDeviceManager cachedDeviceManager =
519                     mLocalBluetoothManager.getCachedDeviceManager();
520 
521             final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>();
522             int deviceCount = 0;
523             for (BluetoothDevice device : bluetoothDevices) {
524                 final CachedBluetoothDevice cachedDevice =
525                         cachedDeviceManager.findDevice(device);
526                 if (cachedDevice != null) {
527                     if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
528                             && !cachedDevice.isConnected()
529                             && isA2dpOrHearingAidDevice(cachedDevice)) {
530                         deviceCount++;
531                         cachedBluetoothDeviceList.add(cachedDevice);
532                         if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) {
533                             break;
534                         }
535                     }
536                 }
537             }
538 
539             unRegisterDeviceAttributeChangeCallback();
540             mDisconnectedMediaDevices.clear();
541             for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) {
542                 final MediaDevice mediaDevice = new BluetoothMediaDevice(mContext,
543                         cachedDevice,
544                         null, null, mPackageName);
545                 if (!mMediaDevices.contains(mediaDevice)) {
546                     cachedDevice.registerCallback(mDeviceAttributeChangeCallback);
547                     mDisconnectedMediaDevices.add(mediaDevice);
548                 }
549             }
550             return new ArrayList<>(mDisconnectedMediaDevices);
551         }
552 
isA2dpOrHearingAidDevice(CachedBluetoothDevice device)553         private boolean isA2dpOrHearingAidDevice(CachedBluetoothDevice device) {
554             for (LocalBluetoothProfile profile : device.getConnectableProfiles()) {
555                 if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile) {
556                     return true;
557                 }
558             }
559             return false;
560         }
561 
562         @Override
onDeviceRemoved(MediaDevice device)563         public void onDeviceRemoved(MediaDevice device) {
564             boolean isRemoved = false;
565             synchronized (mMediaDevicesLock) {
566                 if (mMediaDevices.contains(device)) {
567                     mMediaDevices.remove(device);
568                     isRemoved = true;
569                 }
570             }
571             if (isRemoved) {
572                 dispatchDeviceListUpdate();
573             }
574         }
575 
576         @Override
onDeviceListRemoved(List<MediaDevice> devices)577         public void onDeviceListRemoved(List<MediaDevice> devices) {
578             synchronized (mMediaDevicesLock) {
579                 mMediaDevices.removeAll(devices);
580             }
581             dispatchDeviceListUpdate();
582         }
583 
584         @Override
onConnectedDeviceChanged(String id)585         public void onConnectedDeviceChanged(String id) {
586             MediaDevice connectDevice = null;
587             synchronized (mMediaDevicesLock) {
588                 connectDevice = getMediaDeviceById(mMediaDevices, id);
589             }
590             connectDevice = connectDevice != null
591                     ? connectDevice : updateCurrentConnectedDevice();
592 
593             mCurrentConnectedDevice = connectDevice;
594             if (connectDevice != null) {
595                 connectDevice.setState(MediaDeviceState.STATE_CONNECTED);
596 
597                 dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice,
598                         MediaDeviceState.STATE_CONNECTED);
599             }
600         }
601 
602         @Override
onDeviceAttributesChanged()603         public void onDeviceAttributesChanged() {
604             dispatchDeviceAttributesChanged();
605         }
606 
607         @Override
onRequestFailed(int reason)608         public void onRequestFailed(int reason) {
609             synchronized (mMediaDevicesLock) {
610                 for (MediaDevice device : mMediaDevices) {
611                     if (device.getState() == MediaDeviceState.STATE_CONNECTING) {
612                         device.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
613                     }
614                 }
615             }
616             dispatchOnRequestFailed(reason);
617         }
618     }
619 
unRegisterDeviceAttributeChangeCallback()620     private void unRegisterDeviceAttributeChangeCallback() {
621         for (MediaDevice device : mDisconnectedMediaDevices) {
622             ((BluetoothMediaDevice) device).getCachedDevice()
623                     .unregisterCallback(mDeviceAttributeChangeCallback);
624         }
625     }
626 
627     /**
628      * Callback for notifying device information updating
629      */
630     public interface DeviceCallback {
631         /**
632          * Callback for notifying device list updated.
633          *
634          * @param devices MediaDevice list
635          */
onDeviceListUpdate(List<MediaDevice> devices)636         default void onDeviceListUpdate(List<MediaDevice> devices) {};
637 
638         /**
639          * Callback for notifying the connected device is changed.
640          *
641          * @param device the changed connected MediaDevice
642          * @param state the current MediaDevice state, the possible values are:
643          * {@link MediaDeviceState#STATE_CONNECTED},
644          * {@link MediaDeviceState#STATE_CONNECTING},
645          * {@link MediaDeviceState#STATE_DISCONNECTED}
646          */
onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state)647         default void onSelectedDeviceStateChanged(MediaDevice device,
648                 @MediaDeviceState int state) {};
649 
650         /**
651          * Callback for notifying the device attributes is changed.
652          */
onDeviceAttributesChanged()653         default void onDeviceAttributesChanged() {};
654 
655         /**
656          * Callback for notifying that transferring is failed.
657          *
658          * @param reason the reason that the request has failed. Can be one of followings:
659          * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR},
660          * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED},
661          * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR},
662          * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE},
663          * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND},
664          */
onRequestFailed(int reason)665         default void onRequestFailed(int reason){};
666     }
667 
668     /**
669      * This callback is for update {@link BluetoothMediaDevice} summary when
670      * {@link CachedBluetoothDevice} connection state is changed.
671      */
672     @VisibleForTesting
673     class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
674 
675         @Override
onDeviceAttributesChanged()676         public void onDeviceAttributesChanged() {
677             if (mOnTransferBluetoothDevice != null
678                     && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice()
679                     .isBusy()
680                     && !mOnTransferBluetoothDevice.isConnected()) {
681                 // Failed to connect
682                 mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED);
683                 mOnTransferBluetoothDevice = null;
684                 dispatchOnRequestFailed(REASON_UNKNOWN_ERROR);
685             }
686             dispatchDeviceAttributesChanged();
687         }
688     }
689 }
690