blob: e9189974e988b826d66e38f0c45eafc61322cd15 [file] [log] [blame]
/*
* 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 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;
}
}
//----------------------------------------------------------------------
// 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;
}
/*package*/ synchronized void setAvrcpAbsoluteVolumeIndex(int index) {
if (mA2dp == null) {
if (AudioService.DEBUG_VOL) {
Log.d(TAG, "setAvrcpAbsoluteVolumeIndex: bailing due to null mA2dp");
return;
}
}
if (!mAvrcpAbsVolSupported) {
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;
}
//----------------------------------------------------------------------
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 = btDevice.getName();
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;
}
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();
}
}
private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
new BluetoothProfile.ServiceListener() {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
final BluetoothDevice btDevice;
List<BluetoothDevice> deviceList;
switch(profile) {
case BluetoothProfile.A2DP:
synchronized (BtHelper.this) {
mA2dp = (BluetoothA2dp) proxy;
deviceList = mA2dp.getConnectedDevices();
if (deviceList.size() > 0) {
btDevice = deviceList.get(0);
if (btDevice == null) {
Log.e(TAG, "Invalid null device in BT profile listener");
return;
}
final @BluetoothProfile.BtProfileState int state =
mA2dp.getConnectionState(btDevice);
mDeviceBroker.handleSetA2dpSinkConnectionState(
state, new BluetoothA2dpDeviceInfo(btDevice));
}
}
break;
case BluetoothProfile.A2DP_SINK:
deviceList = proxy.getConnectedDevices();
if (deviceList.size() > 0) {
btDevice = deviceList.get(0);
final @BluetoothProfile.BtProfileState int state =
proxy.getConnectionState(btDevice);
mDeviceBroker.postSetA2dpSourceConnectionState(
state, new BluetoothA2dpDeviceInfo(btDevice));
}
break;
case BluetoothProfile.HEADSET:
synchronized (BtHelper.this) {
// Discard timeout message
mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
mBluetoothHeadset = (BluetoothHeadset) proxy;
setBtScoActiveDevice(mBluetoothHeadset.getActiveDevice());
// Refresh SCO audio state
checkScoAudioState();
// Continue pending action if any
if (mScoAudioState == SCO_STATE_ACTIVATE_REQ
|| mScoAudioState == SCO_STATE_DEACTIVATE_REQ) {
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);
}
}
}
break;
case BluetoothProfile.HEARING_AID:
synchronized (BtHelper.this) {
mHearingAid = (BluetoothHearingAid) proxy;
deviceList = mHearingAid.getConnectedDevices();
if (deviceList.size() > 0) {
btDevice = deviceList.get(0);
final @BluetoothProfile.BtProfileState int state =
mHearingAid.getConnectionState(btDevice);
mDeviceBroker.setBluetoothHearingAidDeviceConnectionState(
btDevice, state,
/*suppressNoisyIntent*/ false,
/*musicDevice*/ android.media.AudioSystem.DEVICE_NONE,
/*eventSource*/ "mBluetoothProfileServiceListener");
}
}
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;
}
}
};
//----------------------------------------------------------------------
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() {
// this is the only place the implementation of ScoClient needs to be synchronized
// on the instance, as all other methods are directly or indirectly called from
// package-private methods, which are synchronized
synchronized (BtHelper.this) {
Log.w(TAG, "SCO client died");
int index = mScoClients.indexOf(this);
if (index < 0) {
Log.w(TAG, "unregistered SCO client died");
} else {
clearCount(true);
mScoClients.remove(this);
}
}
}
void incCount(int scoAudioMode) {
requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode);
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++;
}
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");
}
}
requestScoState(BluetoothHeadset.STATE_AUDIO_DISCONNECTED, 0);
}
}
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;
}
private void requestScoState(int state, int scoAudioMode) {
checkScoAudioState();
int clientCount = totalCount();
if (clientCount != 0) {
Log.i(TAG, "requestScoState: state=" + state + ", scoAudioMode=" + scoAudioMode
+ ", clientCount=" + clientCount);
return;
}
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.
// TODO do not sync that way, see b/123769055
synchronized (mDeviceBroker.mSetModeLock) {
int modeOwnerPid = mDeviceBroker.getSetModeDeathHandlers().isEmpty()
? 0 : mDeviceBroker.getSetModeDeathHandlers().get(0).getPid();
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;
}
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);
}
break;
}
if (mBluetoothHeadsetDevice == null) {
Log.w(TAG, "requestScoState: no active device while connecting,"
+ " mScoAudioMode=" + mScoAudioMode);
broadcastScoConnectionState(
AudioManager.SCO_AUDIO_STATE_DISCONNECTED);
break;
}
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);
}
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);
break;
}
}
} 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);
}
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);
break;
}
}
}
}
//-----------------------------------------------------
// 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;
}
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;
}
}
}