• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.Nullable;
20 import android.bluetooth.BluetoothA2dp;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothClass;
23 import android.bluetooth.BluetoothCodecConfig;
24 import android.bluetooth.BluetoothCodecStatus;
25 import android.bluetooth.BluetoothDevice;
26 import android.bluetooth.BluetoothHeadset;
27 import android.bluetooth.BluetoothHearingAid;
28 import android.bluetooth.BluetoothLeAudio;
29 import android.bluetooth.BluetoothProfile;
30 import android.content.Intent;
31 import android.media.AudioDeviceAttributes;
32 import android.media.AudioManager;
33 import android.media.AudioSystem;
34 import android.media.BluetoothProfileConnectionInfo;
35 import android.os.Binder;
36 import android.os.UserHandle;
37 import android.provider.Settings;
38 import android.text.TextUtils;
39 import android.util.Log;
40 
41 import com.android.internal.annotations.GuardedBy;
42 
43 import java.io.PrintWriter;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Objects;
47 
48 /**
49  * @hide
50  * Class to encapsulate all communication with Bluetooth services
51  */
52 public class BtHelper {
53 
54     private static final String TAG = "AS.BtHelper";
55 
56     private static final int SOURCE_CODEC_TYPE_OPUS = 6; // TODO remove in U
57 
58     private final @NonNull AudioDeviceBroker mDeviceBroker;
59 
BtHelper(@onNull AudioDeviceBroker broker)60     BtHelper(@NonNull AudioDeviceBroker broker) {
61         mDeviceBroker = broker;
62     }
63 
64     // BluetoothHeadset API to control SCO connection
65     private @Nullable BluetoothHeadset mBluetoothHeadset;
66 
67     // Bluetooth headset device
68     private @Nullable BluetoothDevice mBluetoothHeadsetDevice;
69 
70     private @Nullable BluetoothHearingAid mHearingAid;
71 
72     private @Nullable BluetoothLeAudio mLeAudio;
73 
74     // Reference to BluetoothA2dp to query for AbsoluteVolume.
75     private @Nullable BluetoothA2dp mA2dp;
76 
77     // If absolute volume is supported in AVRCP device
78     private boolean mAvrcpAbsVolSupported = false;
79 
80     // Current connection state indicated by bluetooth headset
81     private int mScoConnectionState;
82 
83     // Indicate if SCO audio connection is currently active and if the initiator is
84     // audio service (internal) or bluetooth headset (external)
85     private int mScoAudioState;
86 
87     // Indicates the mode used for SCO audio connection. The mode is virtual call if the request
88     // originated from an app targeting an API version before JB MR2 and raw audio after that.
89     private int mScoAudioMode;
90 
91     // SCO audio state is not active
92     private static final int SCO_STATE_INACTIVE = 0;
93     // SCO audio activation request waiting for headset service to connect
94     private static final int SCO_STATE_ACTIVATE_REQ = 1;
95     // SCO audio state is active due to an action in BT handsfree (either voice recognition or
96     // in call audio)
97     private static final int SCO_STATE_ACTIVE_EXTERNAL = 2;
98     // SCO audio state is active or starting due to a request from AudioManager API
99     private static final int SCO_STATE_ACTIVE_INTERNAL = 3;
100     // SCO audio deactivation request waiting for headset service to connect
101     private static final int SCO_STATE_DEACTIVATE_REQ = 4;
102     // SCO audio deactivation in progress, waiting for Bluetooth audio intent
103     private static final int SCO_STATE_DEACTIVATING = 5;
104 
105     // SCO audio mode is undefined
106     /*package*/  static final int SCO_MODE_UNDEFINED = -1;
107     // SCO audio mode is virtual voice call (BluetoothHeadset.startScoUsingVirtualVoiceCall())
108     /*package*/  static final int SCO_MODE_VIRTUAL_CALL = 0;
109     // SCO audio mode is Voice Recognition (BluetoothHeadset.startVoiceRecognition())
110     private static final int SCO_MODE_VR = 2;
111     // max valid SCO audio mode values
112     private static final int SCO_MODE_MAX = 2;
113 
114     private static final int BT_HEARING_AID_GAIN_MIN = -128;
115     private static final int BT_LE_AUDIO_MIN_VOL = 0;
116     private static final int BT_LE_AUDIO_MAX_VOL = 255;
117 
118     /**
119      * Returns a string representation of the scoAudioMode.
120      */
scoAudioModeToString(int scoAudioMode)121     public static String scoAudioModeToString(int scoAudioMode) {
122         switch (scoAudioMode) {
123             case SCO_MODE_UNDEFINED:
124                 return "SCO_MODE_UNDEFINED";
125             case SCO_MODE_VIRTUAL_CALL:
126                 return "SCO_MODE_VIRTUAL_CALL";
127             case SCO_MODE_VR:
128                 return "SCO_MODE_VR";
129             default:
130                 return "SCO_MODE_(" + scoAudioMode + ")";
131         }
132     }
133 
134     /**
135      * Returns a string representation of the scoAudioState.
136      */
scoAudioStateToString(int scoAudioState)137     public static String scoAudioStateToString(int scoAudioState) {
138         switch (scoAudioState) {
139             case SCO_STATE_INACTIVE:
140                 return "SCO_STATE_INACTIVE";
141             case SCO_STATE_ACTIVATE_REQ:
142                 return "SCO_STATE_ACTIVATE_REQ";
143             case SCO_STATE_ACTIVE_EXTERNAL:
144                 return "SCO_STATE_ACTIVE_EXTERNAL";
145             case SCO_STATE_ACTIVE_INTERNAL:
146                 return "SCO_STATE_ACTIVE_INTERNAL";
147             case SCO_STATE_DEACTIVATING:
148                 return "SCO_STATE_DEACTIVATING";
149             default:
150                 return "SCO_STATE_(" + scoAudioState + ")";
151         }
152     }
153 
154     //----------------------------------------------------------------------
155     /*package*/ static class BluetoothA2dpDeviceInfo {
156         private final @NonNull BluetoothDevice mBtDevice;
157         private final int mVolume;
158         private final @AudioSystem.AudioFormatNativeEnumForBtCodec int mCodec;
159 
BluetoothA2dpDeviceInfo(@onNull BluetoothDevice btDevice)160         BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) {
161             this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT);
162         }
163 
BluetoothA2dpDeviceInfo(@onNull BluetoothDevice btDevice, int volume, int codec)164         BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) {
165             mBtDevice = btDevice;
166             mVolume = volume;
167             mCodec = codec;
168         }
169 
getBtDevice()170         public @NonNull BluetoothDevice getBtDevice() {
171             return mBtDevice;
172         }
173 
getVolume()174         public int getVolume() {
175             return mVolume;
176         }
177 
getCodec()178         public @AudioSystem.AudioFormatNativeEnumForBtCodec int getCodec() {
179             return mCodec;
180         }
181 
182         // redefine equality op so we can match messages intended for this device
183         @Override
equals(Object o)184         public boolean equals(Object o) {
185             if (o == null) {
186                 return false;
187             }
188             if (this == o) {
189                 return true;
190             }
191             if (o instanceof BluetoothA2dpDeviceInfo) {
192                 return mBtDevice.equals(((BluetoothA2dpDeviceInfo) o).getBtDevice());
193             }
194             return false;
195         }
196 
197 
198     }
199 
200     // A2DP device events
201     /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0;
202     /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1;
203 
a2dpDeviceEventToString(int event)204     /*package*/ static String a2dpDeviceEventToString(int event) {
205         switch (event) {
206             case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE";
207             case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE";
208             default:
209                 return new String("invalid event:" + event);
210         }
211     }
212 
getName(@onNull BluetoothDevice device)213     /*package*/ @NonNull static String getName(@NonNull BluetoothDevice device) {
214         final String deviceName = device.getName();
215         if (deviceName == null) {
216             return "";
217         }
218         return deviceName;
219     }
220 
221     //----------------------------------------------------------------------
222     // Interface for AudioDeviceBroker
223 
224     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
225     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onSystemReady()226     /*package*/ synchronized void onSystemReady() {
227         mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
228         resetBluetoothSco();
229         getBluetoothHeadset();
230 
231         //FIXME: this is to maintain compatibility with deprecated intent
232         // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
233         Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
234         newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
235                 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
236         sendStickyBroadcastToAll(newIntent);
237 
238         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
239         if (adapter != null) {
240             adapter.getProfileProxy(mDeviceBroker.getContext(),
241                     mBluetoothProfileServiceListener, BluetoothProfile.A2DP);
242             adapter.getProfileProxy(mDeviceBroker.getContext(),
243                     mBluetoothProfileServiceListener, BluetoothProfile.HEARING_AID);
244             adapter.getProfileProxy(mDeviceBroker.getContext(),
245                     mBluetoothProfileServiceListener, BluetoothProfile.LE_AUDIO);
246         }
247     }
248 
onAudioServerDiedRestoreA2dp()249     /*package*/ synchronized void onAudioServerDiedRestoreA2dp() {
250         final int forMed = mDeviceBroker.getBluetoothA2dpEnabled()
251                 ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
252         mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()");
253     }
254 
isAvrcpAbsoluteVolumeSupported()255     /*package*/ synchronized boolean isAvrcpAbsoluteVolumeSupported() {
256         return (mA2dp != null && mAvrcpAbsVolSupported);
257     }
258 
setAvrcpAbsoluteVolumeSupported(boolean supported)259     /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
260         mAvrcpAbsVolSupported = supported;
261         Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
262     }
263 
setAvrcpAbsoluteVolumeIndex(int index)264     /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
265         if (mA2dp == null) {
266             if (AudioService.DEBUG_VOL) {
267                 AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
268                         "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG));
269                 return;
270             }
271         }
272         if (!mAvrcpAbsVolSupported) {
273             AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent(
274                     "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG));
275             return;
276         }
277         if (AudioService.DEBUG_VOL) {
278             Log.i(TAG, "setAvrcpAbsoluteVolumeIndex index=" + index);
279         }
280         AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
281                 AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index));
282         try {
283             mA2dp.setAvrcpAbsoluteVolume(index);
284         } catch (Exception e) {
285             Log.e(TAG, "Exception while changing abs volume", e);
286         }
287     }
288 
getA2dpCodec( @onNull BluetoothDevice device)289     /*package*/ synchronized @AudioSystem.AudioFormatNativeEnumForBtCodec int getA2dpCodec(
290             @NonNull BluetoothDevice device) {
291         if (mA2dp == null) {
292             return AudioSystem.AUDIO_FORMAT_DEFAULT;
293         }
294         BluetoothCodecStatus btCodecStatus = null;
295         try {
296             btCodecStatus = mA2dp.getCodecStatus(device);
297         } catch (Exception e) {
298             Log.e(TAG, "Exception while getting status of " + device, e);
299         }
300         if (btCodecStatus == null) {
301             return AudioSystem.AUDIO_FORMAT_DEFAULT;
302         }
303         final BluetoothCodecConfig btCodecConfig = btCodecStatus.getCodecConfig();
304         if (btCodecConfig == null) {
305             return AudioSystem.AUDIO_FORMAT_DEFAULT;
306         }
307         return AudioSystem.bluetoothCodecToAudioFormat(btCodecConfig.getCodecType());
308     }
309 
310     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
311     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
receiveBtEvent(Intent intent)312     /*package*/ synchronized void receiveBtEvent(Intent intent) {
313         final String action = intent.getAction();
314 
315         Log.i(TAG, "receiveBtEvent action: " + action + " mScoAudioState: " + mScoAudioState);
316         if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
317             BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
318             setBtScoActiveDevice(btDevice);
319         } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
320             int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
321             Log.i(TAG,"receiveBtEvent ACTION_AUDIO_STATE_CHANGED: "+btState);
322             mDeviceBroker.postScoAudioStateChanged(btState);
323         }
324     }
325 
326     /**
327      * Exclusively called from AudioDeviceBroker when handling MSG_I_SCO_AUDIO_STATE_CHANGED
328      * as part of the serialization of the communication route selection
329      */
330     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
331     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onScoAudioStateChanged(int state)332     void onScoAudioStateChanged(int state) {
333         boolean broadcast = false;
334         int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
335         switch (state) {
336             case BluetoothHeadset.STATE_AUDIO_CONNECTED:
337                 scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED;
338                 if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
339                         && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
340                     mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
341                 } else if (mDeviceBroker.isBluetoothScoRequested()) {
342                     // broadcast intent if the connection was initated by AudioService
343                     broadcast = true;
344                 }
345                 mDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent");
346                 break;
347             case BluetoothHeadset.STATE_AUDIO_DISCONNECTED:
348                 mDeviceBroker.setBluetoothScoOn(false, "BtHelper.receiveBtEvent");
349                 scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED;
350                 // There are two cases where we want to immediately reconnect audio:
351                 // 1) If a new start request was received while disconnecting: this was
352                 // notified by requestScoState() setting state to SCO_STATE_ACTIVATE_REQ.
353                 // 2) If audio was connected then disconnected via Bluetooth APIs and
354                 // we still have pending activation requests by apps: this is indicated by
355                 // state SCO_STATE_ACTIVE_EXTERNAL and BT SCO is requested.
356                 if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) {
357                     if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null
358                             && connectBluetoothScoAudioHelper(mBluetoothHeadset,
359                             mBluetoothHeadsetDevice, mScoAudioMode)) {
360                         mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
361                         scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTING;
362                         broadcast = true;
363                         break;
364                     }
365                 }
366                 if (mScoAudioState != SCO_STATE_ACTIVE_EXTERNAL) {
367                     broadcast = true;
368                 }
369                 mScoAudioState = SCO_STATE_INACTIVE;
370                 break;
371             case BluetoothHeadset.STATE_AUDIO_CONNECTING:
372                 if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL
373                         && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
374                     mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
375                 }
376                 break;
377             default:
378                 break;
379         }
380         if(broadcast) {
381             broadcastScoConnectionState(scoAudioState);
382             //FIXME: this is to maintain compatibility with deprecated intent
383             // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate.
384             Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED);
385             newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState);
386             sendStickyBroadcastToAll(newIntent);
387         }
388 
389     }
390     /**
391      *
392      * @return false if SCO isn't connected
393      */
isBluetoothScoOn()394     /*package*/ synchronized boolean isBluetoothScoOn() {
395         if (mBluetoothHeadset == null || mBluetoothHeadsetDevice == null) {
396             return false;
397         }
398         return mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
399                 == BluetoothHeadset.STATE_AUDIO_CONNECTED;
400     }
401 
402     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
403     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
startBluetoothSco(int scoAudioMode, @NonNull String eventSource)404     /*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
405                 @NonNull String eventSource) {
406         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource));
407         return requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
408     }
409 
410     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
411     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
stopBluetoothSco(@onNull String eventSource)412     /*package*/ synchronized boolean stopBluetoothSco(@NonNull String eventSource) {
413         AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource));
414         return requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, SCO_MODE_VIRTUAL_CALL);
415     }
416 
setLeAudioVolume(int index, int maxIndex, int streamType)417     /*package*/ synchronized void setLeAudioVolume(int index, int maxIndex, int streamType) {
418         if (mLeAudio == null) {
419             if (AudioService.DEBUG_VOL) {
420                 Log.i(TAG, "setLeAudioVolume: null mLeAudio");
421             }
422             return;
423         }
424         /* leaudio expect volume value in range 0 to 255 */
425         int volume = (int) Math.round((double) index * BT_LE_AUDIO_MAX_VOL / maxIndex);
426 
427         if (AudioService.DEBUG_VOL) {
428             Log.i(TAG, "setLeAudioVolume: calling mLeAudio.setVolume idx="
429                     + index + " volume=" + volume);
430         }
431         AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
432                 AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex));
433         try {
434             mLeAudio.setVolume(volume);
435         } catch (Exception e) {
436             Log.e(TAG, "Exception while setting LE volume", e);
437         }
438     }
439 
setHearingAidVolume(int index, int streamType, boolean isHeadAidConnected)440     /*package*/ synchronized void setHearingAidVolume(int index, int streamType,
441             boolean isHeadAidConnected) {
442         if (mHearingAid == null) {
443             if (AudioService.DEBUG_VOL) {
444                 Log.i(TAG, "setHearingAidVolume: null mHearingAid");
445             }
446             return;
447         }
448         //hearing aid expect volume value in range -128dB to 0dB
449         int gainDB = (int) AudioSystem.getStreamVolumeDB(streamType, index / 10,
450                 AudioSystem.DEVICE_OUT_HEARING_AID);
451         if (gainDB < BT_HEARING_AID_GAIN_MIN) {
452             gainDB = BT_HEARING_AID_GAIN_MIN;
453         }
454         if (AudioService.DEBUG_VOL) {
455             Log.i(TAG, "setHearingAidVolume: calling mHearingAid.setVolume idx="
456                     + index + " gain=" + gainDB);
457         }
458         // do not log when hearing aid is not connected to avoid confusion when reading dumpsys
459         if (isHeadAidConnected) {
460             AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent(
461                     AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB));
462         }
463         try {
464             mHearingAid.setVolume(gainDB);
465         } catch (Exception e) {
466             Log.i(TAG, "Exception while setting hearing aid volume", e);
467         }
468     }
469 
onBroadcastScoConnectionState(int state)470     /*package*/ synchronized void onBroadcastScoConnectionState(int state) {
471         if (state == mScoConnectionState) {
472             return;
473         }
474         Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
475         newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state);
476         newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE,
477                 mScoConnectionState);
478         sendStickyBroadcastToAll(newIntent);
479         mScoConnectionState = state;
480     }
481 
disconnectAllBluetoothProfiles()482     /*package*/ synchronized void disconnectAllBluetoothProfiles() {
483         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.A2DP);
484         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.A2DP_SINK);
485         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.HEADSET);
486         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.HEARING_AID);
487         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.LE_AUDIO);
488         mDeviceBroker.postBtProfileDisconnected(BluetoothProfile.LE_AUDIO_BROADCAST);
489     }
490 
491     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
492     //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
resetBluetoothSco()493     /*package*/ synchronized void resetBluetoothSco() {
494         mScoAudioState = SCO_STATE_INACTIVE;
495         broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
496         AudioSystem.setParameters("A2dpSuspended=false");
497         mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
498     }
499 
500     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
501     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
disconnectHeadset()502     /*package*/ synchronized void disconnectHeadset() {
503         setBtScoActiveDevice(null);
504         mBluetoothHeadset = null;
505     }
506 
507     //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onBtProfileDisconnected(int profile)508     /*package*/ synchronized void onBtProfileDisconnected(int profile) {
509         switch (profile) {
510             case BluetoothProfile.A2DP:
511                 mA2dp = null;
512                 break;
513             case BluetoothProfile.HEARING_AID:
514                 mHearingAid = null;
515                 break;
516             case BluetoothProfile.LE_AUDIO:
517                 mLeAudio = null;
518                 break;
519 
520             case BluetoothProfile.A2DP_SINK:
521             case BluetoothProfile.LE_AUDIO_BROADCAST:
522                 // shouldn't be received here as profile doesn't involve BtHelper
523                 Log.e(TAG, "onBtProfileDisconnected: Not a profile handled by BtHelper "
524                         + BluetoothProfile.getProfileName(profile));
525                 break;
526 
527             default:
528                 // Not a valid profile to disconnect
529                 Log.e(TAG, "onBtProfileDisconnected: Not a valid profile to disconnect "
530                         + BluetoothProfile.getProfileName(profile));
531                 break;
532         }
533     }
534 
535     @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onBtProfileConnected(int profile, BluetoothProfile proxy)536     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
537         if (profile == BluetoothProfile.HEADSET) {
538             onHeadsetProfileConnected((BluetoothHeadset) proxy);
539             return;
540         }
541         if (profile == BluetoothProfile.A2DP) {
542             mA2dp = (BluetoothA2dp) proxy;
543         } else if (profile == BluetoothProfile.HEARING_AID) {
544             mHearingAid = (BluetoothHearingAid) proxy;
545         } else if (profile == BluetoothProfile.LE_AUDIO) {
546             mLeAudio = (BluetoothLeAudio) proxy;
547         }
548         final List<BluetoothDevice> deviceList = proxy.getConnectedDevices();
549         if (deviceList.isEmpty()) {
550             return;
551         }
552         final BluetoothDevice btDevice = deviceList.get(0);
553         if (proxy.getConnectionState(btDevice) == BluetoothProfile.STATE_CONNECTED) {
554             mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
555                     new AudioDeviceBroker.BtDeviceChangedData(btDevice, null,
556                         new BluetoothProfileConnectionInfo(profile),
557                         "mBluetoothProfileServiceListener"));
558         } else {
559             mDeviceBroker.queueOnBluetoothActiveDeviceChanged(
560                     new AudioDeviceBroker.BtDeviceChangedData(null, btDevice,
561                         new BluetoothProfileConnectionInfo(profile),
562                         "mBluetoothProfileServiceListener"));
563         }
564     }
565 
566     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
567     //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
onHeadsetProfileConnected(BluetoothHeadset headset)568     /*package*/ synchronized void onHeadsetProfileConnected(BluetoothHeadset headset) {
569         // Discard timeout message
570         mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
571         mBluetoothHeadset = headset;
572         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
573         List<BluetoothDevice> activeDevices = Collections.emptyList();
574         if (adapter != null) {
575             activeDevices = adapter.getActiveDevices(BluetoothProfile.HEADSET);
576         }
577         setBtScoActiveDevice((activeDevices.size() > 0) ? activeDevices.get(0) : null);
578         // Refresh SCO audio state
579         checkScoAudioState();
580         if (mScoAudioState != SCO_STATE_ACTIVATE_REQ
581                 && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) {
582             return;
583         }
584         boolean status = false;
585         if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null) {
586             switch (mScoAudioState) {
587                 case SCO_STATE_ACTIVATE_REQ:
588                     status = connectBluetoothScoAudioHelper(
589                             mBluetoothHeadset,
590                             mBluetoothHeadsetDevice, mScoAudioMode);
591                     if (status) {
592                         mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
593                     }
594                     break;
595                 case SCO_STATE_DEACTIVATE_REQ:
596                     status = disconnectBluetoothScoAudioHelper(
597                             mBluetoothHeadset,
598                             mBluetoothHeadsetDevice, mScoAudioMode);
599                     if (status) {
600                         mScoAudioState = SCO_STATE_DEACTIVATING;
601                     }
602                     break;
603             }
604         }
605         if (!status) {
606             mScoAudioState = SCO_STATE_INACTIVE;
607             broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
608         }
609     }
610 
611     //----------------------------------------------------------------------
broadcastScoConnectionState(int state)612     private void broadcastScoConnectionState(int state) {
613         mDeviceBroker.postBroadcastScoConnectionState(state);
614     }
615 
getHeadsetAudioDevice()616     @Nullable AudioDeviceAttributes getHeadsetAudioDevice() {
617         if (mBluetoothHeadsetDevice == null) {
618             return null;
619         }
620         return btHeadsetDeviceToAudioDevice(mBluetoothHeadsetDevice);
621     }
622 
btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice)623     private AudioDeviceAttributes btHeadsetDeviceToAudioDevice(BluetoothDevice btDevice) {
624         if (btDevice == null) {
625             return new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, "");
626         }
627         String address = btDevice.getAddress();
628         if (!BluetoothAdapter.checkBluetoothAddress(address)) {
629             address = "";
630         }
631         BluetoothClass btClass = btDevice.getBluetoothClass();
632         int nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO;
633         if (btClass != null) {
634             switch (btClass.getDeviceClass()) {
635                 case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
636                 case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
637                     nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
638                     break;
639                 case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
640                     nativeType = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
641                     break;
642             }
643         }
644         if (AudioService.DEBUG_DEVICES) {
645             Log.i(TAG, "btHeadsetDeviceToAudioDevice btDevice: " + btDevice
646                     + " btClass: " + (btClass == null ? "Unknown" : btClass)
647                     + " nativeType: " + nativeType + " address: " + address);
648         }
649         return new AudioDeviceAttributes(nativeType, address);
650     }
651 
handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive)652     private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
653         if (btDevice == null) {
654             return true;
655         }
656         int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
657         AudioDeviceAttributes audioDevice =  btHeadsetDeviceToAudioDevice(btDevice);
658         String btDeviceName =  getName(btDevice);
659         boolean result = false;
660         if (isActive) {
661             result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
662                     audioDevice.getInternalType(), audioDevice.getAddress(), btDeviceName),
663                     isActive);
664         } else {
665             int[] outDeviceTypes = {
666                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO,
667                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET,
668                     AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT
669             };
670             for (int outDeviceType : outDeviceTypes) {
671                 result |= mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
672                         outDeviceType, audioDevice.getAddress(), btDeviceName),
673                         isActive);
674             }
675         }
676         // handleDeviceConnection() && result to make sure the method get executed
677         result = mDeviceBroker.handleDeviceConnection(new AudioDeviceAttributes(
678                         inDevice, audioDevice.getAddress(), btDeviceName),
679                 isActive) && result;
680         return result;
681     }
682 
683     // Return `(null)` if given BluetoothDevice is null. Otherwise, return the anonymized address.
getAnonymizedAddress(BluetoothDevice btDevice)684     private String getAnonymizedAddress(BluetoothDevice btDevice) {
685         return btDevice == null ? "(null)" : btDevice.getAnonymizedAddress();
686     }
687 
688     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
689     //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
690     @GuardedBy("BtHelper.this")
setBtScoActiveDevice(BluetoothDevice btDevice)691     private void setBtScoActiveDevice(BluetoothDevice btDevice) {
692         Log.i(TAG, "setBtScoActiveDevice: " + getAnonymizedAddress(mBluetoothHeadsetDevice)
693                 + " -> " + getAnonymizedAddress(btDevice));
694         final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
695         if (Objects.equals(btDevice, previousActiveDevice)) {
696             return;
697         }
698         if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) {
699             Log.w(TAG, "setBtScoActiveDevice() failed to remove previous device "
700                     + getAnonymizedAddress(previousActiveDevice));
701         }
702         if (!handleBtScoActiveDeviceChange(btDevice, true)) {
703             Log.e(TAG, "setBtScoActiveDevice() failed to add new device "
704                     + getAnonymizedAddress(btDevice));
705             // set mBluetoothHeadsetDevice to null when failing to add new device
706             btDevice = null;
707         }
708         mBluetoothHeadsetDevice = btDevice;
709         if (mBluetoothHeadsetDevice == null) {
710             resetBluetoothSco();
711         }
712     }
713 
714     // NOTE this listener is NOT called from AudioDeviceBroker event thread, only call async
715     //      methods inside listener.
716     private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
717             new BluetoothProfile.ServiceListener() {
718                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
719                     switch(profile) {
720                         case BluetoothProfile.A2DP:
721                         case BluetoothProfile.HEADSET:
722                         case BluetoothProfile.HEARING_AID:
723                         case BluetoothProfile.LE_AUDIO:
724                             AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
725                                     "BT profile service: connecting "
726                                     + BluetoothProfile.getProfileName(profile) + " profile"));
727                             mDeviceBroker.postBtProfileConnected(profile, proxy);
728                             break;
729 
730                         case BluetoothProfile.A2DP_SINK:
731                             // no A2DP sink functionality handled by BtHelper
732                         case BluetoothProfile.LE_AUDIO_BROADCAST:
733                             // no broadcast functionality handled by BtHelper
734                         default:
735                             break;
736                     }
737                 }
738                 public void onServiceDisconnected(int profile) {
739 
740                     switch (profile) {
741                         case BluetoothProfile.A2DP:
742                         case BluetoothProfile.HEADSET:
743                         case BluetoothProfile.HEARING_AID:
744                         case BluetoothProfile.LE_AUDIO:
745                             AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(
746                                     "BT profile service: disconnecting "
747                                         + BluetoothProfile.getProfileName(profile) + " profile"));
748                             mDeviceBroker.postBtProfileDisconnected(profile);
749                             break;
750 
751                         case BluetoothProfile.A2DP_SINK:
752                             // no A2DP sink functionality handled by BtHelper
753                         case BluetoothProfile.LE_AUDIO_BROADCAST:
754                             // no broadcast functionality handled by BtHelper
755                         default:
756                             break;
757                     }
758                 }
759             };
760 
761     //----------------------------------------------------------------------
762 
763     // @GuardedBy("AudioDeviceBroker.mSetModeLock")
764     //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
765     @GuardedBy("BtHelper.this")
requestScoState(int state, int scoAudioMode)766     private boolean requestScoState(int state, int scoAudioMode) {
767         checkScoAudioState();
768         if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
769             // Make sure that the state transitions to CONNECTING even if we cannot initiate
770             // the connection.
771             broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING);
772             switch (mScoAudioState) {
773                 case SCO_STATE_INACTIVE:
774                     mScoAudioMode = scoAudioMode;
775                     if (scoAudioMode == SCO_MODE_UNDEFINED) {
776                         mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
777                         if (mBluetoothHeadsetDevice != null) {
778                             mScoAudioMode = Settings.Global.getInt(
779                                     mDeviceBroker.getContentResolver(),
780                                     "bluetooth_sco_channel_"
781                                             + mBluetoothHeadsetDevice.getAddress(),
782                                     SCO_MODE_VIRTUAL_CALL);
783                             if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) {
784                                 mScoAudioMode = SCO_MODE_VIRTUAL_CALL;
785                             }
786                         }
787                     }
788                     if (mBluetoothHeadset == null) {
789                         if (getBluetoothHeadset()) {
790                             mScoAudioState = SCO_STATE_ACTIVATE_REQ;
791                         } else {
792                             Log.w(TAG, "requestScoState: getBluetoothHeadset failed during"
793                                     + " connection, mScoAudioMode=" + mScoAudioMode);
794                             broadcastScoConnectionState(
795                                     AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
796                             return false;
797                         }
798                         break;
799                     }
800                     if (mBluetoothHeadsetDevice == null) {
801                         Log.w(TAG, "requestScoState: no active device while connecting,"
802                                 + " mScoAudioMode=" + mScoAudioMode);
803                         broadcastScoConnectionState(
804                                 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
805                         return false;
806                     }
807                     if (connectBluetoothScoAudioHelper(mBluetoothHeadset,
808                             mBluetoothHeadsetDevice, mScoAudioMode)) {
809                         mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
810                     } else {
811                         Log.w(TAG, "requestScoState: connect to "
812                                 + getAnonymizedAddress(mBluetoothHeadsetDevice)
813                                 + " failed, mScoAudioMode=" + mScoAudioMode);
814                         broadcastScoConnectionState(
815                                 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
816                         return false;
817                     }
818                     break;
819                 case SCO_STATE_DEACTIVATING:
820                     mScoAudioState = SCO_STATE_ACTIVATE_REQ;
821                     break;
822                 case SCO_STATE_DEACTIVATE_REQ:
823                     mScoAudioState = SCO_STATE_ACTIVE_INTERNAL;
824                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
825                     break;
826                 case SCO_STATE_ACTIVE_INTERNAL:
827                     Log.w(TAG, "requestScoState: already in ACTIVE mode, simply return");
828                     break;
829                 case SCO_STATE_ACTIVE_EXTERNAL:
830                     /* Confirm SCO Audio connection to requesting app as it is already connected
831                      * externally (i.e. through SCO APIs by Telecom service).
832                      * Once SCO Audio is disconnected by the external owner, we will reconnect it
833                      * automatically on behalf of the requesting app and the state will move to
834                      * SCO_STATE_ACTIVE_INTERNAL.
835                      */
836                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED);
837                     break;
838                 default:
839                     Log.w(TAG, "requestScoState: failed to connect in state "
840                             + mScoAudioState + ", scoAudioMode=" + scoAudioMode);
841                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
842                     return false;
843             }
844         } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
845             switch (mScoAudioState) {
846                 case SCO_STATE_ACTIVE_INTERNAL:
847                     if (mBluetoothHeadset == null) {
848                         if (getBluetoothHeadset()) {
849                             mScoAudioState = SCO_STATE_DEACTIVATE_REQ;
850                         } else {
851                             Log.w(TAG, "requestScoState: getBluetoothHeadset failed during"
852                                     + " disconnection, mScoAudioMode=" + mScoAudioMode);
853                             mScoAudioState = SCO_STATE_INACTIVE;
854                             broadcastScoConnectionState(
855                                     AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
856                             return false;
857                         }
858                         break;
859                     }
860                     if (mBluetoothHeadsetDevice == null) {
861                         mScoAudioState = SCO_STATE_INACTIVE;
862                         broadcastScoConnectionState(
863                                 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
864                         break;
865                     }
866                     if (disconnectBluetoothScoAudioHelper(mBluetoothHeadset,
867                             mBluetoothHeadsetDevice, mScoAudioMode)) {
868                         mScoAudioState = SCO_STATE_DEACTIVATING;
869                     } else {
870                         mScoAudioState = SCO_STATE_INACTIVE;
871                         broadcastScoConnectionState(
872                                 AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
873                     }
874                     break;
875                 case SCO_STATE_ACTIVATE_REQ:
876                     mScoAudioState = SCO_STATE_INACTIVE;
877                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
878                     break;
879                 default:
880                     Log.w(TAG, "requestScoState: failed to disconnect in state "
881                             + mScoAudioState + ", scoAudioMode=" + scoAudioMode);
882                     broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
883                     return false;
884             }
885         }
886         return true;
887     }
888 
889     //-----------------------------------------------------
890     // Utilities
sendStickyBroadcastToAll(Intent intent)891     private void sendStickyBroadcastToAll(Intent intent) {
892         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
893         final long ident = Binder.clearCallingIdentity();
894         try {
895             mDeviceBroker.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
896         } finally {
897             Binder.restoreCallingIdentity(ident);
898         }
899     }
900 
disconnectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset, BluetoothDevice device, int scoAudioMode)901     private static boolean disconnectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset,
902             BluetoothDevice device, int scoAudioMode) {
903         switch (scoAudioMode) {
904             case SCO_MODE_VIRTUAL_CALL:
905                 return bluetoothHeadset.stopScoUsingVirtualVoiceCall();
906             case SCO_MODE_VR:
907                 return bluetoothHeadset.stopVoiceRecognition(device);
908             default:
909                 return false;
910         }
911     }
912 
connectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset, BluetoothDevice device, int scoAudioMode)913     private static boolean connectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset,
914             BluetoothDevice device, int scoAudioMode) {
915         switch (scoAudioMode) {
916             case SCO_MODE_VIRTUAL_CALL:
917                 return bluetoothHeadset.startScoUsingVirtualVoiceCall();
918             case SCO_MODE_VR:
919                 return bluetoothHeadset.startVoiceRecognition(device);
920             default:
921                 return false;
922         }
923     }
924 
checkScoAudioState()925     private void checkScoAudioState() {
926         if (mBluetoothHeadset != null
927                 && mBluetoothHeadsetDevice != null
928                 && mScoAudioState == SCO_STATE_INACTIVE
929                 && mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
930                 != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
931             mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL;
932         }
933     }
934 
getBluetoothHeadset()935     private boolean getBluetoothHeadset() {
936         boolean result = false;
937         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
938         if (adapter != null) {
939             result = adapter.getProfileProxy(mDeviceBroker.getContext(),
940                     mBluetoothProfileServiceListener, BluetoothProfile.HEADSET);
941         }
942         // If we could not get a bluetooth headset proxy, send a failure message
943         // without delay to reset the SCO audio state and clear SCO clients.
944         // If we could get a proxy, send a delayed failure message that will reset our state
945         // in case we don't receive onServiceConnected().
946         mDeviceBroker.handleFailureToConnectToBtHeadsetService(
947                 result ? AudioDeviceBroker.BT_HEADSET_CNCT_TIMEOUT_MS : 0);
948         return result;
949     }
950 
951     /**
952      * Returns the String equivalent of the btCodecType.
953      *
954      * This uses an "ENCODING_" prefix for consistency with Audio;
955      * we could alternately use the "SOURCE_CODEC_TYPE_" prefix from Bluetooth.
956      */
bluetoothCodecToEncodingString(int btCodecType)957     public static String bluetoothCodecToEncodingString(int btCodecType) {
958         switch (btCodecType) {
959             case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
960                 return "ENCODING_SBC";
961             case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
962                 return "ENCODING_AAC";
963             case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
964                 return "ENCODING_APTX";
965             case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
966                 return "ENCODING_APTX_HD";
967             case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
968                 return "ENCODING_LDAC";
969             case SOURCE_CODEC_TYPE_OPUS: // TODO update in U
970                 return "ENCODING_OPUS";
971             default:
972                 return "ENCODING_BT_CODEC_TYPE(" + btCodecType + ")";
973         }
974     }
975 
976     /**
977      * Returns the string equivalent for the btDeviceClass class.
978      */
btDeviceClassToString(int btDeviceClass)979     public static String btDeviceClassToString(int btDeviceClass) {
980         switch (btDeviceClass) {
981             case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
982                 return "AUDIO_VIDEO_UNCATEGORIZED";
983             case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
984                 return "AUDIO_VIDEO_WEARABLE_HEADSET";
985             case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
986                 return "AUDIO_VIDEO_HANDSFREE";
987             case 0x040C:
988                 return "AUDIO_VIDEO_RESERVED_0x040C"; // uncommon
989             case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
990                 return "AUDIO_VIDEO_MICROPHONE";
991             case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER:
992                 return "AUDIO_VIDEO_LOUDSPEAKER";
993             case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
994                 return "AUDIO_VIDEO_HEADPHONES";
995             case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO:
996                 return "AUDIO_VIDEO_PORTABLE_AUDIO";
997             case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
998                 return "AUDIO_VIDEO_CAR_AUDIO";
999             case BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX:
1000                 return "AUDIO_VIDEO_SET_TOP_BOX";
1001             case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO:
1002                 return "AUDIO_VIDEO_HIFI_AUDIO";
1003             case BluetoothClass.Device.AUDIO_VIDEO_VCR:
1004                 return "AUDIO_VIDEO_VCR";
1005             case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA:
1006                 return "AUDIO_VIDEO_VIDEO_CAMERA";
1007             case BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER:
1008                 return "AUDIO_VIDEO_CAMCORDER";
1009             case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR:
1010                 return "AUDIO_VIDEO_VIDEO_MONITOR";
1011             case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER:
1012                 return "AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER";
1013             case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING:
1014                 return "AUDIO_VIDEO_VIDEO_CONFERENCING";
1015             case 0x0444:
1016                 return "AUDIO_VIDEO_RESERVED_0x0444"; // uncommon
1017             case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY:
1018                 return "AUDIO_VIDEO_VIDEO_GAMING_TOY";
1019             default: // other device classes printed as a hex string.
1020                 return TextUtils.formatSimple("0x%04x", btDeviceClass);
1021         }
1022     }
1023 
1024     //------------------------------------------------------------
dump(PrintWriter pw, String prefix)1025     /*package*/ void dump(PrintWriter pw, String prefix) {
1026         pw.println("\n" + prefix + "mBluetoothHeadset: " + mBluetoothHeadset);
1027         pw.println(prefix + "mBluetoothHeadsetDevice: " + mBluetoothHeadsetDevice);
1028         if (mBluetoothHeadsetDevice != null) {
1029             final BluetoothClass bluetoothClass = mBluetoothHeadsetDevice.getBluetoothClass();
1030             if (bluetoothClass != null) {
1031                 pw.println(prefix + "mBluetoothHeadsetDevice.DeviceClass: "
1032                         + btDeviceClassToString(bluetoothClass.getDeviceClass()));
1033             }
1034         }
1035         pw.println(prefix + "mScoAudioState: " + scoAudioStateToString(mScoAudioState));
1036         pw.println(prefix + "mScoAudioMode: " + scoAudioModeToString(mScoAudioMode));
1037         pw.println("\n" + prefix + "mHearingAid: " + mHearingAid);
1038         pw.println("\n" + prefix + "mLeAudio: " + mLeAudio);
1039         pw.println(prefix + "mA2dp: " + mA2dp);
1040         pw.println(prefix + "mAvrcpAbsVolSupported: " + mAvrcpAbsVolSupported);
1041     }
1042 
1043 }
1044