1 /*
2  * Copyright 2019 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.server.audio;
17 
18 import android.annotation.NonNull;
19 import android.app.ActivityManager;
20 import android.bluetooth.BluetoothA2dp;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothHearingAid;
24 import android.bluetooth.BluetoothProfile;
25 import android.content.Intent;
26 import android.media.AudioDevicePort;
27 import android.media.AudioFormat;
28 import android.media.AudioManager;
29 import android.media.AudioPort;
30 import android.media.AudioRoutesInfo;
31 import android.media.AudioSystem;
32 import android.media.IAudioRoutesObserver;
33 import android.os.Binder;
34 import android.os.RemoteCallbackList;
35 import android.os.RemoteException;
36 import android.os.UserHandle;
37 import android.text.TextUtils;
38 import android.util.ArraySet;
39 import android.util.Log;
40 import android.util.Slog;
41 
42 import com.android.internal.annotations.GuardedBy;
43 import com.android.internal.annotations.VisibleForTesting;
44 
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.LinkedHashMap;
48 import java.util.Set;
49 
50 /**
51  * Class to manage the inventory of all connected devices.
52  * This class is thread-safe.
53  * (non final for mocking/spying)
54  */
55 public class AudioDeviceInventory {
56 
57     private static final String TAG = "AS.AudioDeviceInventory";
58 
59     // Actual list of connected devices
60     // Key for map created from DeviceInfo.makeDeviceListKey()
61     private final LinkedHashMap<String, DeviceInfo> mConnectedDevices = new LinkedHashMap<>();
62 
63     private @NonNull AudioDeviceBroker mDeviceBroker;
64 
65     // Monitoring of audio routes.  Protected by mAudioRoutes.
66     final AudioRoutesInfo mCurAudioRoutes = new AudioRoutesInfo();
67     final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
68             new RemoteCallbackList<IAudioRoutesObserver>();
69 
AudioDeviceInventory(@onNull AudioDeviceBroker broker)70     /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) {
71         mDeviceBroker = broker;
72     }
73 
74     //-----------------------------------------------------------
75     /** for mocking only */
AudioDeviceInventory()76     /*package*/ AudioDeviceInventory() {
77         mDeviceBroker = null;
78     }
79 
setDeviceBroker(@onNull AudioDeviceBroker broker)80     /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) {
81         mDeviceBroker = broker;
82     }
83 
84     //------------------------------------------------------------
85     /**
86      * Class to store info about connected devices.
87      * Use makeDeviceListKey() to make a unique key for this list.
88      */
89     private static class DeviceInfo {
90         final int mDeviceType;
91         final String mDeviceName;
92         final String mDeviceAddress;
93         int mDeviceCodecFormat;
94 
DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat)95         DeviceInfo(int deviceType, String deviceName, String deviceAddress, int deviceCodecFormat) {
96             mDeviceType = deviceType;
97             mDeviceName = deviceName;
98             mDeviceAddress = deviceAddress;
99             mDeviceCodecFormat = deviceCodecFormat;
100         }
101 
102         @Override
toString()103         public String toString() {
104             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
105                     + " name:" + mDeviceName
106                     + " addr:" + mDeviceAddress
107                     + " codec: " + Integer.toHexString(mDeviceCodecFormat) + "]";
108         }
109 
110         /**
111          * Generate a unique key for the mConnectedDevices List by composing the device "type"
112          * and the "address" associated with a specific instance of that device type
113          */
makeDeviceListKey(int device, String deviceAddress)114         private static String makeDeviceListKey(int device, String deviceAddress) {
115             return "0x" + Integer.toHexString(device) + ":" + deviceAddress;
116         }
117     }
118 
119     /**
120      * A class just for packaging up a set of connection parameters.
121      */
122     /*package*/ class WiredDeviceConnectionState {
123         public final int mType;
124         public final @AudioService.ConnectionState int mState;
125         public final String mAddress;
126         public final String mName;
127         public final String mCaller;
128 
WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, String address, String name, String caller)129         /*package*/ WiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
130                                                String address, String name, String caller) {
131             mType = type;
132             mState = state;
133             mAddress = address;
134             mName = name;
135             mCaller = caller;
136         }
137     }
138 
139     //------------------------------------------------------------
140     // Message handling from AudioDeviceBroker
141 
142     /**
143      * Restore previously connected devices. Use in case of audio server crash
144      * (see AudioService.onAudioServerDied() method)
145      */
onRestoreDevices()146     /*package*/ void onRestoreDevices() {
147         synchronized (mConnectedDevices) {
148             for (DeviceInfo di : mConnectedDevices.values()) {
149                 AudioSystem.setDeviceConnectionState(
150                         di.mDeviceType,
151                         AudioSystem.DEVICE_STATE_AVAILABLE,
152                         di.mDeviceAddress,
153                         di.mDeviceName,
154                         di.mDeviceCodecFormat);
155             }
156         }
157     }
158 
159     // only public for mocking/spying
160     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
161     @VisibleForTesting
onSetA2dpSinkConnectionState(@onNull BtHelper.BluetoothA2dpDeviceInfo btInfo, @AudioService.BtProfileConnectionState int state)162     public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
163             @AudioService.BtProfileConnectionState int state) {
164         final BluetoothDevice btDevice = btInfo.getBtDevice();
165         int a2dpVolume = btInfo.getVolume();
166         if (AudioService.DEBUG_DEVICES) {
167             Log.d(TAG, "onSetA2dpSinkConnectionState btDevice=" + btDevice + " state="
168                     + state + " vol=" + a2dpVolume);
169         }
170         String address = btDevice.getAddress();
171         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
172             address = "";
173         }
174 
175         final int a2dpCodec = btInfo.getCodec();
176 
177         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
178                 "A2DP sink connected: device addr=" + address + " state=" + state
179                         + " codec=" + a2dpCodec
180                         + " vol=" + a2dpVolume));
181 
182         synchronized (mConnectedDevices) {
183             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
184                     btDevice.getAddress());
185             final DeviceInfo di = mConnectedDevices.get(key);
186             boolean isConnected = di != null;
187 
188             if (isConnected) {
189                 if (state == BluetoothProfile.STATE_CONNECTED) {
190                     // device is already connected, but we are receiving a connection again,
191                     // it could be for a codec change
192                     if (a2dpCodec != di.mDeviceCodecFormat) {
193                         mDeviceBroker.postBluetoothA2dpDeviceConfigChange(btDevice);
194                     }
195                 } else {
196                     makeA2dpDeviceUnavailableNow(address, di.mDeviceCodecFormat);
197                 }
198             } else if (state == BluetoothProfile.STATE_CONNECTED) {
199                 // device is not already connected
200                 if (a2dpVolume != -1) {
201                     mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
202                             // convert index to internal representation in VolumeStreamState
203                             a2dpVolume * 10,
204                             AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "onSetA2dpSinkConnectionState");
205                 }
206                 makeA2dpDeviceAvailable(address, BtHelper.getName(btDevice),
207                         "onSetA2dpSinkConnectionState", a2dpCodec);
208             }
209         }
210     }
211 
onSetA2dpSourceConnectionState( @onNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int state)212     /*package*/ void onSetA2dpSourceConnectionState(
213             @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int state) {
214         final BluetoothDevice btDevice = btInfo.getBtDevice();
215         if (AudioService.DEBUG_DEVICES) {
216             Log.d(TAG, "onSetA2dpSourceConnectionState btDevice=" + btDevice + " state="
217                     + state);
218         }
219         String address = btDevice.getAddress();
220         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
221             address = "";
222         }
223 
224         synchronized (mConnectedDevices) {
225             final String key = DeviceInfo.makeDeviceListKey(
226                     AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address);
227             final DeviceInfo di = mConnectedDevices.get(key);
228             boolean isConnected = di != null;
229 
230             if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
231                 makeA2dpSrcUnavailable(address);
232             } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
233                 makeA2dpSrcAvailable(address);
234             }
235         }
236     }
237 
onSetHearingAidConnectionState(BluetoothDevice btDevice, @AudioService.BtProfileConnectionState int state, int streamType)238     /*package*/ void onSetHearingAidConnectionState(BluetoothDevice btDevice,
239                 @AudioService.BtProfileConnectionState int state, int streamType) {
240         String address = btDevice.getAddress();
241         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
242             address = "";
243         }
244         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
245                 "onSetHearingAidConnectionState addr=" + address));
246 
247         synchronized (mConnectedDevices) {
248             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID,
249                     btDevice.getAddress());
250             final DeviceInfo di = mConnectedDevices.get(key);
251             boolean isConnected = di != null;
252 
253             if (isConnected && state != BluetoothProfile.STATE_CONNECTED) {
254                 makeHearingAidDeviceUnavailable(address);
255             } else if (!isConnected && state == BluetoothProfile.STATE_CONNECTED) {
256                 makeHearingAidDeviceAvailable(address, BtHelper.getName(btDevice), streamType,
257                         "onSetHearingAidConnectionState");
258             }
259         }
260     }
261 
262     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onBluetoothA2dpActiveDeviceChange( @onNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event)263     /*package*/ void onBluetoothA2dpActiveDeviceChange(
264             @NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
265         final BluetoothDevice btDevice = btInfo.getBtDevice();
266         if (btDevice == null) {
267             return;
268         }
269         if (AudioService.DEBUG_DEVICES) {
270             Log.d(TAG, "onBluetoothA2dpActiveDeviceChange btDevice=" + btDevice);
271         }
272         int a2dpVolume = btInfo.getVolume();
273         final int a2dpCodec = btInfo.getCodec();
274 
275         String address = btDevice.getAddress();
276         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
277             address = "";
278         }
279         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
280                 "onBluetoothA2dpActiveDeviceChange addr=" + address
281                     + " event=" + BtHelper.a2dpDeviceEventToString(event)));
282 
283         synchronized (mConnectedDevices) {
284             if (mDeviceBroker.hasScheduledA2dpSinkConnectionState(btDevice)) {
285                 AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
286                         "A2dp config change ignored (scheduled connection change)"));
287                 return;
288             }
289             final String key = DeviceInfo.makeDeviceListKey(
290                     AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
291             final DeviceInfo di = mConnectedDevices.get(key);
292             if (di == null) {
293                 Log.e(TAG, "invalid null DeviceInfo in onBluetoothA2dpActiveDeviceChange");
294                 return;
295             }
296 
297             if (event == BtHelper.EVENT_ACTIVE_DEVICE_CHANGE) {
298                 // Device is connected
299                 if (a2dpVolume != -1) {
300                     mDeviceBroker.postSetVolumeIndexOnDevice(AudioSystem.STREAM_MUSIC,
301                             // convert index to internal representation in VolumeStreamState
302                             a2dpVolume * 10,
303                             AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
304                             "onBluetoothA2dpActiveDeviceChange");
305                 }
306             } else if (event == BtHelper.EVENT_DEVICE_CONFIG_CHANGE) {
307                 if (di.mDeviceCodecFormat != a2dpCodec) {
308                     di.mDeviceCodecFormat = a2dpCodec;
309                     mConnectedDevices.replace(key, di);
310                 }
311             }
312             if (AudioSystem.handleDeviceConfigChange(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address,
313                     BtHelper.getName(btDevice), a2dpCodec) != AudioSystem.AUDIO_STATUS_OK) {
314                 int musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC);
315                 // force A2DP device disconnection in case of error so that AudioService state is
316                 // consistent with audio policy manager state
317                 setBluetoothA2dpDeviceConnectionState(
318                         btDevice, BluetoothA2dp.STATE_DISCONNECTED, BluetoothProfile.A2DP,
319                         false /* suppressNoisyIntent */, musicDevice,
320                         -1 /* a2dpVolume */);
321             }
322         }
323     }
324 
onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec)325     /*package*/ void onMakeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
326         synchronized (mConnectedDevices) {
327             makeA2dpDeviceUnavailableNow(address, a2dpCodec);
328         }
329     }
330 
onReportNewRoutes()331     /*package*/ void onReportNewRoutes() {
332         int n = mRoutesObservers.beginBroadcast();
333         if (n > 0) {
334             AudioRoutesInfo routes;
335             synchronized (mCurAudioRoutes) {
336                 routes = new AudioRoutesInfo(mCurAudioRoutes);
337             }
338             while (n > 0) {
339                 n--;
340                 IAudioRoutesObserver obs = mRoutesObservers.getBroadcastItem(n);
341                 try {
342                     obs.dispatchAudioRoutesChanged(routes);
343                 } catch (RemoteException e) { }
344             }
345         }
346         mRoutesObservers.finishBroadcast();
347         mDeviceBroker.postObserveDevicesForAllStreams();
348     }
349 
350     private static final Set<Integer> DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET;
351     static {
352         DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET = new HashSet<>();
353         DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET);
354         DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE);
355         DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.add(AudioSystem.DEVICE_OUT_LINE);
356         DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
357     }
358 
onSetWiredDeviceConnectionState( AudioDeviceInventory.WiredDeviceConnectionState wdcs)359     /*package*/ void onSetWiredDeviceConnectionState(
360                             AudioDeviceInventory.WiredDeviceConnectionState wdcs) {
361         AudioService.sDeviceLogger.log(new AudioServiceEvents.WiredDevConnectEvent(wdcs));
362 
363         synchronized (mConnectedDevices) {
364             if ((wdcs.mState == AudioService.CONNECTION_STATE_DISCONNECTED)
365                     && DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) {
366                 mDeviceBroker.setBluetoothA2dpOnInt(true,
367                         "onSetWiredDeviceConnectionState state DISCONNECTED");
368             }
369 
370             if (!handleDeviceConnection(wdcs.mState == AudioService.CONNECTION_STATE_CONNECTED,
371                     wdcs.mType, wdcs.mAddress, wdcs.mName)) {
372                 // change of connection state failed, bailout
373                 return;
374             }
375             if (wdcs.mState != AudioService.CONNECTION_STATE_DISCONNECTED) {
376                 if (DEVICE_OVERRIDE_A2DP_ROUTE_ON_PLUG_SET.contains(wdcs.mType)) {
377                     mDeviceBroker.setBluetoothA2dpOnInt(false,
378                             "onSetWiredDeviceConnectionState state not DISCONNECTED");
379                 }
380                 mDeviceBroker.checkMusicActive(wdcs.mType, wdcs.mCaller);
381             }
382             if (wdcs.mType == AudioSystem.DEVICE_OUT_HDMI) {
383                 mDeviceBroker.checkVolumeCecOnHdmiConnection(wdcs.mState, wdcs.mCaller);
384             }
385             sendDeviceConnectionIntent(wdcs.mType, wdcs.mState, wdcs.mAddress, wdcs.mName);
386             updateAudioRoutes(wdcs.mType, wdcs.mState);
387         }
388     }
389 
onToggleHdmi()390     /*package*/ void onToggleHdmi() {
391         synchronized (mConnectedDevices) {
392             // Is HDMI connected?
393             final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HDMI, "");
394             final DeviceInfo di = mConnectedDevices.get(key);
395             if (di == null) {
396                 Log.e(TAG, "invalid null DeviceInfo in onToggleHdmi");
397                 return;
398             }
399             // Toggle HDMI to retrigger broadcast with proper formats.
400             setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI,
401                     AudioSystem.DEVICE_STATE_UNAVAILABLE, "", "",
402                     "android"); // disconnect
403             setWiredDeviceConnectionState(AudioSystem.DEVICE_OUT_HDMI,
404                     AudioSystem.DEVICE_STATE_AVAILABLE, "", "",
405                     "android"); // reconnect
406         }
407     }
408     //------------------------------------------------------------
409     //
410 
411     /**
412      * Implements the communication with AudioSystem to (dis)connect a device in the native layers
413      * @param connect true if connection
414      * @param device the device type
415      * @param address the address of the device
416      * @param deviceName human-readable name of device
417      * @return false if an error was reported by AudioSystem
418      */
handleDeviceConnection(boolean connect, int device, String address, String deviceName)419     /*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
420             String deviceName) {
421         if (AudioService.DEBUG_DEVICES) {
422             Slog.i(TAG, "handleDeviceConnection(" + connect + " dev:"
423                     + Integer.toHexString(device) + " address:" + address
424                     + " name:" + deviceName + ")");
425         }
426         synchronized (mConnectedDevices) {
427             final String deviceKey = DeviceInfo.makeDeviceListKey(device, address);
428             if (AudioService.DEBUG_DEVICES) {
429                 Slog.i(TAG, "deviceKey:" + deviceKey);
430             }
431             DeviceInfo di = mConnectedDevices.get(deviceKey);
432             boolean isConnected = di != null;
433             if (AudioService.DEBUG_DEVICES) {
434                 Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected);
435             }
436             if (connect && !isConnected) {
437                 final int res = AudioSystem.setDeviceConnectionState(device,
438                         AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName,
439                         AudioSystem.AUDIO_FORMAT_DEFAULT);
440                 if (res != AudioSystem.AUDIO_STATUS_OK) {
441                     Slog.e(TAG, "not connecting device 0x" + Integer.toHexString(device)
442                             + " due to command error " + res);
443                     return false;
444                 }
445                 mConnectedDevices.put(deviceKey, new DeviceInfo(
446                         device, deviceName, address, AudioSystem.AUDIO_FORMAT_DEFAULT));
447                 mDeviceBroker.postAccessoryPlugMediaUnmute(device);
448                 return true;
449             } else if (!connect && isConnected) {
450                 AudioSystem.setDeviceConnectionState(device,
451                         AudioSystem.DEVICE_STATE_UNAVAILABLE, address, deviceName,
452                         AudioSystem.AUDIO_FORMAT_DEFAULT);
453                 // always remove even if disconnection failed
454                 mConnectedDevices.remove(deviceKey);
455                 return true;
456             }
457             Log.w(TAG, "handleDeviceConnection() failed, deviceKey=" + deviceKey
458                     + ", deviceSpec=" + di + ", connect=" + connect);
459         }
460         return false;
461     }
462 
463 
disconnectA2dp()464     /*package*/ void disconnectA2dp() {
465         synchronized (mConnectedDevices) {
466             final ArraySet<String> toRemove = new ArraySet<>();
467             // Disconnect ALL DEVICE_OUT_BLUETOOTH_A2DP devices
468             mConnectedDevices.values().forEach(deviceInfo -> {
469                 if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP) {
470                     toRemove.add(deviceInfo.mDeviceAddress);
471                 }
472             });
473             if (toRemove.size() > 0) {
474                 final int delay = checkSendBecomingNoisyIntentInt(
475                         AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
476                         AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE);
477                 toRemove.stream().forEach(deviceAddress ->
478                         makeA2dpDeviceUnavailableLater(deviceAddress, delay)
479                 );
480             }
481         }
482     }
483 
disconnectA2dpSink()484     /*package*/ void disconnectA2dpSink() {
485         synchronized (mConnectedDevices) {
486             final ArraySet<String> toRemove = new ArraySet<>();
487             // Disconnect ALL DEVICE_IN_BLUETOOTH_A2DP devices
488             mConnectedDevices.values().forEach(deviceInfo -> {
489                 if (deviceInfo.mDeviceType == AudioSystem.DEVICE_IN_BLUETOOTH_A2DP) {
490                     toRemove.add(deviceInfo.mDeviceAddress);
491                 }
492             });
493             toRemove.stream().forEach(deviceAddress -> makeA2dpSrcUnavailable(deviceAddress));
494         }
495     }
496 
disconnectHearingAid()497     /*package*/ void disconnectHearingAid() {
498         synchronized (mConnectedDevices) {
499             final ArraySet<String> toRemove = new ArraySet<>();
500             // Disconnect ALL DEVICE_OUT_HEARING_AID devices
501             mConnectedDevices.values().forEach(deviceInfo -> {
502                 if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
503                     toRemove.add(deviceInfo.mDeviceAddress);
504                 }
505             });
506             if (toRemove.size() > 0) {
507                 final int delay = checkSendBecomingNoisyIntentInt(
508                         AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
509                 toRemove.stream().forEach(deviceAddress ->
510                         // TODO delay not used?
511                         makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/)
512                 );
513             }
514         }
515     }
516 
517     // must be called before removing the device from mConnectedDevices
518     // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying
519     // from AudioSystem
checkSendBecomingNoisyIntent(int device, @AudioService.ConnectionState int state, int musicDevice)520     /*package*/ int checkSendBecomingNoisyIntent(int device,
521             @AudioService.ConnectionState int state, int musicDevice) {
522         synchronized (mConnectedDevices) {
523             return checkSendBecomingNoisyIntentInt(device, state, musicDevice);
524         }
525     }
526 
startWatchingRoutes(IAudioRoutesObserver observer)527     /*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
528         synchronized (mCurAudioRoutes) {
529             AudioRoutesInfo routes = new AudioRoutesInfo(mCurAudioRoutes);
530             mRoutesObservers.register(observer);
531             return routes;
532         }
533     }
534 
getCurAudioRoutes()535     /*package*/ AudioRoutesInfo getCurAudioRoutes() {
536         return mCurAudioRoutes;
537     }
538 
539     // only public for mocking/spying
540     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
541     @VisibleForTesting
setBluetoothA2dpDeviceConnectionState( @onNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume)542     public void setBluetoothA2dpDeviceConnectionState(
543             @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
544             int profile, boolean suppressNoisyIntent, int musicDevice, int a2dpVolume) {
545         int delay;
546         if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
547             throw new IllegalArgumentException("invalid profile " + profile);
548         }
549         synchronized (mConnectedDevices) {
550             if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
551                 @AudioService.ConnectionState int asState =
552                         (state == BluetoothA2dp.STATE_CONNECTED)
553                                 ? AudioService.CONNECTION_STATE_CONNECTED
554                                 : AudioService.CONNECTION_STATE_DISCONNECTED;
555                 delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
556                         asState, musicDevice);
557             } else {
558                 delay = 0;
559             }
560 
561             final int a2dpCodec = mDeviceBroker.getA2dpCodec(device);
562 
563             if (AudioService.DEBUG_DEVICES) {
564                 Log.i(TAG, "setBluetoothA2dpDeviceConnectionState device: " + device
565                         + " state: " + state + " delay(ms): " + delay + "codec:" + a2dpCodec
566                         + " suppressNoisyIntent: " + suppressNoisyIntent);
567             }
568 
569             final BtHelper.BluetoothA2dpDeviceInfo a2dpDeviceInfo =
570                     new BtHelper.BluetoothA2dpDeviceInfo(device, a2dpVolume, a2dpCodec);
571             if (profile == BluetoothProfile.A2DP) {
572                 if (delay == 0) {
573                     onSetA2dpSinkConnectionState(a2dpDeviceInfo, state);
574                 } else {
575                     mDeviceBroker.postA2dpSinkConnection(state,
576                             a2dpDeviceInfo,
577                             delay);
578                 }
579             } else { //profile == BluetoothProfile.A2DP_SINK
580                 mDeviceBroker.postA2dpSourceConnection(state,
581                         a2dpDeviceInfo,
582                         delay);
583             }
584         }
585     }
586 
setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state, String address, String name, String caller)587     /*package*/ int setWiredDeviceConnectionState(int type, @AudioService.ConnectionState int state,
588                                                   String address, String name, String caller) {
589         synchronized (mConnectedDevices) {
590             int delay = checkSendBecomingNoisyIntentInt(type, state, AudioSystem.DEVICE_NONE);
591             mDeviceBroker.postSetWiredDeviceConnectionState(
592                     new WiredDeviceConnectionState(type, state, address, name, caller),
593                     delay);
594             return delay;
595         }
596     }
597 
setBluetoothHearingAidDeviceConnectionState( @onNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state, boolean suppressNoisyIntent, int musicDevice)598     /*package*/ int  setBluetoothHearingAidDeviceConnectionState(
599             @NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
600             boolean suppressNoisyIntent, int musicDevice) {
601         int delay;
602         synchronized (mConnectedDevices) {
603             if (!suppressNoisyIntent) {
604                 int intState = (state == BluetoothHearingAid.STATE_CONNECTED) ? 1 : 0;
605                 delay = checkSendBecomingNoisyIntentInt(AudioSystem.DEVICE_OUT_HEARING_AID,
606                         intState, musicDevice);
607             } else {
608                 delay = 0;
609             }
610             mDeviceBroker.postSetHearingAidConnectionState(state, device, delay);
611             return delay;
612         }
613     }
614 
615 
616     //-------------------------------------------------------------------
617     // Internal utilities
618 
619     @GuardedBy("mConnectedDevices")
makeA2dpDeviceAvailable(String address, String name, String eventSource, int a2dpCodec)620     private void makeA2dpDeviceAvailable(String address, String name, String eventSource,
621             int a2dpCodec) {
622         // enable A2DP before notifying A2DP connection to avoid unnecessary processing in
623         // audio policy manager
624         mDeviceBroker.setBluetoothA2dpOnInt(true, eventSource);
625         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
626                 AudioSystem.DEVICE_STATE_AVAILABLE, address, name, a2dpCodec);
627         // Reset A2DP suspend state each time a new sink is connected
628         AudioSystem.setParameters("A2dpSuspended=false");
629         mConnectedDevices.put(
630                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address),
631                 new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
632                         address, a2dpCodec));
633         mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
634         setCurrentAudioRouteNameIfPossible(name);
635     }
636 
637     @GuardedBy("mConnectedDevices")
makeA2dpDeviceUnavailableNow(String address, int a2dpCodec)638     private void makeA2dpDeviceUnavailableNow(String address, int a2dpCodec) {
639         if (address == null) {
640             return;
641         }
642         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(false);
643         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
644                 AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", a2dpCodec);
645         mConnectedDevices.remove(
646                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address));
647         // Remove A2DP routes as well
648         setCurrentAudioRouteNameIfPossible(null);
649     }
650 
651     @GuardedBy("mConnectedDevices")
makeA2dpDeviceUnavailableLater(String address, int delayMs)652     private void makeA2dpDeviceUnavailableLater(String address, int delayMs) {
653         // prevent any activity on the A2DP audio output to avoid unwanted
654         // reconnection of the sink.
655         AudioSystem.setParameters("A2dpSuspended=true");
656         // retrieve DeviceInfo before removing device
657         final String deviceKey =
658                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address);
659         final DeviceInfo deviceInfo = mConnectedDevices.get(deviceKey);
660         final int a2dpCodec = deviceInfo != null ? deviceInfo.mDeviceCodecFormat :
661                 AudioSystem.AUDIO_FORMAT_DEFAULT;
662         // the device will be made unavailable later, so consider it disconnected right away
663         mConnectedDevices.remove(deviceKey);
664         // send the delayed message to make the device unavailable later
665         mDeviceBroker.setA2dpDockTimeout(address, a2dpCodec, delayMs);
666     }
667 
668 
669     @GuardedBy("mConnectedDevices")
makeA2dpSrcAvailable(String address)670     private void makeA2dpSrcAvailable(String address) {
671         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP,
672                 AudioSystem.DEVICE_STATE_AVAILABLE, address, "",
673                 AudioSystem.AUDIO_FORMAT_DEFAULT);
674         mConnectedDevices.put(
675                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address),
676                 new DeviceInfo(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, "",
677                         address, AudioSystem.AUDIO_FORMAT_DEFAULT));
678     }
679 
680     @GuardedBy("mConnectedDevices")
makeA2dpSrcUnavailable(String address)681     private void makeA2dpSrcUnavailable(String address) {
682         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP,
683                 AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "",
684                 AudioSystem.AUDIO_FORMAT_DEFAULT);
685         mConnectedDevices.remove(
686                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
687     }
688 
689     @GuardedBy("mConnectedDevices")
makeHearingAidDeviceAvailable( String address, String name, int streamType, String eventSource)690     private void makeHearingAidDeviceAvailable(
691             String address, String name, int streamType, String eventSource) {
692         final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
693                 AudioSystem.DEVICE_OUT_HEARING_AID);
694         mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
695 
696         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID,
697                 AudioSystem.DEVICE_STATE_AVAILABLE, address, name,
698                 AudioSystem.AUDIO_FORMAT_DEFAULT);
699         mConnectedDevices.put(
700                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
701                 new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name,
702                         address, AudioSystem.AUDIO_FORMAT_DEFAULT));
703         mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
704         mDeviceBroker.postApplyVolumeOnDevice(streamType,
705                 AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
706         setCurrentAudioRouteNameIfPossible(name);
707     }
708 
709     @GuardedBy("mConnectedDevices")
makeHearingAidDeviceUnavailable(String address)710     private void makeHearingAidDeviceUnavailable(String address) {
711         AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID,
712                 AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "",
713                 AudioSystem.AUDIO_FORMAT_DEFAULT);
714         mConnectedDevices.remove(
715                 DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
716         // Remove Hearing Aid routes as well
717         setCurrentAudioRouteNameIfPossible(null);
718     }
719 
720     @GuardedBy("mConnectedDevices")
setCurrentAudioRouteNameIfPossible(String name)721     private void setCurrentAudioRouteNameIfPossible(String name) {
722         synchronized (mCurAudioRoutes) {
723             if (TextUtils.equals(mCurAudioRoutes.bluetoothName, name)) {
724                 return;
725             }
726             if (name != null || !isCurrentDeviceConnected()) {
727                 mCurAudioRoutes.bluetoothName = name;
728                 mDeviceBroker.postReportNewRoutes();
729             }
730         }
731     }
732 
733     @GuardedBy("mConnectedDevices")
isCurrentDeviceConnected()734     private boolean isCurrentDeviceConnected() {
735         return mConnectedDevices.values().stream().anyMatch(deviceInfo ->
736             TextUtils.equals(deviceInfo.mDeviceName, mCurAudioRoutes.bluetoothName));
737     }
738 
739     // Devices which removal triggers intent ACTION_AUDIO_BECOMING_NOISY. The intent is only
740     // sent if:
741     // - none of these devices are connected anymore after one is disconnected AND
742     // - the device being disconnected is actually used for music.
743     // Access synchronized on mConnectedDevices
744     private static final Set<Integer> BECOMING_NOISY_INTENT_DEVICES_SET;
745     static {
746         BECOMING_NOISY_INTENT_DEVICES_SET = new HashSet<>();
747         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADSET);
748         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_WIRED_HEADPHONE);
749         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI);
750         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET);
751         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET);
752         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE);
753         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID);
754         BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
755         BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
756     }
757 
758     // must be called before removing the device from mConnectedDevices
759     // musicDevice argument is used when not AudioSystem.DEVICE_NONE instead of querying
760     // from AudioSystem
761     @GuardedBy("mConnectedDevices")
checkSendBecomingNoisyIntentInt(int device, @AudioService.ConnectionState int state, int musicDevice)762     private int checkSendBecomingNoisyIntentInt(int device,
763             @AudioService.ConnectionState int state, int musicDevice) {
764         if (state != AudioService.CONNECTION_STATE_DISCONNECTED) {
765             return 0;
766         }
767         if (!BECOMING_NOISY_INTENT_DEVICES_SET.contains(device)) {
768             return 0;
769         }
770         int delay = 0;
771         Set<Integer> devices = new HashSet<>();
772         for (DeviceInfo di : mConnectedDevices.values()) {
773             if (((di.mDeviceType & AudioSystem.DEVICE_BIT_IN) == 0)
774                     && BECOMING_NOISY_INTENT_DEVICES_SET.contains(di.mDeviceType)) {
775                 devices.add(di.mDeviceType);
776             }
777         }
778         if (musicDevice == AudioSystem.DEVICE_NONE) {
779             musicDevice = mDeviceBroker.getDeviceForStream(AudioSystem.STREAM_MUSIC);
780         }
781 
782         // always ignore condition on device being actually used for music when in communication
783         // because music routing is altered in this case.
784         // also checks whether media routing if affected by a dynamic policy or mirroring
785         if (((device == musicDevice) || mDeviceBroker.isInCommunication())
786                 && AudioSystem.isSingleAudioDeviceType(devices, device)
787                 && !mDeviceBroker.hasMediaDynamicPolicy()
788                 && (musicDevice != AudioSystem.DEVICE_OUT_REMOTE_SUBMIX)) {
789             if (!AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0 /*not looking in past*/)
790                     && !mDeviceBroker.hasAudioFocusUsers()) {
791                 // no media playback, not a "becoming noisy" situation, otherwise it could cause
792                 // the pausing of some apps that are playing remotely
793                 AudioService.sDeviceLogger.log((new AudioEventLogger.StringEvent(
794                         "dropping ACTION_AUDIO_BECOMING_NOISY")).printLog(TAG));
795                 return 0;
796             }
797             mDeviceBroker.postBroadcastBecomingNoisy();
798             delay = AudioService.BECOMING_NOISY_DELAY_MS;
799         }
800 
801         return delay;
802     }
803 
804     // Intent "extra" data keys.
805     private static final String CONNECT_INTENT_KEY_PORT_NAME = "portName";
806     private static final String CONNECT_INTENT_KEY_STATE = "state";
807     private static final String CONNECT_INTENT_KEY_ADDRESS = "address";
808     private static final String CONNECT_INTENT_KEY_HAS_PLAYBACK = "hasPlayback";
809     private static final String CONNECT_INTENT_KEY_HAS_CAPTURE = "hasCapture";
810     private static final String CONNECT_INTENT_KEY_HAS_MIDI = "hasMIDI";
811     private static final String CONNECT_INTENT_KEY_DEVICE_CLASS = "class";
812 
sendDeviceConnectionIntent(int device, int state, String address, String deviceName)813     private void sendDeviceConnectionIntent(int device, int state, String address,
814                                             String deviceName) {
815         if (AudioService.DEBUG_DEVICES) {
816             Slog.i(TAG, "sendDeviceConnectionIntent(dev:0x" + Integer.toHexString(device)
817                     + " state:0x" + Integer.toHexString(state) + " address:" + address
818                     + " name:" + deviceName + ");");
819         }
820         Intent intent = new Intent();
821 
822         switch(device) {
823             case AudioSystem.DEVICE_OUT_WIRED_HEADSET:
824                 intent.setAction(Intent.ACTION_HEADSET_PLUG);
825                 intent.putExtra("microphone", 1);
826                 break;
827             case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE:
828             case AudioSystem.DEVICE_OUT_LINE:
829                 intent.setAction(Intent.ACTION_HEADSET_PLUG);
830                 intent.putExtra("microphone", 0);
831                 break;
832             case AudioSystem.DEVICE_OUT_USB_HEADSET:
833                 intent.setAction(Intent.ACTION_HEADSET_PLUG);
834                 intent.putExtra("microphone",
835                         AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_IN_USB_HEADSET, "")
836                                 == AudioSystem.DEVICE_STATE_AVAILABLE ? 1 : 0);
837                 break;
838             case AudioSystem.DEVICE_IN_USB_HEADSET:
839                 if (AudioSystem.getDeviceConnectionState(AudioSystem.DEVICE_OUT_USB_HEADSET, "")
840                         == AudioSystem.DEVICE_STATE_AVAILABLE) {
841                     intent.setAction(Intent.ACTION_HEADSET_PLUG);
842                     intent.putExtra("microphone", 1);
843                 } else {
844                     // do not send ACTION_HEADSET_PLUG when only the input side is seen as changing
845                     return;
846                 }
847                 break;
848             case AudioSystem.DEVICE_OUT_HDMI:
849             case AudioSystem.DEVICE_OUT_HDMI_ARC:
850                 configureHdmiPlugIntent(intent, state);
851                 break;
852         }
853 
854         if (intent.getAction() == null) {
855             return;
856         }
857 
858         intent.putExtra(CONNECT_INTENT_KEY_STATE, state);
859         intent.putExtra(CONNECT_INTENT_KEY_ADDRESS, address);
860         intent.putExtra(CONNECT_INTENT_KEY_PORT_NAME, deviceName);
861 
862         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
863 
864         final long ident = Binder.clearCallingIdentity();
865         try {
866             ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_CURRENT);
867         } finally {
868             Binder.restoreCallingIdentity(ident);
869         }
870     }
871 
updateAudioRoutes(int device, int state)872     private void updateAudioRoutes(int device, int state) {
873         int connType = 0;
874 
875         switch (device) {
876             case AudioSystem.DEVICE_OUT_WIRED_HEADSET:
877                 connType = AudioRoutesInfo.MAIN_HEADSET;
878                 break;
879             case AudioSystem.DEVICE_OUT_WIRED_HEADPHONE:
880             case AudioSystem.DEVICE_OUT_LINE:
881                 connType = AudioRoutesInfo.MAIN_HEADPHONES;
882                 break;
883             case AudioSystem.DEVICE_OUT_HDMI:
884             case AudioSystem.DEVICE_OUT_HDMI_ARC:
885                 connType = AudioRoutesInfo.MAIN_HDMI;
886                 break;
887             case AudioSystem.DEVICE_OUT_USB_DEVICE:
888             case AudioSystem.DEVICE_OUT_USB_HEADSET:
889                 connType = AudioRoutesInfo.MAIN_USB;
890                 break;
891         }
892 
893         synchronized (mCurAudioRoutes) {
894             if (connType == 0) {
895                 return;
896             }
897             int newConn = mCurAudioRoutes.mainType;
898             if (state != 0) {
899                 newConn |= connType;
900             } else {
901                 newConn &= ~connType;
902             }
903             if (newConn != mCurAudioRoutes.mainType) {
904                 mCurAudioRoutes.mainType = newConn;
905                 mDeviceBroker.postReportNewRoutes();
906             }
907         }
908     }
909 
configureHdmiPlugIntent(Intent intent, @AudioService.ConnectionState int state)910     private void configureHdmiPlugIntent(Intent intent, @AudioService.ConnectionState int state) {
911         intent.setAction(AudioManager.ACTION_HDMI_AUDIO_PLUG);
912         intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, state);
913         if (state != AudioService.CONNECTION_STATE_CONNECTED) {
914             return;
915         }
916         ArrayList<AudioPort> ports = new ArrayList<AudioPort>();
917         int[] portGeneration = new int[1];
918         int status = AudioSystem.listAudioPorts(ports, portGeneration);
919         if (status != AudioManager.SUCCESS) {
920             Log.e(TAG, "listAudioPorts error " + status + " in configureHdmiPlugIntent");
921             return;
922         }
923         for (AudioPort port : ports) {
924             if (!(port instanceof AudioDevicePort)) {
925                 continue;
926             }
927             final AudioDevicePort devicePort = (AudioDevicePort) port;
928             if (devicePort.type() != AudioManager.DEVICE_OUT_HDMI
929                     && devicePort.type() != AudioManager.DEVICE_OUT_HDMI_ARC) {
930                 continue;
931             }
932             // found an HDMI port: format the list of supported encodings
933             int[] formats = AudioFormat.filterPublicFormats(devicePort.formats());
934             if (formats.length > 0) {
935                 ArrayList<Integer> encodingList = new ArrayList(1);
936                 for (int format : formats) {
937                     // a format in the list can be 0, skip it
938                     if (format != AudioFormat.ENCODING_INVALID) {
939                         encodingList.add(format);
940                     }
941                 }
942                 final int[] encodingArray = encodingList.stream().mapToInt(i -> i).toArray();
943                 intent.putExtra(AudioManager.EXTRA_ENCODINGS, encodingArray);
944             }
945             // find the maximum supported number of channels
946             int maxChannels = 0;
947             for (int mask : devicePort.channelMasks()) {
948                 int channelCount = AudioFormat.channelCountFromOutChannelMask(mask);
949                 if (channelCount > maxChannels) {
950                     maxChannels = channelCount;
951                 }
952             }
953             intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannels);
954         }
955     }
956 
957     //----------------------------------------------------------
958     // For tests only
959 
960     /**
961      * Check if device is in the list of connected devices
962      * @param device
963      * @return true if connected
964      */
965     @VisibleForTesting
isA2dpDeviceConnected(@onNull BluetoothDevice device)966     public boolean isA2dpDeviceConnected(@NonNull BluetoothDevice device) {
967         final String key = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
968                 device.getAddress());
969         synchronized (mConnectedDevices) {
970             return (mConnectedDevices.get(key) != null);
971         }
972     }
973 }
974