| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.server.audio; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.bluetooth.BluetoothA2dp; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothClass; |
| import android.bluetooth.BluetoothCodecConfig; |
| import android.bluetooth.BluetoothCodecStatus; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadset; |
| import android.bluetooth.BluetoothHearingAid; |
| import android.bluetooth.BluetoothProfile; |
| import android.content.Intent; |
| import android.media.AudioManager; |
| import android.media.AudioSystem; |
| import android.os.Binder; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| import java.util.Objects; |
| |
| /** |
| * @hide |
| * Class to encapsulate all communication with Bluetooth services |
| */ |
| public class BtHelper { |
| |
| private static final String TAG = "AS.BtHelper"; |
| |
| private final @NonNull AudioDeviceBroker mDeviceBroker; |
| |
| BtHelper(@NonNull AudioDeviceBroker broker) { |
| mDeviceBroker = broker; |
| } |
| |
| // List of clients having issued a SCO start request |
| private final @NonNull ArrayList<ScoClient> mScoClients = new ArrayList<ScoClient>(); |
| |
| // BluetoothHeadset API to control SCO connection |
| private @Nullable BluetoothHeadset mBluetoothHeadset; |
| |
| // Bluetooth headset device |
| private @Nullable BluetoothDevice mBluetoothHeadsetDevice; |
| |
| private @Nullable BluetoothHearingAid mHearingAid; |
| |
| // Reference to BluetoothA2dp to query for AbsoluteVolume. |
| private @Nullable BluetoothA2dp mA2dp; |
| |
| // If absolute volume is supported in AVRCP device |
| private boolean mAvrcpAbsVolSupported = false; |
| |
| // Current connection state indicated by bluetooth headset |
| private int mScoConnectionState; |
| |
| // Indicate if SCO audio connection is currently active and if the initiator is |
| // audio service (internal) or bluetooth headset (external) |
| private int mScoAudioState; |
| |
| // Indicates the mode used for SCO audio connection. The mode is virtual call if the request |
| // originated from an app targeting an API version before JB MR2 and raw audio after that. |
| private int mScoAudioMode; |
| |
| // SCO audio state is not active |
| private static final int SCO_STATE_INACTIVE = 0; |
| // SCO audio activation request waiting for headset service to connect |
| private static final int SCO_STATE_ACTIVATE_REQ = 1; |
| // SCO audio state is active due to an action in BT handsfree (either voice recognition or |
| // in call audio) |
| private static final int SCO_STATE_ACTIVE_EXTERNAL = 2; |
| // SCO audio state is active or starting due to a request from AudioManager API |
| private static final int SCO_STATE_ACTIVE_INTERNAL = 3; |
| // SCO audio deactivation request waiting for headset service to connect |
| private static final int SCO_STATE_DEACTIVATE_REQ = 4; |
| // SCO audio deactivation in progress, waiting for Bluetooth audio intent |
| private static final int SCO_STATE_DEACTIVATING = 5; |
| |
| // SCO audio mode is undefined |
| /*package*/ static final int SCO_MODE_UNDEFINED = -1; |
| // SCO audio mode is virtual voice call (BluetoothHeadset.startScoUsingVirtualVoiceCall()) |
| /*package*/ static final int SCO_MODE_VIRTUAL_CALL = 0; |
| // SCO audio mode is raw audio (BluetoothHeadset.connectAudio()) |
| private static final int SCO_MODE_RAW = 1; |
| // SCO audio mode is Voice Recognition (BluetoothHeadset.startVoiceRecognition()) |
| private static final int SCO_MODE_VR = 2; |
| // max valid SCO audio mode values |
| private static final int SCO_MODE_MAX = 2; |
| |
| private static final int BT_HEARING_AID_GAIN_MIN = -128; |
| |
| //---------------------------------------------------------------------- |
| /*package*/ static class BluetoothA2dpDeviceInfo { |
| private final @NonNull BluetoothDevice mBtDevice; |
| private final int mVolume; |
| private final int mCodec; |
| |
| BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice) { |
| this(btDevice, -1, AudioSystem.AUDIO_FORMAT_DEFAULT); |
| } |
| |
| BluetoothA2dpDeviceInfo(@NonNull BluetoothDevice btDevice, int volume, int codec) { |
| mBtDevice = btDevice; |
| mVolume = volume; |
| mCodec = codec; |
| } |
| |
| public @NonNull BluetoothDevice getBtDevice() { |
| return mBtDevice; |
| } |
| |
| public int getVolume() { |
| return mVolume; |
| } |
| |
| public int getCodec() { |
| return mCodec; |
| } |
| |
| // redefine equality op so we can match messages intended for this device |
| @Override |
| public boolean equals(Object o) { |
| return mBtDevice.equals(o); |
| } |
| } |
| |
| // A2DP device events |
| /*package*/ static final int EVENT_DEVICE_CONFIG_CHANGE = 0; |
| /*package*/ static final int EVENT_ACTIVE_DEVICE_CHANGE = 1; |
| |
| /*package*/ static String a2dpDeviceEventToString(int event) { |
| switch (event) { |
| case EVENT_DEVICE_CONFIG_CHANGE: return "DEVICE_CONFIG_CHANGE"; |
| case EVENT_ACTIVE_DEVICE_CHANGE: return "ACTIVE_DEVICE_CHANGE"; |
| default: |
| return new String("invalid event:" + event); |
| } |
| } |
| |
| /*package*/ @NonNull static String getName(@NonNull BluetoothDevice device) { |
| final String deviceName = device.getName(); |
| if (deviceName == null) { |
| return ""; |
| } |
| return deviceName; |
| } |
| |
| //---------------------------------------------------------------------- |
| // Interface for AudioDeviceBroker |
| |
| /*package*/ synchronized void onSystemReady() { |
| mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR; |
| resetBluetoothSco(); |
| getBluetoothHeadset(); |
| |
| //FIXME: this is to maintain compatibility with deprecated intent |
| // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate. |
| Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED); |
| newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| sendStickyBroadcastToAll(newIntent); |
| |
| BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| if (adapter != null) { |
| adapter.getProfileProxy(mDeviceBroker.getContext(), |
| mBluetoothProfileServiceListener, BluetoothProfile.A2DP); |
| adapter.getProfileProxy(mDeviceBroker.getContext(), |
| mBluetoothProfileServiceListener, BluetoothProfile.HEARING_AID); |
| } |
| } |
| |
| /*package*/ synchronized void onAudioServerDiedRestoreA2dp() { |
| final int forMed = mDeviceBroker.getBluetoothA2dpEnabled() |
| ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP; |
| mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()"); |
| } |
| |
| /*package*/ synchronized boolean isAvrcpAbsoluteVolumeSupported() { |
| return (mA2dp != null && mAvrcpAbsVolSupported); |
| } |
| |
| /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) { |
| mAvrcpAbsVolSupported = supported; |
| Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported); |
| } |
| |
| /*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) { |
| if (mA2dp == null) { |
| if (AudioService.DEBUG_VOL) { |
| AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent( |
| "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp").printLog(TAG)); |
| return; |
| } |
| } |
| if (!mAvrcpAbsVolSupported) { |
| AudioService.sVolumeLogger.log(new AudioEventLogger.StringEvent( |
| "setAvrcpAbsoluteVolumeIndex: abs vol not supported ").printLog(TAG)); |
| return; |
| } |
| if (AudioService.DEBUG_VOL) { |
| Log.i(TAG, "setAvrcpAbsoluteVolumeIndex index=" + index); |
| } |
| AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent( |
| AudioServiceEvents.VolumeEvent.VOL_SET_AVRCP_VOL, index)); |
| mA2dp.setAvrcpAbsoluteVolume(index); |
| } |
| |
| /*package*/ synchronized int getA2dpCodec(@NonNull BluetoothDevice device) { |
| if (mA2dp == null) { |
| return AudioSystem.AUDIO_FORMAT_DEFAULT; |
| } |
| final BluetoothCodecStatus btCodecStatus = mA2dp.getCodecStatus(device); |
| if (btCodecStatus == null) { |
| return AudioSystem.AUDIO_FORMAT_DEFAULT; |
| } |
| final BluetoothCodecConfig btCodecConfig = btCodecStatus.getCodecConfig(); |
| if (btCodecConfig == null) { |
| return AudioSystem.AUDIO_FORMAT_DEFAULT; |
| } |
| return mapBluetoothCodecToAudioFormat(btCodecConfig.getCodecType()); |
| } |
| |
| /*package*/ synchronized void receiveBtEvent(Intent intent) { |
| final String action = intent.getAction(); |
| if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) { |
| BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| setBtScoActiveDevice(btDevice); |
| } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { |
| boolean broadcast = false; |
| int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR; |
| int btState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); |
| // broadcast intent if the connection was initated by AudioService |
| if (!mScoClients.isEmpty() |
| && (mScoAudioState == SCO_STATE_ACTIVE_INTERNAL |
| || mScoAudioState == SCO_STATE_ACTIVATE_REQ |
| || mScoAudioState == SCO_STATE_DEACTIVATE_REQ |
| || mScoAudioState == SCO_STATE_DEACTIVATING)) { |
| broadcast = true; |
| } |
| switch (btState) { |
| case BluetoothHeadset.STATE_AUDIO_CONNECTED: |
| scoAudioState = AudioManager.SCO_AUDIO_STATE_CONNECTED; |
| if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL |
| && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { |
| mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; |
| } |
| mDeviceBroker.setBluetoothScoOn(true, "BtHelper.receiveBtEvent"); |
| break; |
| case BluetoothHeadset.STATE_AUDIO_DISCONNECTED: |
| mDeviceBroker.setBluetoothScoOn(false, "BtHelper.receiveBtEvent"); |
| scoAudioState = AudioManager.SCO_AUDIO_STATE_DISCONNECTED; |
| // startBluetoothSco called after stopBluetoothSco |
| if (mScoAudioState == SCO_STATE_ACTIVATE_REQ) { |
| if (mBluetoothHeadset != null && mBluetoothHeadsetDevice != null |
| && connectBluetoothScoAudioHelper(mBluetoothHeadset, |
| mBluetoothHeadsetDevice, mScoAudioMode)) { |
| mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; |
| broadcast = false; |
| break; |
| } |
| } |
| // Tear down SCO if disconnected from external |
| clearAllScoClients(0, mScoAudioState == SCO_STATE_ACTIVE_INTERNAL); |
| mScoAudioState = SCO_STATE_INACTIVE; |
| break; |
| case BluetoothHeadset.STATE_AUDIO_CONNECTING: |
| if (mScoAudioState != SCO_STATE_ACTIVE_INTERNAL |
| && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { |
| mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; |
| } |
| break; |
| default: |
| // do not broadcast CONNECTING or invalid state |
| broadcast = false; |
| break; |
| } |
| if (broadcast) { |
| broadcastScoConnectionState(scoAudioState); |
| //FIXME: this is to maintain compatibility with deprecated intent |
| // AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED. Remove when appropriate. |
| Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED); |
| newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, scoAudioState); |
| sendStickyBroadcastToAll(newIntent); |
| } |
| } |
| } |
| |
| /** |
| * |
| * @return false if SCO isn't connected |
| */ |
| /*package*/ synchronized boolean isBluetoothScoOn() { |
| if ((mBluetoothHeadset != null) |
| && (mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice) |
| != BluetoothHeadset.STATE_AUDIO_CONNECTED)) { |
| Log.w(TAG, "isBluetoothScoOn(true) returning false because " |
| + mBluetoothHeadsetDevice + " is not in audio connected mode"); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Disconnect all SCO connections started by {@link AudioManager} except those started by |
| * {@param exceptPid} |
| * |
| * @param exceptPid pid whose SCO connections through {@link AudioManager} should be kept |
| */ |
| /*package*/ synchronized void disconnectBluetoothSco(int exceptPid) { |
| checkScoAudioState(); |
| if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) { |
| return; |
| } |
| clearAllScoClients(exceptPid, true); |
| } |
| |
| /*package*/ synchronized void startBluetoothScoForClient(IBinder cb, int scoAudioMode, |
| @NonNull String eventSource) { |
| ScoClient client = getScoClient(cb, true); |
| // The calling identity must be cleared before calling ScoClient.incCount(). |
| // inCount() calls requestScoState() which in turn can call BluetoothHeadset APIs |
| // and this must be done on behalf of system server to make sure permissions are granted. |
| // The caller identity must be cleared after getScoClient() because it is needed if a new |
| // client is created. |
| final long ident = Binder.clearCallingIdentity(); |
| try { |
| eventSource += " client count before=" + client.getCount(); |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource)); |
| client.incCount(scoAudioMode); |
| } catch (NullPointerException e) { |
| Log.e(TAG, "Null ScoClient", e); |
| } |
| Binder.restoreCallingIdentity(ident); |
| } |
| |
| /*package*/ synchronized void stopBluetoothScoForClient(IBinder cb, |
| @NonNull String eventSource) { |
| ScoClient client = getScoClient(cb, false); |
| // The calling identity must be cleared before calling ScoClient.decCount(). |
| // decCount() calls requestScoState() which in turn can call BluetoothHeadset APIs |
| // and this must be done on behalf of system server to make sure permissions are granted. |
| final long ident = Binder.clearCallingIdentity(); |
| if (client != null) { |
| eventSource += " client count before=" + client.getCount(); |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent(eventSource)); |
| client.decCount(); |
| } |
| Binder.restoreCallingIdentity(ident); |
| } |
| |
| |
| /*package*/ synchronized void setHearingAidVolume(int index, int streamType) { |
| if (mHearingAid == null) { |
| if (AudioService.DEBUG_VOL) { |
| Log.i(TAG, "setHearingAidVolume: null mHearingAid"); |
| } |
| return; |
| } |
| //hearing aid expect volume value in range -128dB to 0dB |
| int gainDB = (int) AudioSystem.getStreamVolumeDB(streamType, index / 10, |
| AudioSystem.DEVICE_OUT_HEARING_AID); |
| if (gainDB < BT_HEARING_AID_GAIN_MIN) { |
| gainDB = BT_HEARING_AID_GAIN_MIN; |
| } |
| if (AudioService.DEBUG_VOL) { |
| Log.i(TAG, "setHearingAidVolume: calling mHearingAid.setVolume idx=" |
| + index + " gain=" + gainDB); |
| } |
| AudioService.sVolumeLogger.log(new AudioServiceEvents.VolumeEvent( |
| AudioServiceEvents.VolumeEvent.VOL_SET_HEARING_AID_VOL, index, gainDB)); |
| mHearingAid.setVolume(gainDB); |
| } |
| |
| /*package*/ synchronized void onBroadcastScoConnectionState(int state) { |
| if (state == mScoConnectionState) { |
| return; |
| } |
| Intent newIntent = new Intent(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); |
| newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, state); |
| newIntent.putExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, |
| mScoConnectionState); |
| sendStickyBroadcastToAll(newIntent); |
| mScoConnectionState = state; |
| } |
| |
| /*package*/ synchronized void disconnectAllBluetoothProfiles() { |
| mDeviceBroker.postDisconnectA2dp(); |
| mDeviceBroker.postDisconnectA2dpSink(); |
| mDeviceBroker.postDisconnectHeadset(); |
| mDeviceBroker.postDisconnectHearingAid(); |
| } |
| |
| /*package*/ synchronized void resetBluetoothSco() { |
| clearAllScoClients(0, false); |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| AudioSystem.setParameters("A2dpSuspended=false"); |
| mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco"); |
| } |
| |
| /*package*/ synchronized void disconnectHeadset() { |
| setBtScoActiveDevice(null); |
| mBluetoothHeadset = null; |
| } |
| |
| /*package*/ synchronized void onA2dpProfileConnected(BluetoothA2dp a2dp) { |
| mA2dp = a2dp; |
| final List<BluetoothDevice> deviceList = mA2dp.getConnectedDevices(); |
| if (deviceList.isEmpty()) { |
| return; |
| } |
| final BluetoothDevice btDevice = deviceList.get(0); |
| // the device is guaranteed CONNECTED |
| mDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(btDevice, |
| BluetoothA2dp.STATE_CONNECTED, BluetoothProfile.A2DP_SINK, true, -1); |
| } |
| |
| /*package*/ synchronized void onA2dpSinkProfileConnected(BluetoothProfile profile) { |
| final List<BluetoothDevice> deviceList = profile.getConnectedDevices(); |
| if (deviceList.isEmpty()) { |
| return; |
| } |
| final BluetoothDevice btDevice = deviceList.get(0); |
| final @BluetoothProfile.BtProfileState int state = |
| profile.getConnectionState(btDevice); |
| mDeviceBroker.postSetA2dpSourceConnectionState( |
| state, new BluetoothA2dpDeviceInfo(btDevice)); |
| } |
| |
| /*package*/ synchronized void onHearingAidProfileConnected(BluetoothHearingAid hearingAid) { |
| mHearingAid = hearingAid; |
| final List<BluetoothDevice> deviceList = mHearingAid.getConnectedDevices(); |
| if (deviceList.isEmpty()) { |
| return; |
| } |
| final BluetoothDevice btDevice = deviceList.get(0); |
| final @BluetoothProfile.BtProfileState int state = |
| mHearingAid.getConnectionState(btDevice); |
| mDeviceBroker.postBluetoothHearingAidDeviceConnectionState( |
| btDevice, state, |
| /*suppressNoisyIntent*/ false, |
| /*musicDevice*/ android.media.AudioSystem.DEVICE_NONE, |
| /*eventSource*/ "mBluetoothProfileServiceListener"); |
| } |
| |
| /*package*/ synchronized void onHeadsetProfileConnected(BluetoothHeadset headset) { |
| // Discard timeout message |
| mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService(); |
| mBluetoothHeadset = headset; |
| setBtScoActiveDevice(mBluetoothHeadset.getActiveDevice()); |
| // Refresh SCO audio state |
| checkScoAudioState(); |
| if (mScoAudioState != SCO_STATE_ACTIVATE_REQ |
| && mScoAudioState != SCO_STATE_DEACTIVATE_REQ) { |
| return; |
| } |
| boolean status = false; |
| if (mBluetoothHeadsetDevice != null) { |
| switch (mScoAudioState) { |
| case SCO_STATE_ACTIVATE_REQ: |
| status = connectBluetoothScoAudioHelper( |
| mBluetoothHeadset, |
| mBluetoothHeadsetDevice, mScoAudioMode); |
| if (status) { |
| mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; |
| } |
| break; |
| case SCO_STATE_DEACTIVATE_REQ: |
| status = disconnectBluetoothScoAudioHelper( |
| mBluetoothHeadset, |
| mBluetoothHeadsetDevice, mScoAudioMode); |
| if (status) { |
| mScoAudioState = SCO_STATE_DEACTIVATING; |
| } |
| break; |
| } |
| } |
| if (!status) { |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| } |
| } |
| |
| //---------------------------------------------------------------------- |
| private void broadcastScoConnectionState(int state) { |
| mDeviceBroker.postBroadcastScoConnectionState(state); |
| } |
| |
| private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) { |
| if (btDevice == null) { |
| return true; |
| } |
| String address = btDevice.getAddress(); |
| BluetoothClass btClass = btDevice.getBluetoothClass(); |
| int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET; |
| int[] outDeviceTypes = { |
| AudioSystem.DEVICE_OUT_BLUETOOTH_SCO, |
| AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET, |
| AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT |
| }; |
| if (btClass != null) { |
| switch (btClass.getDeviceClass()) { |
| case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: |
| case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: |
| outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET }; |
| break; |
| case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: |
| outDeviceTypes = new int[] { AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT }; |
| break; |
| } |
| } |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| address = ""; |
| } |
| String btDeviceName = getName(btDevice); |
| boolean result = false; |
| if (isActive) { |
| result |= mDeviceBroker.handleDeviceConnection( |
| isActive, outDeviceTypes[0], address, btDeviceName); |
| } else { |
| for (int outDeviceType : outDeviceTypes) { |
| result |= mDeviceBroker.handleDeviceConnection( |
| isActive, outDeviceType, address, btDeviceName); |
| } |
| } |
| // handleDeviceConnection() && result to make sure the method get executed |
| result = mDeviceBroker.handleDeviceConnection( |
| isActive, inDevice, address, btDeviceName) && result; |
| return result; |
| } |
| |
| @GuardedBy("BtHelper.this") |
| private void setBtScoActiveDevice(BluetoothDevice btDevice) { |
| Log.i(TAG, "setBtScoActiveDevice: " + mBluetoothHeadsetDevice + " -> " + btDevice); |
| final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice; |
| if (Objects.equals(btDevice, previousActiveDevice)) { |
| return; |
| } |
| if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) { |
| Log.w(TAG, "setBtScoActiveDevice() failed to remove previous device " |
| + previousActiveDevice); |
| } |
| if (!handleBtScoActiveDeviceChange(btDevice, true)) { |
| Log.e(TAG, "setBtScoActiveDevice() failed to add new device " + btDevice); |
| // set mBluetoothHeadsetDevice to null when failing to add new device |
| btDevice = null; |
| } |
| mBluetoothHeadsetDevice = btDevice; |
| if (mBluetoothHeadsetDevice == null) { |
| resetBluetoothSco(); |
| } |
| } |
| |
| // NOTE this listener is NOT called from AudioDeviceBroker event thread, only call async |
| // methods inside listener. |
| private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = |
| new BluetoothProfile.ServiceListener() { |
| public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| switch(profile) { |
| case BluetoothProfile.A2DP: |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "BT profile service: connecting A2DP profile")); |
| mDeviceBroker.postBtA2dpProfileConnected((BluetoothA2dp) proxy); |
| break; |
| |
| case BluetoothProfile.A2DP_SINK: |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "BT profile service: connecting A2DP_SINK profile")); |
| mDeviceBroker.postBtA2dpSinkProfileConnected(proxy); |
| break; |
| |
| case BluetoothProfile.HEADSET: |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "BT profile service: connecting HEADSET profile")); |
| mDeviceBroker.postBtHeasetProfileConnected((BluetoothHeadset) proxy); |
| break; |
| |
| case BluetoothProfile.HEARING_AID: |
| AudioService.sDeviceLogger.log(new AudioEventLogger.StringEvent( |
| "BT profile service: connecting HEARING_AID profile")); |
| mDeviceBroker.postBtHearingAidProfileConnected( |
| (BluetoothHearingAid) proxy); |
| break; |
| default: |
| break; |
| } |
| } |
| public void onServiceDisconnected(int profile) { |
| |
| switch (profile) { |
| case BluetoothProfile.A2DP: |
| mDeviceBroker.postDisconnectA2dp(); |
| break; |
| |
| case BluetoothProfile.A2DP_SINK: |
| mDeviceBroker.postDisconnectA2dpSink(); |
| break; |
| |
| case BluetoothProfile.HEADSET: |
| mDeviceBroker.postDisconnectHeadset(); |
| break; |
| |
| case BluetoothProfile.HEARING_AID: |
| mDeviceBroker.postDisconnectHearingAid(); |
| break; |
| |
| default: |
| break; |
| } |
| } |
| }; |
| |
| //---------------------------------------------------------------------- |
| /*package*/ synchronized void scoClientDied(Object obj) { |
| final ScoClient client = (ScoClient) obj; |
| Log.w(TAG, "SCO client died"); |
| int index = mScoClients.indexOf(client); |
| if (index < 0) { |
| Log.w(TAG, "unregistered SCO client died"); |
| } else { |
| client.clearCount(true); |
| mScoClients.remove(client); |
| } |
| } |
| |
| private class ScoClient implements IBinder.DeathRecipient { |
| private IBinder mCb; // To be notified of client's death |
| private int mCreatorPid; |
| private int mStartcount; // number of SCO connections started by this client |
| |
| ScoClient(IBinder cb) { |
| mCb = cb; |
| mCreatorPid = Binder.getCallingPid(); |
| mStartcount = 0; |
| } |
| |
| @Override |
| public void binderDied() { |
| // process this from DeviceBroker's message queue to take the right locks since |
| // this event can impact SCO mode and requires querying audio mode stack |
| mDeviceBroker.postScoClientDied(this); |
| } |
| |
| @GuardedBy("BtHelper.this") |
| void incCount(int scoAudioMode) { |
| if (!requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode)) { |
| Log.e(TAG, "Request sco connected with scoAudioMode(" |
| + scoAudioMode + ") failed"); |
| return; |
| } |
| if (mStartcount == 0) { |
| try { |
| mCb.linkToDeath(this, 0); |
| } catch (RemoteException e) { |
| // client has already died! |
| Log.w(TAG, "ScoClient incCount() could not link to " |
| + mCb + " binder death"); |
| } |
| } |
| mStartcount++; |
| } |
| |
| @GuardedBy("BtHelper.this") |
| void decCount() { |
| if (mStartcount == 0) { |
| Log.w(TAG, "ScoClient.decCount() already 0"); |
| } else { |
| mStartcount--; |
| if (mStartcount == 0) { |
| try { |
| mCb.unlinkToDeath(this, 0); |
| } catch (NoSuchElementException e) { |
| Log.w(TAG, "decCount() going to 0 but not registered to binder"); |
| } |
| } |
| if (!requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, 0)) { |
| Log.w(TAG, "Request sco disconnected with scoAudioMode(0) failed"); |
| } |
| } |
| } |
| |
| @GuardedBy("BtHelper.this") |
| void clearCount(boolean stopSco) { |
| if (mStartcount != 0) { |
| try { |
| mCb.unlinkToDeath(this, 0); |
| } catch (NoSuchElementException e) { |
| Log.w(TAG, "clearCount() mStartcount: " |
| + mStartcount + " != 0 but not registered to binder"); |
| } |
| } |
| mStartcount = 0; |
| if (stopSco) { |
| requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, 0); |
| } |
| } |
| |
| int getCount() { |
| return mStartcount; |
| } |
| |
| IBinder getBinder() { |
| return mCb; |
| } |
| |
| int getPid() { |
| return mCreatorPid; |
| } |
| |
| private int totalCount() { |
| int count = 0; |
| for (ScoClient mScoClient : mScoClients) { |
| count += mScoClient.getCount(); |
| } |
| return count; |
| } |
| |
| @GuardedBy("BtHelper.this") |
| private boolean requestScoState(int state, int scoAudioMode) { |
| checkScoAudioState(); |
| int clientCount = totalCount(); |
| if (clientCount != 0) { |
| Log.i(TAG, "requestScoState: state=" + state + ", scoAudioMode=" + scoAudioMode |
| + ", clientCount=" + clientCount); |
| return true; |
| } |
| if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { |
| // Make sure that the state transitions to CONNECTING even if we cannot initiate |
| // the connection. |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTING); |
| // Accept SCO audio activation only in NORMAL audio mode or if the mode is |
| // currently controlled by the same client process. |
| final int modeOwnerPid = mDeviceBroker.getModeOwnerPid(); |
| if (modeOwnerPid != 0 && (modeOwnerPid != mCreatorPid)) { |
| Log.w(TAG, "requestScoState: audio mode is not NORMAL and modeOwnerPid " |
| + modeOwnerPid + " != creatorPid " + mCreatorPid); |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| switch (mScoAudioState) { |
| case SCO_STATE_INACTIVE: |
| mScoAudioMode = scoAudioMode; |
| if (scoAudioMode == SCO_MODE_UNDEFINED) { |
| mScoAudioMode = SCO_MODE_VIRTUAL_CALL; |
| if (mBluetoothHeadsetDevice != null) { |
| mScoAudioMode = Settings.Global.getInt( |
| mDeviceBroker.getContentResolver(), |
| "bluetooth_sco_channel_" |
| + mBluetoothHeadsetDevice.getAddress(), |
| SCO_MODE_VIRTUAL_CALL); |
| if (mScoAudioMode > SCO_MODE_MAX || mScoAudioMode < 0) { |
| mScoAudioMode = SCO_MODE_VIRTUAL_CALL; |
| } |
| } |
| } |
| if (mBluetoothHeadset == null) { |
| if (getBluetoothHeadset()) { |
| mScoAudioState = SCO_STATE_ACTIVATE_REQ; |
| } else { |
| Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" |
| + " connection, mScoAudioMode=" + mScoAudioMode); |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| break; |
| } |
| if (mBluetoothHeadsetDevice == null) { |
| Log.w(TAG, "requestScoState: no active device while connecting," |
| + " mScoAudioMode=" + mScoAudioMode); |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| if (connectBluetoothScoAudioHelper(mBluetoothHeadset, |
| mBluetoothHeadsetDevice, mScoAudioMode)) { |
| mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; |
| } else { |
| Log.w(TAG, "requestScoState: connect to " + mBluetoothHeadsetDevice |
| + " failed, mScoAudioMode=" + mScoAudioMode); |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| break; |
| case SCO_STATE_DEACTIVATING: |
| mScoAudioState = SCO_STATE_ACTIVATE_REQ; |
| break; |
| case SCO_STATE_DEACTIVATE_REQ: |
| mScoAudioState = SCO_STATE_ACTIVE_INTERNAL; |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_CONNECTED); |
| break; |
| default: |
| Log.w(TAG, "requestScoState: failed to connect in state " |
| + mScoAudioState + ", scoAudioMode=" + scoAudioMode); |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| |
| } |
| } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { |
| switch (mScoAudioState) { |
| case SCO_STATE_ACTIVE_INTERNAL: |
| if (mBluetoothHeadset == null) { |
| if (getBluetoothHeadset()) { |
| mScoAudioState = SCO_STATE_DEACTIVATE_REQ; |
| } else { |
| Log.w(TAG, "requestScoState: getBluetoothHeadset failed during" |
| + " disconnection, mScoAudioMode=" + mScoAudioMode); |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| break; |
| } |
| if (mBluetoothHeadsetDevice == null) { |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| break; |
| } |
| if (disconnectBluetoothScoAudioHelper(mBluetoothHeadset, |
| mBluetoothHeadsetDevice, mScoAudioMode)) { |
| mScoAudioState = SCO_STATE_DEACTIVATING; |
| } else { |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState( |
| AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| } |
| break; |
| case SCO_STATE_ACTIVATE_REQ: |
| mScoAudioState = SCO_STATE_INACTIVE; |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| break; |
| default: |
| Log.w(TAG, "requestScoState: failed to disconnect in state " |
| + mScoAudioState + ", scoAudioMode=" + scoAudioMode); |
| broadcastScoConnectionState(AudioManager.SCO_AUDIO_STATE_DISCONNECTED); |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| |
| //----------------------------------------------------- |
| // Utilities |
| private void sendStickyBroadcastToAll(Intent intent) { |
| intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); |
| final long ident = Binder.clearCallingIdentity(); |
| try { |
| mDeviceBroker.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL); |
| } finally { |
| Binder.restoreCallingIdentity(ident); |
| } |
| } |
| |
| private static boolean disconnectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset, |
| BluetoothDevice device, int scoAudioMode) { |
| switch (scoAudioMode) { |
| case SCO_MODE_RAW: |
| return bluetoothHeadset.disconnectAudio(); |
| case SCO_MODE_VIRTUAL_CALL: |
| return bluetoothHeadset.stopScoUsingVirtualVoiceCall(); |
| case SCO_MODE_VR: |
| return bluetoothHeadset.stopVoiceRecognition(device); |
| default: |
| return false; |
| } |
| } |
| |
| private static boolean connectBluetoothScoAudioHelper(BluetoothHeadset bluetoothHeadset, |
| BluetoothDevice device, int scoAudioMode) { |
| switch (scoAudioMode) { |
| case SCO_MODE_RAW: |
| return bluetoothHeadset.connectAudio(); |
| case SCO_MODE_VIRTUAL_CALL: |
| return bluetoothHeadset.startScoUsingVirtualVoiceCall(); |
| case SCO_MODE_VR: |
| return bluetoothHeadset.startVoiceRecognition(device); |
| default: |
| return false; |
| } |
| } |
| |
| private void checkScoAudioState() { |
| if (mBluetoothHeadset != null |
| && mBluetoothHeadsetDevice != null |
| && mScoAudioState == SCO_STATE_INACTIVE |
| && mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice) |
| != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { |
| mScoAudioState = SCO_STATE_ACTIVE_EXTERNAL; |
| } |
| } |
| |
| |
| private ScoClient getScoClient(IBinder cb, boolean create) { |
| for (ScoClient existingClient : mScoClients) { |
| if (existingClient.getBinder() == cb) { |
| return existingClient; |
| } |
| } |
| if (create) { |
| ScoClient newClient = new ScoClient(cb); |
| mScoClients.add(newClient); |
| return newClient; |
| } |
| return null; |
| } |
| |
| @GuardedBy("BtHelper.this") |
| private void clearAllScoClients(int exceptPid, boolean stopSco) { |
| ScoClient savedClient = null; |
| for (ScoClient cl : mScoClients) { |
| if (cl.getPid() != exceptPid) { |
| cl.clearCount(stopSco); |
| } else { |
| savedClient = cl; |
| } |
| } |
| mScoClients.clear(); |
| if (savedClient != null) { |
| mScoClients.add(savedClient); |
| } |
| } |
| |
| private boolean getBluetoothHeadset() { |
| boolean result = false; |
| BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| if (adapter != null) { |
| result = adapter.getProfileProxy(mDeviceBroker.getContext(), |
| mBluetoothProfileServiceListener, BluetoothProfile.HEADSET); |
| } |
| // If we could not get a bluetooth headset proxy, send a failure message |
| // without delay to reset the SCO audio state and clear SCO clients. |
| // If we could get a proxy, send a delayed failure message that will reset our state |
| // in case we don't receive onServiceConnected(). |
| mDeviceBroker.handleFailureToConnectToBtHeadsetService( |
| result ? AudioDeviceBroker.BT_HEADSET_CNCT_TIMEOUT_MS : 0); |
| return result; |
| } |
| |
| private int mapBluetoothCodecToAudioFormat(int btCodecType) { |
| switch (btCodecType) { |
| case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC: |
| return AudioSystem.AUDIO_FORMAT_SBC; |
| case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC: |
| return AudioSystem.AUDIO_FORMAT_AAC; |
| case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX: |
| return AudioSystem.AUDIO_FORMAT_APTX; |
| case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD: |
| return AudioSystem.AUDIO_FORMAT_APTX_HD; |
| case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC: |
| return AudioSystem.AUDIO_FORMAT_LDAC; |
| default: |
| return AudioSystem.AUDIO_FORMAT_DEFAULT; |
| } |
| } |
| } |