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