/*
* Copyright (C) 2012 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.bluetooth.hfp;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
import static android.media.audio.Flags.deprecateStreamBtSco;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSinkAudioPolicy;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.BluetoothUuid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.BluetoothProfileConnectionInfo;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.UserHandle;
import android.sysprop.BluetoothProperties;
import android.telecom.PhoneAccount;
import android.util.Log;
import com.android.bluetooth.BluetoothStatsLog;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dp.A2dpService;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.hfpclient.HeadsetClientService;
import com.android.bluetooth.hfpclient.HeadsetClientStateMachine;
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.bluetooth.telephony.BluetoothInCallService;
import com.android.bluetooth.util.SystemProperties;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* Provides Bluetooth Headset and Handsfree profile, as a service in the Bluetooth application.
*
*
Three modes for SCO audio: Mode 1: Telecom call through {@link #phoneStateChanged(int, int,
* int, String, int, String, boolean)} Mode 2: Virtual call through {@link
* #startScoUsingVirtualVoiceCall()} Mode 3: Voice recognition through {@link
* #startVoiceRecognition(BluetoothDevice)}
*
*
When one mode is active, other mode cannot be started. API user has to terminate existing
* modes using the correct API or just {@link #disconnectAudio()} if user is a system service,
* before starting a new mode.
*
*
{@link #connectAudio()} will start SCO audio at one of the above modes, but won't change mode
* {@link #disconnectAudio()} can happen in any mode to disconnect SCO
*
*
When audio is disconnected, only Mode 1 Telecom call will be persisted, both Mode 2 virtual
* call and Mode 3 voice call will be terminated upon SCO termination and client has to restart the
* mode.
*
*
NOTE: SCO termination can either be initiated on the AG side or the HF side TODO(b/79660380):
* As a workaround, voice recognition will be terminated if virtual call or Telecom call is
* initiated while voice recognition is ongoing, in case calling app did not call {@link
* #stopVoiceRecognition(BluetoothDevice)}
*
*
AG - Audio Gateway, device running this {@link HeadsetService}, e.g. Android Phone HF -
* Handsfree device, device running headset client, e.g. Wireless headphones or car kits
*/
public class HeadsetService extends ProfileService {
private static final String TAG = HeadsetService.class.getSimpleName();
/** HFP AG owned/managed components */
private static final String HFP_AG_IN_CALL_SERVICE =
BluetoothInCallService.class.getCanonicalName();
private static final String DISABLE_INBAND_RINGING_PROPERTY =
"persist.bluetooth.disableinbandringing";
private static final String REJECT_SCO_IF_HFPC_CONNECTED_PROPERTY =
"bluetooth.hfp.reject_sco_if_hfpc_connected";
private static final ParcelUuid[] HEADSET_UUIDS = {BluetoothUuid.HSP, BluetoothUuid.HFP};
private static final int[] CONNECTING_CONNECTED_STATES = {STATE_CONNECTING, STATE_CONNECTED};
private static final int DIALING_OUT_TIMEOUT_MS = 10000;
private static final int CLCC_END_MARK_INDEX = 0;
// Timeout for state machine thread join, to prevent potential ANR.
private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000;
private final AdapterService mAdapterService;
private final DatabaseManager mDatabaseManager;
private final HeadsetNativeInterface mNativeInterface;
private final HashMap mStateMachines = new HashMap<>();
private final Handler mHandler;
private final Looper mStateMachinesLooper;
private final Handler mStateMachinesThreadHandler;
private final HandlerThread mStateMachinesThread;
// This is also used as a lock for shared data in HeadsetService
private final HeadsetSystemInterface mSystemInterface;
private int mMaxHeadsetConnections = 1;
private BluetoothDevice mExposedActiveDevice;
private BluetoothDevice mActiveDevice;
private boolean mAudioRouteAllowed = true;
// Indicates whether SCO audio needs to be forced to open regardless ANY OTHER restrictions
private boolean mForceScoAudio;
private boolean mInbandRingingRuntimeDisable;
private boolean mVirtualCallStarted;
// Non null value indicates a pending dialing out event is going on
private DialingOutTimeoutEvent mDialingOutTimeoutEvent;
private boolean mVoiceRecognitionStarted;
// Non null value indicates a pending voice recognition request from headset is going on
private VoiceRecognitionTimeoutEvent mVoiceRecognitionTimeoutEvent;
// Timeout when voice recognition is started by remote device
@VisibleForTesting static int sStartVrTimeoutMs = 5000;
private final ArrayList mPendingClccResponses = new ArrayList<>();
private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback =
new AudioManagerAudioDeviceCallback();
private static HeadsetService sHeadsetService;
@VisibleForTesting boolean mIsAptXSwbEnabled = false;
@VisibleForTesting boolean mIsAptXSwbPmEnabled = false;
@VisibleForTesting ServiceFactory mFactory = new ServiceFactory();
public HeadsetService(AdapterService adapterService) {
this(adapterService, HeadsetNativeInterface.getInstance(), null);
}
@VisibleForTesting
HeadsetService(AdapterService adapterService, HeadsetNativeInterface nativeInterface) {
this(adapterService, nativeInterface, null);
}
@VisibleForTesting
HeadsetService(
AdapterService adapterService, HeadsetNativeInterface nativeInterface, Looper looper) {
super(requireNonNull(adapterService));
mAdapterService = adapterService;
mDatabaseManager = requireNonNull(mAdapterService.getDatabase());
mNativeInterface = requireNonNull(nativeInterface);
if (looper != null) {
mHandler = new Handler(looper);
mStateMachinesThread = null;
mStateMachinesLooper = looper;
} else {
mHandler = new Handler(Looper.getMainLooper());
mStateMachinesThread = new HandlerThread("HeadsetService.StateMachines");
mStateMachinesThread.start();
mStateMachinesLooper = mStateMachinesThread.getLooper();
}
mStateMachinesThreadHandler = new Handler(mStateMachinesLooper);
setComponentAvailable(HFP_AG_IN_CALL_SERVICE, true);
// Step 3: Initialize system interface
mSystemInterface = HeadsetObjectsFactory.getInstance().makeSystemInterface(this);
// Step 4: Initialize native interface
mIsAptXSwbEnabled =
SystemProperties.getBoolean("bluetooth.hfp.codec_aptx_voice.enabled", false);
Log.i(TAG, "mIsAptXSwbEnabled: " + mIsAptXSwbEnabled);
mIsAptXSwbPmEnabled =
SystemProperties.getBoolean(
"bluetooth.hfp.swb.aptx.power_management.enabled", false);
Log.i(TAG, "mIsAptXSwbPmEnabled: " + mIsAptXSwbPmEnabled);
setHeadsetService(this);
mMaxHeadsetConnections = mAdapterService.getMaxConnectedAudioDevices();
// Add 1 to allow a pending device to be connecting or disconnecting
mNativeInterface.init(mMaxHeadsetConnections + 1, isInbandRingingEnabled());
enableSwbCodec(
HeadsetHalConstants.BTHF_SWB_CODEC_VENDOR_APTX, mIsAptXSwbEnabled, mActiveDevice);
// Step 6: Register Audio Device callback
if (Utils.isScoManagedByAudioEnabled()) {
mSystemInterface
.getAudioManager()
.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);
}
// Step 7: Setup broadcast receivers
IntentFilter filter = new IntentFilter();
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(AudioManager.ACTION_VOLUME_CHANGED);
filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
registerReceiver(mHeadsetReceiver, filter);
}
public static boolean isEnabled() {
return BluetoothProperties.isProfileHfpAgEnabled().orElse(false);
}
@Override
public IProfileServiceBinder initBinder() {
return new HeadsetServiceBinder(this);
}
@Override
public void cleanup() {
Log.i(TAG, "Cleanup Headset Service");
// Step 7: Tear down broadcast receivers
unregisterReceiver(mHeadsetReceiver);
// Step 6: Unregister Audio Device Callback
if (Utils.isScoManagedByAudioEnabled()) {
mSystemInterface
.getAudioManager()
.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);
}
synchronized (mStateMachines) {
// Reset active device to null
if (mActiveDevice != null) {
mExposedActiveDevice = null;
mActiveDevice = null;
broadcastActiveDevice(null);
}
mInbandRingingRuntimeDisable = false;
mForceScoAudio = false;
mAudioRouteAllowed = true;
mMaxHeadsetConnections = 1;
mVoiceRecognitionStarted = false;
mVirtualCallStarted = false;
if (mDialingOutTimeoutEvent != null) {
mStateMachinesThreadHandler.removeCallbacks(mDialingOutTimeoutEvent);
mDialingOutTimeoutEvent = null;
}
if (mVoiceRecognitionTimeoutEvent != null) {
mStateMachinesThreadHandler.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
try {
mSystemInterface.getVoiceRecognitionWakeLock().release();
} catch (RuntimeException e) {
Log.d(TAG, "non properly release getVoiceRecognitionWakeLock", e);
}
}
}
// Step 5: Destroy state machines
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
HeadsetObjectsFactory.getInstance().destroyStateMachine(stateMachine);
}
mStateMachines.clear();
}
// Step 4: Destroy native interface
mNativeInterface.cleanup();
setHeadsetService(null);
// Step 3: Destroy system interface
mSystemInterface.stop();
// Step 2: Stop handler thread
if (mStateMachinesThread != null) {
try {
mStateMachinesThread.quitSafely();
mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
} catch (InterruptedException e) {
// Do not rethrow as we are shutting down anyway
}
}
// Unregister Handler and stop all queued messages.
mHandler.removeCallbacksAndMessages(null);
// Step 1: Clear
setComponentAvailable(HFP_AG_IN_CALL_SERVICE, false);
}
/**
* Checks if this service object is able to accept binder calls
*
* @return True if the object can accept binder calls, False otherwise
*/
public boolean isAlive() {
return isAvailable();
}
/**
* Get the {@link Looper} for the state machine thread. This is used in testing and helper
* objects
*
* @return {@link Looper} for the state machine thread
*/
@VisibleForTesting
public Looper getStateMachinesThreadLooper() {
return mStateMachinesThread.getLooper();
}
interface StateMachineTask {
void execute(HeadsetStateMachine stateMachine);
}
private boolean doForStateMachine(BluetoothDevice device, StateMachineTask task) {
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return false;
}
task.execute(stateMachine);
}
return true;
}
private void doForEachConnectedStateMachine(StateMachineTask task) {
synchronized (mStateMachines) {
for (BluetoothDevice device : getConnectedDevices()) {
task.execute(mStateMachines.get(device));
}
}
}
private void doForEachConnectedStateMachine(List tasks) {
synchronized (mStateMachines) {
for (BluetoothDevice device : getConnectedDevices()) {
for (StateMachineTask task : tasks) {
task.execute(mStateMachines.get(device));
}
}
}
}
void onDeviceStateChanged(HeadsetDeviceState deviceState) {
doForEachConnectedStateMachine(
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.DEVICE_STATE_CHANGED, deviceState));
}
/**
* Handle messages from native (JNI) to Java. This needs to be synchronized to avoid posting
* messages to state machine before start() is done
*
* @param stackEvent event from native stack
*/
void messageFromNative(HeadsetStackEvent stackEvent) {
requireNonNull(stackEvent.device);
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(stackEvent.device);
if (stackEvent.type == HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
switch (stackEvent.valueInt) {
case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
case HeadsetHalConstants.CONNECTION_STATE_CONNECTING:
{
// Create new state machine if none is found
if (stateMachine == null) {
stateMachine =
HeadsetObjectsFactory.getInstance()
.makeStateMachine(
stackEvent.device,
mStateMachinesLooper,
this,
mAdapterService,
mNativeInterface,
mSystemInterface);
mStateMachines.put(stackEvent.device, stateMachine);
}
break;
}
}
}
if (stateMachine == null) {
throw new IllegalStateException(
"State machine not found for stack event: " + stackEvent);
}
stateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, stackEvent);
}
}
private final BroadcastReceiver mHeadsetReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action == null) {
Log.w(TAG, "mHeadsetReceiver, action is null");
return;
}
switch (action) {
case Intent.ACTION_BATTERY_CHANGED:
{
int batteryLevel =
intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
if (batteryLevel < 0 || scale <= 0) {
Log.e(
TAG,
"Bad Battery Changed intent: batteryLevel="
+ batteryLevel
+ ", scale="
+ scale);
return;
}
int cindBatteryLevel =
Math.round(batteryLevel * 5 / ((float) scale));
mSystemInterface
.getHeadsetPhoneState()
.setCindBatteryCharge(cindBatteryLevel);
break;
}
case AudioManager.ACTION_VOLUME_CHANGED:
{
int streamType =
intent.getIntExtra(
AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
int volStream = AudioManager.STREAM_BLUETOOTH_SCO;
if (deprecateStreamBtSco()) {
volStream = AudioManager.STREAM_VOICE_CALL;
}
if (streamType == volStream) {
doForEachConnectedStateMachine(
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine
.INTENT_SCO_VOLUME_CHANGED,
intent));
}
break;
}
case BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY:
{
int requestType =
intent.getIntExtra(
BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
logD(
"Received BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY,"
+ " device="
+ device
+ ", type="
+ requestType);
if (requestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine =
mStateMachines.get(device);
if (stateMachine == null) {
Log.wtf(TAG, "Cannot find state machine for " + device);
return;
}
stateMachine.sendMessage(
HeadsetStateMachine.INTENT_CONNECTION_ACCESS_REPLY,
intent);
}
}
break;
}
default:
Log.w(TAG, "Unknown action " + action);
}
}
};
public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) {
mHandler.post(() -> bondStateChanged(device, toState));
}
private void bondStateChanged(BluetoothDevice device, int state) {
logD("Bond state changed for device: " + device + " state: " + state);
if (state != BluetoothDevice.BOND_NONE) {
return;
}
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return;
}
if (stateMachine.getConnectionState() != STATE_DISCONNECTED) {
return;
}
removeStateMachine(device);
}
}
// API methods
public static synchronized HeadsetService getHeadsetService() {
if (sHeadsetService == null) {
Log.w(TAG, "getHeadsetService(): service is NULL");
return null;
}
if (!sHeadsetService.isAvailable()) {
Log.w(TAG, "getHeadsetService(): service is not available");
return null;
}
return sHeadsetService;
}
@VisibleForTesting
public static synchronized void setHeadsetService(HeadsetService instance) {
logD("setHeadsetService(): set to: " + instance);
sHeadsetService = instance;
}
public boolean connect(BluetoothDevice device) {
if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) {
Log.w(
TAG,
"connect: CONNECTION_POLICY_FORBIDDEN, device="
+ device
+ ", "
+ Utils.getUidPidString());
return false;
}
final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) {
Log.e(
TAG,
"connect: Cannot connect to "
+ device
+ ": no headset UUID, "
+ Utils.getUidPidString());
return false;
}
synchronized (mStateMachines) {
Log.i(TAG, "connect: device=" + device + ", " + Utils.getUidPidString());
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
stateMachine =
HeadsetObjectsFactory.getInstance()
.makeStateMachine(
device,
mStateMachinesLooper,
this,
mAdapterService,
mNativeInterface,
mSystemInterface);
mStateMachines.put(device, stateMachine);
}
int connectionState = stateMachine.getConnectionState();
if (connectionState == STATE_CONNECTED || connectionState == STATE_CONNECTING) {
Log.w(
TAG,
"connect: device "
+ device
+ " is already connected/connecting, connectionState="
+ connectionState);
return false;
}
List connectingConnectedDevices =
getDevicesMatchingConnectionStates(CONNECTING_CONNECTED_STATES);
boolean disconnectExisting = false;
if (connectingConnectedDevices.size() >= mMaxHeadsetConnections) {
// When there is maximum one device, we automatically disconnect the current one
if (mMaxHeadsetConnections == 1) {
disconnectExisting = true;
} else {
Log.w(TAG, "Max connection has reached, rejecting connection to " + device);
return false;
}
}
if (disconnectExisting) {
for (BluetoothDevice connectingConnectedDevice : connectingConnectedDevices) {
disconnect(connectingConnectedDevice);
}
setActiveDevice(null);
}
stateMachine.sendMessage(HeadsetStateMachine.CONNECT, device);
}
return true;
}
/**
* Disconnects hfp from the passed in device
*
* @param device is the device with which we will disconnect hfp
* @return true if hfp is disconnected, false if the device is not connected
*/
public boolean disconnect(BluetoothDevice device) {
Log.i(TAG, "disconnect: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "disconnect: device " + device + " not ever connected/connecting");
return false;
}
int connectionState = stateMachine.getConnectionState();
if (connectionState != STATE_CONNECTED && connectionState != STATE_CONNECTING) {
Log.w(
TAG,
"disconnect: device "
+ device
+ " not connected/connecting, connectionState="
+ connectionState);
return false;
}
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT, device);
}
return true;
}
public List getConnectedDevices() {
ArrayList devices = new ArrayList<>();
synchronized (mStateMachines) {
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
if (stateMachine.getConnectionState() == STATE_CONNECTED) {
devices.add(stateMachine.getDevice());
}
}
}
return devices;
}
/**
* Same as the API method {@link BluetoothHeadset#getDevicesMatchingConnectionStates(int[])}
*
* @param states an array of states from {@link BluetoothProfile}
* @return a list of devices matching the array of connection states
*/
public List getDevicesMatchingConnectionStates(int[] states) {
ArrayList devices = new ArrayList<>();
synchronized (mStateMachines) {
if (states == null) {
return devices;
}
final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
if (bondedDevices == null) {
return devices;
}
for (BluetoothDevice device : bondedDevices) {
final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) {
continue;
}
int connectionState = getConnectionState(device);
for (int state : states) {
if (connectionState == state) {
devices.add(device);
break;
}
}
}
}
return devices;
}
public int getConnectionState(BluetoothDevice device) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return STATE_DISCONNECTED;
}
return stateMachine.getConnectionState();
}
}
/**
* Set connection policy of the profile and connects it if connectionPolicy is {@link
* BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
* BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
*
* The device should already be paired. Connection policy can be one of: {@link
* BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
* BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
* BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
*
* @param device Paired bluetooth device
* @param connectionPolicy is the connection policy to set to for this profile
* @return true if connectionPolicy is set, false on error
*/
public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
Log.i(
TAG,
"setConnectionPolicy: device="
+ device
+ ", connectionPolicy="
+ connectionPolicy
+ ", "
+ Utils.getUidPidString());
if (!mDatabaseManager.setProfileConnectionPolicy(
device, BluetoothProfile.HEADSET, connectionPolicy)) {
return false;
}
if (connectionPolicy == CONNECTION_POLICY_ALLOWED) {
connect(device);
} else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) {
disconnect(device);
}
return true;
}
/**
* Get the connection policy of the profile.
*
*
The connection policy can be any of: {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
* {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
* BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
*
* @param device Bluetooth device
* @return connection policy of the device
*/
public int getConnectionPolicy(BluetoothDevice device) {
return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET);
}
boolean isNoiseReductionSupported(BluetoothDevice device) {
return mNativeInterface.isNoiseReductionSupported(device);
}
boolean isVoiceRecognitionSupported(BluetoothDevice device) {
return mNativeInterface.isVoiceRecognitionSupported(device);
}
boolean startVoiceRecognition(BluetoothDevice device) {
Log.i(TAG, "startVoiceRecognition: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
// TODO(b/79660380): Workaround in case voice recognition was not terminated properly
if (mVoiceRecognitionStarted) {
boolean status = stopVoiceRecognition(mActiveDevice);
Log.w(
TAG,
"startVoiceRecognition: voice recognition is still active, just called "
+ "stopVoiceRecognition, returned "
+ status
+ " on "
+ mActiveDevice
+ ", please try again");
mVoiceRecognitionStarted = false;
return false;
}
if (!isAudioModeIdle()) {
Log.w(
TAG,
"startVoiceRecognition: audio mode not idle, active device is "
+ mActiveDevice);
return false;
}
// Audio should not be on when no audio mode is active
if (isAudioOn()) {
// Disconnect audio so that API user can try later
int status = disconnectAudio();
Log.w(
TAG,
"startVoiceRecognition: audio is still active, please wait for audio to"
+ " be disconnected, disconnectAudio() returned "
+ status
+ ", active device is "
+ mActiveDevice);
return false;
}
boolean pendingRequestByHeadset = false;
if (mVoiceRecognitionTimeoutEvent != null) {
if (!mVoiceRecognitionTimeoutEvent.mVoiceRecognitionDevice.equals(device)) {
// TODO(b/79660380): Workaround when target device != requesting device
Log.w(
TAG,
"startVoiceRecognition: device "
+ device
+ " is not the same as requesting device "
+ mVoiceRecognitionTimeoutEvent.mVoiceRecognitionDevice
+ ", fall back to requesting device");
device = mVoiceRecognitionTimeoutEvent.mVoiceRecognitionDevice;
}
mStateMachinesThreadHandler.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
try {
mSystemInterface.getVoiceRecognitionWakeLock().release();
} catch (RuntimeException e) {
Log.d(TAG, "non properly release getVoiceRecognitionWakeLock", e);
}
}
pendingRequestByHeadset = true;
}
if (!device.equals(mActiveDevice) && !setActiveDevice(device)) {
Log.w(TAG, "startVoiceRecognition: failed to set " + device + " as active");
return false;
}
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "startVoiceRecognition: " + device + " is never connected");
return false;
}
int connectionState = stateMachine.getConnectionState();
if (connectionState != STATE_CONNECTED && connectionState != STATE_CONNECTING) {
Log.w(TAG, "startVoiceRecognition: " + device + " is not connected or connecting");
return false;
}
if (SystemProperties.getBoolean(REJECT_SCO_IF_HFPC_CONNECTED_PROPERTY, false)
&& isHeadsetClientConnected()) {
Log.w(TAG, "startVoiceRecognition: rejected SCO since HFPC is connected!");
return false;
}
mVoiceRecognitionStarted = true;
if (pendingRequestByHeadset) {
stateMachine.sendMessage(
HeadsetStateMachine.VOICE_RECOGNITION_RESULT, 1 /* success */, 0, device);
} else {
stateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_START, device);
}
if (!Utils.isScoManagedByAudioEnabled()) {
stateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO, device);
logScoSessionMetric(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_CONNECT_AUDIO_START,
Binder.getCallingUid());
}
}
if (Utils.isScoManagedByAudioEnabled()) {
BluetoothDevice voiceRecognitionDevice = device;
// when isScoManagedByAudio is on, tell AudioManager to connect SCO
AudioManager am = mSystemInterface.getAudioManager();
Optional audioDeviceInfo =
am.getAvailableCommunicationDevices().stream()
.filter(
x ->
x.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
&& x.getAddress()
.equals(
voiceRecognitionDevice
.getAddress()))
.findFirst();
if (audioDeviceInfo.isPresent()) {
BluetoothDevice finalDevice = device;
mHandler.post(
() -> {
am.setCommunicationDevice(audioDeviceInfo.get());
logScoSessionMetric(
finalDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VOICE_RECOGNITION_INITIATED_START,
Binder.getCallingUid());
Log.i(TAG, "Audio Manager will initiate the SCO for Voice Recognition");
});
} else {
Log.w(
TAG,
"Cannot find audioDeviceInfo that matches device="
+ voiceRecognitionDevice
+ " to create the SCO");
return false;
}
}
enableSwbCodec(HeadsetHalConstants.BTHF_SWB_CODEC_VENDOR_APTX, true, device);
return true;
}
boolean stopVoiceRecognition(BluetoothDevice device) {
Log.i(TAG, "stopVoiceRecognition: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
if (!Objects.equals(mActiveDevice, device)) {
Log.w(
TAG,
"stopVoiceRecognition: requested device "
+ device
+ " is not active, use active device "
+ mActiveDevice
+ " instead");
device = mActiveDevice;
}
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "stopVoiceRecognition: " + device + " is never connected");
return false;
}
int connectionState = stateMachine.getConnectionState();
if (connectionState != STATE_CONNECTED && connectionState != STATE_CONNECTING) {
Log.w(TAG, "stopVoiceRecognition: " + device + " is not connected or connecting");
return false;
}
if (!mVoiceRecognitionStarted) {
Log.w(TAG, "stopVoiceRecognition: voice recognition was not started");
return false;
}
mVoiceRecognitionStarted = false;
stateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_STOP, device);
if (!Utils.isScoManagedByAudioEnabled()) {
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, device);
logScoSessionMetric(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_DISCONNECT_AUDIO_END,
Binder.getCallingUid());
}
}
if (Utils.isScoManagedByAudioEnabled()) {
// do the task outside synchronized to avoid deadlock with Audio Fwk
BluetoothDevice finalDevice = device;
mHandler.post(
() -> {
mSystemInterface.getAudioManager().clearCommunicationDevice();
logScoSessionMetric(
finalDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VOICE_RECOGNITION_INITIATED_END,
Binder.getCallingUid());
});
}
enableSwbCodec(HeadsetHalConstants.BTHF_SWB_CODEC_VENDOR_APTX, false, device);
return true;
}
boolean isAudioOn() {
return getNonIdleAudioDevices().size() > 0;
}
boolean isAudioConnected(BluetoothDevice device) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return false;
}
return stateMachine.getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
}
int getAudioState(BluetoothDevice device) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
return stateMachine.getAudioState();
}
}
public void setAudioRouteAllowed(boolean allowed) {
Log.i(TAG, "setAudioRouteAllowed: allowed=" + allowed + ", " + Utils.getUidPidString());
mAudioRouteAllowed = allowed;
mNativeInterface.setScoAllowed(allowed);
}
public boolean getAudioRouteAllowed() {
return mAudioRouteAllowed;
}
public void setForceScoAudio(boolean forced) {
Log.i(TAG, "setForceScoAudio: forced=" + forced + ", " + Utils.getUidPidString());
mForceScoAudio = forced;
}
@VisibleForTesting
public boolean getForceScoAudio() {
return mForceScoAudio;
}
/**
* Get first available device for SCO audio
*
* @return first connected headset device
*/
@VisibleForTesting
@Nullable
public BluetoothDevice getFirstConnectedAudioDevice() {
ArrayList stateMachines = new ArrayList<>();
synchronized (mStateMachines) {
List availableDevices =
getDevicesMatchingConnectionStates(CONNECTING_CONNECTED_STATES);
for (BluetoothDevice device : availableDevices) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
continue;
}
stateMachines.add(stateMachine);
}
}
stateMachines.sort(Comparator.comparingLong(HeadsetStateMachine::getConnectingTimestampMs));
if (stateMachines.size() > 0) {
return stateMachines.get(0).getDevice();
}
return null;
}
/**
* Process a change in the silence mode for a {@link BluetoothDevice}.
*
* @param device the device to change silence mode
* @param silence true to enable silence mode, false to disable.
* @return true on success, false on error
*/
@VisibleForTesting
public boolean setSilenceMode(BluetoothDevice device, boolean silence) {
Log.d(TAG, "setSilenceMode(" + device + "): " + silence);
if (silence && Objects.equals(mActiveDevice, device)) {
setActiveDevice(null);
} else if (!silence && mActiveDevice == null) {
// Set the device as the active device if currently no active device.
setActiveDevice(device);
}
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "setSilenceMode: device " + device + " was never connected/connecting");
return false;
}
stateMachine.setSilenceDevice(silence);
}
return true;
}
/**
* Get the Bluetooth Audio Policy stored in the state machine
*
* @param device the device to change silence mode
* @return a {@link BluetoothSinkAudioPolicy} object
*/
public BluetoothSinkAudioPolicy getHfpCallAudioPolicy(BluetoothDevice device) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "getHfpCallAudioPolicy(), " + device + " does not have a state machine");
return null;
}
return stateMachine.getHfpCallAudioPolicy();
}
}
/** Remove the active device */
private void removeActiveDevice() {
synchronized (mStateMachines) {
// As per b/202602952, if we remove the active device due to a disconnection,
// we need to check if another device is connected and set it active instead.
// Calling this before any other active related calls has the same effect as
// a classic active device switch.
BluetoothDevice fallbackDevice = getFallbackDevice();
if (fallbackDevice != null
&& mActiveDevice != null
&& getConnectionState(mActiveDevice) != STATE_CONNECTED) {
setActiveDevice(fallbackDevice);
return;
}
// Clear the active device
if (mVoiceRecognitionStarted) {
if (!stopVoiceRecognition(mActiveDevice)) {
Log.w(
TAG,
"removeActiveDevice: fail to stopVoiceRecognition from "
+ mActiveDevice);
}
}
if (mVirtualCallStarted) {
if (!stopScoUsingVirtualVoiceCall()) {
Log.w(
TAG,
"removeActiveDevice: fail to stopScoUsingVirtualVoiceCall from "
+ mActiveDevice);
}
}
if (getAudioState(mActiveDevice) != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
int disconnectStatus = disconnectAudio(mActiveDevice);
if (disconnectStatus != BluetoothStatusCodes.SUCCESS) {
Log.w(
TAG,
"removeActiveDevice: disconnectAudio failed on "
+ mActiveDevice
+ " with status code "
+ disconnectStatus);
}
}
// Make sure the Audio Manager knows the previous active device is no longer active.
BluetoothDevice previousActiveDevice = mActiveDevice;
mActiveDevice = null;
mNativeInterface.setActiveDevice(null);
if (Utils.isScoManagedByAudioEnabled()) {
mSystemInterface
.getAudioManager()
.handleBluetoothActiveDeviceChanged(
null,
previousActiveDevice,
BluetoothProfileConnectionInfo.createHfpInfo());
} else {
broadcastActiveDevice(null);
}
updateInbandRinging(null, true);
}
}
/**
* Set the active device.
*
* @param device the active device
* @return true on success, otherwise false
*/
public boolean setActiveDevice(BluetoothDevice device) {
Log.i(TAG, "setActiveDevice: device=" + device + ", " + Utils.getUidPidString());
if (device == null) {
removeActiveDevice();
return true;
}
synchronized (mStateMachines) {
if (device.equals(mActiveDevice)) {
Log.i(TAG, "setActiveDevice: device " + device + " is already active");
return true;
}
if (getConnectionState(device) != STATE_CONNECTED) {
Log.e(
TAG,
"setActiveDevice: Cannot set "
+ device
+ " as active, device is not connected");
return false;
}
if (!mNativeInterface.setActiveDevice(device)) {
Log.e(TAG, "setActiveDevice: Cannot set " + device + " as active in native layer");
return false;
}
BluetoothDevice previousActiveDevice = mActiveDevice;
mActiveDevice = device;
/* If HFP is getting active for a phone call and there are active LE Audio devices,
* Lets inactive LeAudio device as soon as possible so there is no CISes connected
* when SCO is going to be created
*/
if (mSystemInterface.isInCall() || mSystemInterface.isRinging()) {
LeAudioService leAudioService = mFactory.getLeAudioService();
if (leAudioService != null && !leAudioService.getConnectedDevices().isEmpty()) {
Log.i(TAG, "Make sure no le audio device active for HFP handover.");
leAudioService.setInactiveForHfpHandover(mActiveDevice);
}
}
if (getAudioState(previousActiveDevice) != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
int disconnectStatus = disconnectAudio(previousActiveDevice);
if (disconnectStatus != BluetoothStatusCodes.SUCCESS) {
Log.e(
TAG,
"setActiveDevice: fail to disconnectAudio from "
+ previousActiveDevice
+ " with status code "
+ disconnectStatus);
mActiveDevice = previousActiveDevice;
mNativeInterface.setActiveDevice(previousActiveDevice);
return false;
}
if (Utils.isScoManagedByAudioEnabled()) {
// tell Audio Framework that active device changed
mSystemInterface
.getAudioManager()
.handleBluetoothActiveDeviceChanged(
mActiveDevice,
previousActiveDevice,
BluetoothProfileConnectionInfo.createHfpInfo());
} else {
broadcastActiveDevice(mActiveDevice);
}
} else if (shouldPersistAudio()) {
if (Utils.isScoManagedByAudioEnabled()) {
// tell Audio Framework that active device changed
mSystemInterface
.getAudioManager()
.handleBluetoothActiveDeviceChanged(
mActiveDevice,
previousActiveDevice,
BluetoothProfileConnectionInfo.createHfpInfo());
// Audio Framework will handle audio transition
return true;
}
broadcastActiveDevice(mActiveDevice);
int connectStatus = connectAudio(mActiveDevice);
if (connectStatus != BluetoothStatusCodes.SUCCESS) {
Log.e(
TAG,
"setActiveDevice: fail to connectAudio to "
+ mActiveDevice
+ " with status code "
+ connectStatus);
if (previousActiveDevice == null) {
removeActiveDevice();
} else {
mActiveDevice = previousActiveDevice;
mNativeInterface.setActiveDevice(previousActiveDevice);
}
return false;
}
} else {
if (Utils.isScoManagedByAudioEnabled()) {
// tell Audio Framework that active device changed
mSystemInterface
.getAudioManager()
.handleBluetoothActiveDeviceChanged(
mActiveDevice,
previousActiveDevice,
BluetoothProfileConnectionInfo.createHfpInfo());
} else {
broadcastActiveDevice(mActiveDevice);
}
}
updateInbandRinging(device, true);
}
return true;
}
/**
* Get the active device.
*
* @return the active device or null if no device is active
*/
public BluetoothDevice getActiveDevice() {
synchronized (mStateMachines) {
return mActiveDevice;
}
}
public int connectAudio() {
synchronized (mStateMachines) {
BluetoothDevice device = mActiveDevice;
if (device == null) {
Log.w(TAG, "connectAudio: no active device, " + Utils.getUidPidString());
return BluetoothStatusCodes.ERROR_NO_ACTIVE_DEVICES;
}
return connectAudio(device);
}
}
int connectAudio(BluetoothDevice device) {
Log.i(TAG, "connectAudio: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "connectAudio: device " + device + " was never connected/connecting");
return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
}
int scoConnectionAllowedState = isScoAcceptable(device);
if (scoConnectionAllowedState != BluetoothStatusCodes.SUCCESS) {
Log.w(TAG, "connectAudio, rejected SCO request to " + device);
return scoConnectionAllowedState;
}
if (stateMachine.getConnectionState() != STATE_CONNECTED) {
Log.w(TAG, "connectAudio: profile not connected");
return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
}
if (stateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
logD("connectAudio: audio is not idle for device " + device);
logScoSessionMetric(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_CONNECT_AUDIO_START,
Binder.getCallingUid());
return BluetoothStatusCodes.SUCCESS;
}
if (isAudioOn()) {
Log.w(
TAG,
"connectAudio: audio is not idle, current audio devices are "
+ Arrays.toString(getNonIdleAudioDevices().toArray()));
return BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED;
}
stateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO, device);
logScoSessionMetric(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_CONNECT_AUDIO_START,
Binder.getCallingUid());
}
return BluetoothStatusCodes.SUCCESS;
}
private List getNonIdleAudioDevices() {
ArrayList devices = new ArrayList<>();
synchronized (mStateMachines) {
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
if (stateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
devices.add(stateMachine.getDevice());
}
}
}
return devices;
}
int disconnectAudio() {
int disconnectResult = BluetoothStatusCodes.ERROR_NO_ACTIVE_DEVICES;
synchronized (mStateMachines) {
for (BluetoothDevice device : getNonIdleAudioDevices()) {
disconnectResult = disconnectAudio(device);
if (disconnectResult == BluetoothStatusCodes.SUCCESS) {
logScoSessionMetric(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_DISCONNECT_AUDIO_END,
Binder.getCallingUid());
return disconnectResult;
} else {
Log.e(
TAG,
"disconnectAudio() from "
+ device
+ " failed with status code "
+ disconnectResult);
}
}
}
logD("disconnectAudio() no active audio connection");
return disconnectResult;
}
int disconnectAudio(BluetoothDevice device) {
synchronized (mStateMachines) {
Log.i(TAG, "disconnectAudio: device=" + device + ", " + Utils.getUidPidString());
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "disconnectAudio: device " + device + " was never connected/connecting");
return BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED;
}
if (stateMachine.getAudioState() == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.w(TAG, "disconnectAudio, audio is already disconnected for " + device);
return BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_DISCONNECTED;
}
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, device);
}
return BluetoothStatusCodes.SUCCESS;
}
boolean isVirtualCallStarted() {
synchronized (mStateMachines) {
return mVirtualCallStarted;
}
}
boolean startScoUsingVirtualVoiceCall() {
Log.i(TAG, "startScoUsingVirtualVoiceCall: " + Utils.getUidPidString());
synchronized (mStateMachines) {
// TODO(b/79660380): Workaround in case voice recognition was not terminated properly
if (mVoiceRecognitionStarted) {
boolean status = stopVoiceRecognition(mActiveDevice);
Log.w(
TAG,
"startScoUsingVirtualVoiceCall: voice recognition is still active, "
+ "just called stopVoiceRecognition, returned "
+ status
+ " on "
+ mActiveDevice
+ ", please try again");
mVoiceRecognitionStarted = false;
return false;
}
if (!isAudioModeIdle()) {
Log.w(
TAG,
"startScoUsingVirtualVoiceCall: audio mode not idle, active device is "
+ mActiveDevice);
return false;
}
// Audio should not be on when no audio mode is active
if (isAudioOn()) {
// Disconnect audio so that API user can try later
int status = disconnectAudio();
Log.w(
TAG,
"startScoUsingVirtualVoiceCall: audio is still active, please wait for "
+ "audio to be disconnected, disconnectAudio() returned "
+ status
+ ", active device is "
+ mActiveDevice);
return false;
}
if (mActiveDevice == null) {
Log.w(TAG, "startScoUsingVirtualVoiceCall: no active device");
return false;
}
if (SystemProperties.getBoolean(REJECT_SCO_IF_HFPC_CONNECTED_PROPERTY, false)
&& isHeadsetClientConnected()) {
Log.w(TAG, "startScoUsingVirtualVoiceCall: rejected SCO since HFPC is connected!");
return false;
}
mVirtualCallStarted = true;
// Send virtual phone state changed to initialize SCO
phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0, "", true);
phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0, "", true);
phoneStateChanged(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, "", true);
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VIRTUAL_VOICE_INITIATED_START,
Binder.getCallingUid());
return true;
}
}
boolean stopScoUsingVirtualVoiceCall() {
Log.i(TAG, "stopScoUsingVirtualVoiceCall: " + Utils.getUidPidString());
synchronized (mStateMachines) {
// 1. Check if virtual call has already started
if (!mVirtualCallStarted) {
Log.w(TAG, "stopScoUsingVirtualVoiceCall: virtual call not started");
return false;
}
mVirtualCallStarted = false;
// 2. Send virtual phone state changed to close SCO
phoneStateChanged(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0, "", true);
}
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VIRTUAL_VOICE_INITIATED_END,
Binder.getCallingUid());
return true;
}
class DialingOutTimeoutEvent implements Runnable {
BluetoothDevice mDialingOutDevice;
DialingOutTimeoutEvent(BluetoothDevice fromDevice) {
mDialingOutDevice = fromDevice;
}
@Override
public void run() {
synchronized (mStateMachines) {
mDialingOutTimeoutEvent = null;
doForStateMachine(
mDialingOutDevice,
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.DIALING_OUT_RESULT,
0 /* fail */,
0,
mDialingOutDevice));
}
}
@Override
public String toString() {
return "DialingOutTimeoutEvent[" + mDialingOutDevice + "]";
}
}
/**
* Dial an outgoing call as requested by the remote device
*
* @param fromDevice remote device that initiated this dial out action
* @param dialNumber number to dial
* @return true on successful dial out
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean dialOutgoingCall(BluetoothDevice fromDevice, String dialNumber) {
synchronized (mStateMachines) {
Log.i(TAG, "dialOutgoingCall: from " + fromDevice);
if (mDialingOutTimeoutEvent != null) {
Log.e(TAG, "dialOutgoingCall, already dialing by " + mDialingOutTimeoutEvent);
return false;
}
if (isVirtualCallStarted()) {
if (!stopScoUsingVirtualVoiceCall()) {
Log.e(TAG, "dialOutgoingCall failed to stop current virtual call");
return false;
}
}
if (!setActiveDevice(fromDevice)) {
Log.e(TAG, "dialOutgoingCall failed to set active device to " + fromDevice);
return false;
}
Intent intent =
new Intent(
Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts(PhoneAccount.SCHEME_TEL, dialNumber, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
mDialingOutTimeoutEvent = new DialingOutTimeoutEvent(fromDevice);
mStateMachinesThreadHandler.postDelayed(
mDialingOutTimeoutEvent, DIALING_OUT_TIMEOUT_MS);
return true;
}
}
/**
* Check if any connected headset has started dialing calls
*
* @return true if some device has started dialing calls
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public boolean hasDeviceInitiatedDialingOut() {
synchronized (mStateMachines) {
return mDialingOutTimeoutEvent != null;
}
}
class VoiceRecognitionTimeoutEvent implements Runnable {
BluetoothDevice mVoiceRecognitionDevice;
VoiceRecognitionTimeoutEvent(BluetoothDevice device) {
mVoiceRecognitionDevice = device;
}
@Override
public void run() {
synchronized (mStateMachines) {
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
try {
mSystemInterface.getVoiceRecognitionWakeLock().release();
} catch (RuntimeException e) {
Log.d(TAG, "non properly release getVoiceRecognitionWakeLock", e);
}
}
mVoiceRecognitionTimeoutEvent = null;
doForStateMachine(
mVoiceRecognitionDevice,
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.VOICE_RECOGNITION_RESULT,
0 /* fail */,
0,
mVoiceRecognitionDevice));
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VOICE_RECOGNITION_HEADSET_TIMEOUT,
Binder.getCallingUid());
}
}
@Override
public String toString() {
return "VoiceRecognitionTimeoutEvent[" + mVoiceRecognitionDevice + "]";
}
}
boolean startVoiceRecognitionByHeadset(BluetoothDevice fromDevice) {
synchronized (mStateMachines) {
Log.i(TAG, "startVoiceRecognitionByHeadset: from " + fromDevice);
// TODO(b/79660380): Workaround in case voice recognition was not terminated properly
if (mVoiceRecognitionStarted) {
boolean status = stopVoiceRecognition(mActiveDevice);
Log.w(
TAG,
"startVoiceRecognitionByHeadset: voice recognition is still active, "
+ "just called stopVoiceRecognition, returned "
+ status
+ " on "
+ mActiveDevice
+ ", please try again");
mVoiceRecognitionStarted = false;
return false;
}
if (fromDevice == null) {
Log.e(TAG, "startVoiceRecognitionByHeadset: fromDevice is null");
return false;
}
if (!isAudioModeIdle()) {
Log.w(
TAG,
"startVoiceRecognitionByHeadset: audio mode not idle, active device is "
+ mActiveDevice);
return false;
}
// Audio should not be on when no audio mode is active
if (isAudioOn()) {
// Disconnect audio so that user can try later
int status = disconnectAudio();
Log.w(
TAG,
"startVoiceRecognitionByHeadset: audio is still active, please wait for"
+ " audio to be disconnected, disconnectAudio() returned "
+ status
+ ", active device is "
+ mActiveDevice);
return false;
}
// Do not start new request until the current one is finished or timeout
if (mVoiceRecognitionTimeoutEvent != null) {
Log.w(
TAG,
"startVoiceRecognitionByHeadset: failed request from "
+ fromDevice
+ ", already pending by "
+ mVoiceRecognitionTimeoutEvent);
return false;
}
if (!setActiveDevice(fromDevice)) {
Log.w(
TAG,
"startVoiceRecognitionByHeadset: failed to set "
+ fromDevice
+ " as active");
return false;
}
if (!mSystemInterface.activateVoiceRecognition()) {
Log.w(TAG, "startVoiceRecognitionByHeadset: failed request from " + fromDevice);
return false;
}
if (SystemProperties.getBoolean(REJECT_SCO_IF_HFPC_CONNECTED_PROPERTY, false)
&& isHeadsetClientConnected()) {
Log.w(TAG, "startVoiceRecognitionByHeadset: rejected SCO since HFPC is connected!");
return false;
}
mVoiceRecognitionTimeoutEvent = new VoiceRecognitionTimeoutEvent(fromDevice);
mStateMachinesThreadHandler.postDelayed(
mVoiceRecognitionTimeoutEvent, sStartVrTimeoutMs);
if (!mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().acquire(sStartVrTimeoutMs);
}
enableSwbCodec(HeadsetHalConstants.BTHF_SWB_CODEC_VENDOR_APTX, true, fromDevice);
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VOICE_RECOGNITION_HEADSET_START,
Binder.getCallingUid());
return true;
}
}
boolean stopVoiceRecognitionByHeadset(BluetoothDevice fromDevice) {
synchronized (mStateMachines) {
Log.i(TAG, "stopVoiceRecognitionByHeadset: from " + fromDevice);
if (!Objects.equals(fromDevice, mActiveDevice)) {
Log.w(
TAG,
"stopVoiceRecognitionByHeadset: "
+ fromDevice
+ " is not active, active device is "
+ mActiveDevice);
return false;
}
if (!mVoiceRecognitionStarted && mVoiceRecognitionTimeoutEvent == null) {
Log.w(
TAG,
"stopVoiceRecognitionByHeadset: voice recognition not started, device="
+ fromDevice);
return false;
}
if (mVoiceRecognitionTimeoutEvent != null) {
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
try {
mSystemInterface.getVoiceRecognitionWakeLock().release();
} catch (RuntimeException e) {
Log.d(TAG, "non properly release getVoiceRecognitionWakeLock", e);
}
}
mStateMachinesThreadHandler.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
}
if (mVoiceRecognitionStarted) {
int disconnectStatus = disconnectAudio();
if (disconnectStatus != BluetoothStatusCodes.SUCCESS) {
Log.w(
TAG,
"stopVoiceRecognitionByHeadset: failed to disconnect audio from "
+ fromDevice
+ " with status code "
+ disconnectStatus);
}
mVoiceRecognitionStarted = false;
}
if (!mSystemInterface.deactivateVoiceRecognition()) {
Log.w(TAG, "stopVoiceRecognitionByHeadset: failed request from " + fromDevice);
return false;
}
enableSwbCodec(HeadsetHalConstants.BTHF_SWB_CODEC_VENDOR_APTX, false, fromDevice);
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_VOICE_RECOGNITION_HEADSET_END,
Binder.getCallingUid());
return true;
}
}
public void phoneStateChanged(
int numActive,
int numHeld,
int callState,
String number,
int type,
String name,
boolean isVirtualCall) {
synchronized (mStateMachines) {
// Should stop all other audio mode in this case
if ((numActive + numHeld) > 0 || callState != HeadsetHalConstants.CALL_STATE_IDLE) {
if (!isVirtualCall && mVirtualCallStarted) {
// stop virtual voice call if there is an incoming Telecom call update
stopScoUsingVirtualVoiceCall();
}
if (mVoiceRecognitionStarted) {
// stop voice recognition if there is any incoming call
stopVoiceRecognition(mActiveDevice);
}
}
if (mDialingOutTimeoutEvent != null) {
// Send result to state machine when dialing starts
if (callState == HeadsetHalConstants.CALL_STATE_DIALING) {
mStateMachinesThreadHandler.removeCallbacks(mDialingOutTimeoutEvent);
doForStateMachine(
mDialingOutTimeoutEvent.mDialingOutDevice,
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.DIALING_OUT_RESULT,
1 /* success */,
0,
mDialingOutTimeoutEvent.mDialingOutDevice));
} else if (callState == HeadsetHalConstants.CALL_STATE_ACTIVE
|| callState == HeadsetHalConstants.CALL_STATE_IDLE) {
// Clear the timeout event when the call is connected or disconnected
if (!mStateMachinesThreadHandler.hasCallbacks(mDialingOutTimeoutEvent)) {
mDialingOutTimeoutEvent = null;
}
}
}
}
mStateMachinesThreadHandler.post(
() -> {
boolean isCallIdleBefore = mSystemInterface.isCallIdle();
mSystemInterface.getHeadsetPhoneState().setNumActiveCall(numActive);
mSystemInterface.getHeadsetPhoneState().setNumHeldCall(numHeld);
mSystemInterface.getHeadsetPhoneState().setCallState(callState);
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_TELECOM_INITIATED_START,
Binder.getCallingUid());
// Suspend A2DP when call about is about to become active
if (mActiveDevice != null
&& callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED
&& !mSystemInterface.isCallIdle()
&& isCallIdleBefore
&& !Utils.isScoManagedByAudioEnabled()) {
mSystemInterface.getAudioManager().setA2dpSuspended(true);
if (isAtLeastU()) {
mSystemInterface.getAudioManager().setLeAudioSuspended(true);
}
}
});
doForEachConnectedStateMachine(
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.CALL_STATE_CHANGED,
new HeadsetCallState(
numActive, numHeld, callState, number, type, name)));
mStateMachinesThreadHandler.post(
() -> {
if (callState == HeadsetHalConstants.CALL_STATE_IDLE
&& mSystemInterface.isCallIdle()
&& !isAudioOn()
&& !Utils.isScoManagedByAudioEnabled()) {
// Resume A2DP when call ended and SCO is not connected
mSystemInterface.getAudioManager().setA2dpSuspended(false);
if (isAtLeastU()) {
mSystemInterface.getAudioManager().setLeAudioSuspended(false);
}
}
});
if (callState == HeadsetHalConstants.CALL_STATE_IDLE) {
final HeadsetStateMachine stateMachine = mStateMachines.get(mActiveDevice);
if (stateMachine == null) {
Log.d(TAG, "phoneStateChanged: CALL_STATE_IDLE, mActiveDevice is Null");
} else {
BluetoothSinkAudioPolicy currentPolicy = stateMachine.getHfpCallAudioPolicy();
if (currentPolicy != null
&& currentPolicy.getActiveDevicePolicyAfterConnection()
== BluetoothSinkAudioPolicy.POLICY_NOT_ALLOWED) {
/*
* If the active device was set because of the pick up audio policy and the
* connecting policy is NOT_ALLOWED, then after the call is terminated, we must
* de-activate this device. If there is a fallback mechanism, we should follow
* it to set fallback device be active.
*/
removeActiveDevice();
BluetoothDevice fallbackDevice = getFallbackDevice();
if (fallbackDevice != null
&& getConnectionState(fallbackDevice) == STATE_CONNECTED) {
Log.d(
TAG,
"BluetoothSinkAudioPolicy set fallbackDevice="
+ fallbackDevice
+ " active");
setActiveDevice(fallbackDevice);
}
}
}
logScoSessionMetric(
mActiveDevice,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__SCO_TELECOM_INITIATED_END,
Binder.getCallingUid());
}
}
public void clccResponse(
int index, int direction, int status, int mode, boolean mpty, String number, int type) {
mPendingClccResponses.add(
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.SEND_CLCC_RESPONSE,
new HeadsetClccResponse(
index, direction, status, mode, mpty, number, type)));
if (index == CLCC_END_MARK_INDEX) {
doForEachConnectedStateMachine(mPendingClccResponses);
mPendingClccResponses.clear();
}
}
boolean sendVendorSpecificResultCode(BluetoothDevice device, String command, String arg) {
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(
TAG,
"sendVendorSpecificResultCode: device "
+ device
+ " was never connected/connecting");
return false;
}
int connectionState = stateMachine.getConnectionState();
if (connectionState != STATE_CONNECTED) {
return false;
}
// Currently we support only "+ANDROID".
if (!command.equals(BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID)) {
Log.w(TAG, "Disallowed unsolicited result code command: " + command);
return false;
}
stateMachine.sendMessage(
HeadsetStateMachine.SEND_VENDOR_SPECIFIC_RESULT_CODE,
new HeadsetVendorSpecificResultCode(device, command, arg));
}
return true;
}
/**
* Checks if headset devices are able to get inband ringing.
*
* @return True if inband ringing is enabled.
*/
public boolean isInbandRingingEnabled() {
boolean isInbandRingingSupported =
getResources()
.getBoolean(
com.android.bluetooth.R.bool
.config_bluetooth_hfp_inband_ringing_support);
boolean inbandRingtoneAllowedByPolicy = true;
List audioConnectableDevices = getConnectedDevices();
if (audioConnectableDevices.size() == 1) {
BluetoothDevice connectedDevice = audioConnectableDevices.get(0);
BluetoothSinkAudioPolicy callAudioPolicy = getHfpCallAudioPolicy(connectedDevice);
if (callAudioPolicy != null
&& callAudioPolicy.getInBandRingtonePolicy()
== BluetoothSinkAudioPolicy.POLICY_NOT_ALLOWED) {
inbandRingtoneAllowedByPolicy = false;
}
}
return isInbandRingingSupported
&& !SystemProperties.getBoolean(DISABLE_INBAND_RINGING_PROPERTY, false)
&& !mInbandRingingRuntimeDisable
&& inbandRingtoneAllowedByPolicy
&& !isHeadsetClientConnected();
}
private static boolean isHeadsetClientConnected() {
HeadsetClientService headsetClientService = HeadsetClientService.getHeadsetClientService();
if (headsetClientService == null) {
return false;
}
return !(headsetClientService.getConnectedDevices().isEmpty());
}
/**
* Called from {@link HeadsetStateMachine} in state machine thread when there is a connection
* state change
*
* @param device remote device
* @param fromState from which connection state is the change
* @param toState to which connection state is the change
*/
@VisibleForTesting
public void onConnectionStateChangedFromStateMachine(
BluetoothDevice device, int fromState, int toState) {
if (fromState != STATE_CONNECTED && toState == STATE_CONNECTED) {
updateInbandRinging(device, true);
}
if (fromState != STATE_DISCONNECTED && toState == STATE_DISCONNECTED) {
updateInbandRinging(device, false);
if (device.equals(mActiveDevice)) {
setActiveDevice(null);
}
}
mAdapterService
.getActiveDeviceManager()
.profileConnectionStateChanged(
BluetoothProfile.HEADSET, device, fromState, toState);
mAdapterService
.getSilenceDeviceManager()
.hfpConnectionStateChanged(device, fromState, toState);
mAdapterService
.getRemoteDevices()
.handleHeadsetConnectionStateChanged(device, fromState, toState);
mAdapterService.notifyProfileConnectionStateChangeToGatt(
BluetoothProfile.HEADSET, fromState, toState);
mAdapterService.handleProfileConnectionStateChange(
BluetoothProfile.HEADSET, device, fromState, toState);
mAdapterService.updateProfileConnectionAdapterProperties(
device, BluetoothProfile.HEADSET, toState, fromState);
}
/** Called from {@link HeadsetClientStateMachine} to update inband ringing status. */
public void updateInbandRinging(BluetoothDevice device, boolean connected) {
synchronized (mStateMachines) {
final boolean inbandRingingRuntimeDisable = mInbandRingingRuntimeDisable;
if (getConnectedDevices().size() > 1
|| isHeadsetClientConnected()
|| mActiveDevice == null) {
mInbandRingingRuntimeDisable = true;
} else {
mInbandRingingRuntimeDisable = false;
}
final boolean updateAll = inbandRingingRuntimeDisable != mInbandRingingRuntimeDisable;
Log.i(
TAG,
"updateInbandRinging():"
+ " Device="
+ device
+ " ActiveDevice="
+ mActiveDevice
+ " enabled="
+ !mInbandRingingRuntimeDisable
+ " connected="
+ connected
+ " Update all="
+ updateAll);
StateMachineTask sendBsirTask =
stateMachine ->
stateMachine.sendMessage(
HeadsetStateMachine.SEND_BSIR,
mInbandRingingRuntimeDisable ? 0 : 1);
if (updateAll) {
doForEachConnectedStateMachine(sendBsirTask);
} else if (connected) {
// Same Inband ringing status, send +BSIR only to the new connected device
doForStateMachine(device, sendBsirTask);
}
}
}
/**
* Check if no audio mode is active
*
* @return false if virtual call, voice recognition, or Telecom call is active, true if all idle
*/
private boolean isAudioModeIdle() {
synchronized (mStateMachines) {
if (mVoiceRecognitionStarted || mVirtualCallStarted || !mSystemInterface.isCallIdle()) {
Log.i(
TAG,
"isAudioModeIdle: not idle, mVoiceRecognitionStarted="
+ mVoiceRecognitionStarted
+ ", mVirtualCallStarted="
+ mVirtualCallStarted
+ ", isCallIdle="
+ mSystemInterface.isCallIdle());
return false;
}
return true;
}
}
/**
* Check if the device only allows HFP profile as audio profile
*
* @param device Bluetooth device
* @return true if it is a BluetoothDevice with only HFP profile connectable
*/
private boolean isHFPAudioOnly(@NonNull BluetoothDevice device) {
int hfpPolicy =
mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET);
int a2dpPolicy = mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP);
int leAudioPolicy =
mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO);
int ashaPolicy =
mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID);
return hfpPolicy == CONNECTION_POLICY_ALLOWED
&& a2dpPolicy != CONNECTION_POLICY_ALLOWED
&& leAudioPolicy != CONNECTION_POLICY_ALLOWED
&& ashaPolicy != CONNECTION_POLICY_ALLOWED;
}
private boolean shouldCallAudioBeActive() {
return mSystemInterface.isInCall()
|| (mSystemInterface.isRinging() && isInbandRingingEnabled());
}
/**
* Only persist audio during active device switch when call audio is supposed to be active and
* virtual call has not been started. Virtual call is ignored because AudioService and
* applications should reconnect SCO during active device switch and forcing SCO connection here
* will make AudioService think SCO is started externally instead of by one of its SCO clients.
*
* @return true if call audio should be active and no virtual call is going on
*/
private boolean shouldPersistAudio() {
return !mVirtualCallStarted && shouldCallAudioBeActive();
}
/**
* Called from {@link HeadsetStateMachine} in state machine thread when there is a audio
* connection state change
*
* @param device remote device
* @param fromState from which audio connection state is the change
* @param toState to which audio connection state is the change
*/
@VisibleForTesting
public void onAudioStateChangedFromStateMachine(
BluetoothDevice device, int fromState, int toState) {
synchronized (mStateMachines) {
if (toState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
if (fromState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
if (mActiveDevice != null
&& !mActiveDevice.equals(device)
&& shouldPersistAudio()) {
int connectStatus = connectAudio(mActiveDevice);
if (connectStatus != BluetoothStatusCodes.SUCCESS) {
Log.w(
TAG,
"onAudioStateChangedFromStateMachine, failed to connect"
+ " audio to new "
+ "active device "
+ mActiveDevice
+ ", after "
+ device
+ " is disconnected from SCO due to"
+ " status code "
+ connectStatus);
}
}
}
if (mVoiceRecognitionStarted) {
if (!stopVoiceRecognitionByHeadset(device)) {
Log.w(
TAG,
"onAudioStateChangedFromStateMachine: failed to stop voice "
+ "recognition");
}
}
if (mVirtualCallStarted) {
if (!stopScoUsingVirtualVoiceCall()) {
Log.w(
TAG,
"onAudioStateChangedFromStateMachine: failed to stop virtual "
+ "voice call");
}
}
// Resumes LE audio previous active device if HFP handover happened before.
// Do it here because some controllers cannot handle SCO and CIS
// co-existence see {@link LeAudioService#setInactiveForHfpHandover}
LeAudioService leAudioService = mFactory.getLeAudioService();
boolean isLeAudioConnectedDeviceNotActive =
leAudioService != null
&& !leAudioService.getConnectedDevices().isEmpty()
&& leAudioService.getActiveDevices().get(0) == null;
// usually controller limitation cause CONNECTING -> DISCONNECTED, so only
// resume LE audio active device if it is HFP audio only and SCO disconnected
if (fromState != BluetoothHeadset.STATE_AUDIO_CONNECTING
&& isHFPAudioOnly(device)
&& isLeAudioConnectedDeviceNotActive) {
leAudioService.setActiveAfterHfpHandover();
}
// Unsuspend A2DP when SCO connection is gone and call state is idle
if (mSystemInterface.isCallIdle() && !Utils.isScoManagedByAudioEnabled()) {
mSystemInterface.getAudioManager().setA2dpSuspended(false);
if (isAtLeastU()) {
mSystemInterface.getAudioManager().setLeAudioSuspended(false);
}
}
}
}
}
private void broadcastActiveDevice(BluetoothDevice device) {
logD("broadcastActiveDevice: " + device);
mAdapterService.handleActiveDeviceChange(BluetoothProfile.HEADSET, device);
BluetoothStatsLog.write(
BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED,
BluetoothProfile.HEADSET,
mAdapterService.obfuscateAddress(device),
mAdapterService.getMetricId(device));
Intent intent = new Intent(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(
Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
sendBroadcastAsUser(
intent,
UserHandle.ALL,
BLUETOOTH_CONNECT,
Utils.getTempBroadcastOptions().toBundle());
}
/* Notifications of audio device connection/disconnection events. */
private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
if (mSystemInterface.getAudioManager() == null || mAdapterService == null) {
Log.e(TAG, "Callback called when A2dpService is stopped");
return;
}
synchronized (mStateMachines) {
for (AudioDeviceInfo deviceInfo : addedDevices) {
if (deviceInfo.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
continue;
}
String address = deviceInfo.getAddress();
if (address.equals("00:00:00:00:00:00")) {
continue;
}
byte[] addressBytes = Utils.getBytesFromAddress(address);
BluetoothDevice device = mAdapterService.getDeviceFromByte(addressBytes);
Log.d(
TAG,
" onAudioDevicesAdded: "
+ device
+ ", device type: "
+ deviceInfo.getType());
/* Don't expose already exposed active device */
if (device.equals(mExposedActiveDevice)) {
Log.d(TAG, " onAudioDevicesAdded: " + device + " is already exposed");
return;
}
if (!device.equals(mActiveDevice)) {
Log.e(
TAG,
"Added device does not match to the one activated here. ("
+ device
+ " != "
+ mActiveDevice
+ " / "
+ mActiveDevice
+ ")");
continue;
}
mExposedActiveDevice = device;
broadcastActiveDevice(device);
return;
}
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
if (mSystemInterface.getAudioManager() == null || mAdapterService == null) {
Log.e(TAG, "Callback called when LeAudioService is stopped");
return;
}
synchronized (mStateMachines) {
for (AudioDeviceInfo deviceInfo : removedDevices) {
if (deviceInfo.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_SCO) {
continue;
}
String address = deviceInfo.getAddress();
if (address.equals("00:00:00:00:00:00")) {
continue;
}
mExposedActiveDevice = null;
Log.d(
TAG,
" onAudioDevicesRemoved: "
+ address
+ ", device type: "
+ deviceInfo.getType()
+ ", mActiveDevice: "
+ mActiveDevice);
}
}
}
}
/**
* Check whether it is OK to accept a headset connection from a remote device
*
* @param device remote device that initiates the connection
* @return true if the connection is acceptable
*/
public boolean okToAcceptConnection(BluetoothDevice device, boolean isOutgoingRequest) {
// Check if this is an incoming connection in Quiet mode.
if (mAdapterService.isQuietModeEnabled()) {
Log.w(TAG, "okToAcceptConnection: return false as quiet mode enabled");
return false;
}
// Check connection policy and accept or reject the connection.
int connectionPolicy = getConnectionPolicy(device);
if (!Flags.donotValidateBondStateFromProfiles()) {
int bondState = mAdapterService.getBondState(device);
// Allow this connection only if the device is bonded. Any attempt to connect while
// bonding would potentially lead to an unauthorized connection.
if (bondState != BluetoothDevice.BOND_BONDED) {
Log.w(TAG, "okToAcceptConnection: return false, bondState=" + bondState);
return false;
}
}
if (connectionPolicy != CONNECTION_POLICY_UNKNOWN
&& connectionPolicy != CONNECTION_POLICY_ALLOWED) {
// Otherwise, reject the connection if connection policy is not valid.
if (!isOutgoingRequest) {
A2dpService a2dpService = A2dpService.getA2dpService();
if (a2dpService != null && a2dpService.okToConnect(device, true)) {
Log.d(
TAG,
"okToAcceptConnection: return false,"
+ " Fallback connection to allowed A2DP profile");
a2dpService.connect(device);
return false;
}
}
Log.w(TAG, "okToAcceptConnection: return false, connectionPolicy=" + connectionPolicy);
return false;
}
List connectingConnectedDevices =
getDevicesMatchingConnectionStates(CONNECTING_CONNECTED_STATES);
if (connectingConnectedDevices.size() >= mMaxHeadsetConnections) {
Log.w(
TAG,
"Maximum number of connections "
+ mMaxHeadsetConnections
+ " was reached, rejecting connection from "
+ device);
return false;
}
return true;
}
/**
* Checks if SCO should be connected at current system state. Returns {@link
* BluetoothStatusCodes#SUCCESS} if SCO is allowed to be connected or an error code on failure.
*
* @param device device for SCO to be connected
* @return whether SCO can be connected
*/
public int isScoAcceptable(BluetoothDevice device) {
synchronized (mStateMachines) {
if (device == null || !device.equals(mActiveDevice)) {
Log.w(
TAG,
"isScoAcceptable: rejected SCO since "
+ device
+ " is not the current active device "
+ mActiveDevice);
return BluetoothStatusCodes.ERROR_NOT_ACTIVE_DEVICE;
}
if (SystemProperties.getBoolean(REJECT_SCO_IF_HFPC_CONNECTED_PROPERTY, false)
&& isHeadsetClientConnected()) {
Log.w(TAG, "isScoAcceptable: rejected SCO since HFPC is connected!");
return BluetoothStatusCodes.ERROR_AUDIO_ROUTE_BLOCKED;
}
if (mForceScoAudio) {
return BluetoothStatusCodes.SUCCESS;
}
if (!mAudioRouteAllowed) {
Log.w(TAG, "isScoAcceptable: rejected SCO since audio route is not allowed");
return BluetoothStatusCodes.ERROR_AUDIO_ROUTE_BLOCKED;
}
if (mVoiceRecognitionStarted || mVirtualCallStarted) {
return BluetoothStatusCodes.SUCCESS;
}
if (shouldCallAudioBeActive()) {
return BluetoothStatusCodes.SUCCESS;
}
Log.w(
TAG,
"isScoAcceptable: rejected SCO, inCall="
+ mSystemInterface.isInCall()
+ ", voiceRecognition="
+ mVoiceRecognitionStarted
+ ", ringing="
+ mSystemInterface.isRinging()
+ ", inbandRinging="
+ isInbandRingingEnabled()
+ ", isVirtualCallStarted="
+ mVirtualCallStarted);
return BluetoothStatusCodes.ERROR_CALL_ACTIVE;
}
}
/**
* Remove state machine in {@link #mStateMachines} for a {@link BluetoothDevice}
*
* @param device device whose state machine is to be removed.
*/
void removeStateMachine(BluetoothDevice device) {
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "removeStateMachine(), " + device + " does not have a state machine");
return;
}
Log.i(TAG, "removeStateMachine(), removing state machine for device: " + device);
HeadsetObjectsFactory.getInstance().destroyStateMachine(stateMachine);
mStateMachines.remove(device);
}
}
/** Retrieves the most recently connected device in the A2DP connected devices list. */
public BluetoothDevice getFallbackDevice() {
DatabaseManager dbManager = mAdapterService.getDatabase();
return dbManager != null
? dbManager.getMostRecentlyConnectedDevicesInList(getFallbackCandidates(dbManager))
: null;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
List getFallbackCandidates(DatabaseManager dbManager) {
List fallbackCandidates = getConnectedDevices();
List uninterestedCandidates = new ArrayList<>();
for (BluetoothDevice device : fallbackCandidates) {
if (Utils.isWatch(mAdapterService, device)) {
uninterestedCandidates.add(device);
}
}
for (BluetoothDevice device : uninterestedCandidates) {
fallbackCandidates.remove(device);
}
return fallbackCandidates;
}
@Override
public void dump(StringBuilder sb) {
boolean isScoOn = mSystemInterface.getAudioManager().isBluetoothScoOn();
boolean isInbandRingingSupported =
getResources()
.getBoolean(
com.android.bluetooth.R.bool
.config_bluetooth_hfp_inband_ringing_support);
synchronized (mStateMachines) {
super.dump(sb);
ProfileService.println(sb, "mMaxHeadsetConnections: " + mMaxHeadsetConnections);
ProfileService.println(
sb,
"DefaultMaxHeadsetConnections: "
+ mAdapterService.getMaxConnectedAudioDevices());
ProfileService.println(sb, "mActiveDevice: " + mActiveDevice);
ProfileService.println(sb, "isInbandRingingEnabled: " + isInbandRingingEnabled());
ProfileService.println(sb, "isInbandRingingSupported: " + isInbandRingingSupported);
ProfileService.println(
sb, "mInbandRingingRuntimeDisable: " + mInbandRingingRuntimeDisable);
ProfileService.println(sb, "mAudioRouteAllowed: " + mAudioRouteAllowed);
ProfileService.println(sb, "mVoiceRecognitionStarted: " + mVoiceRecognitionStarted);
ProfileService.println(
sb, "mVoiceRecognitionTimeoutEvent: " + mVoiceRecognitionTimeoutEvent);
ProfileService.println(sb, "mVirtualCallStarted: " + mVirtualCallStarted);
ProfileService.println(sb, "mDialingOutTimeoutEvent: " + mDialingOutTimeoutEvent);
ProfileService.println(sb, "mForceScoAudio: " + mForceScoAudio);
ProfileService.println(sb, "AudioManager.isBluetoothScoOn(): " + isScoOn);
ProfileService.println(sb, "Telecom.isInCall(): " + mSystemInterface.isInCall());
ProfileService.println(sb, "Telecom.isRinging(): " + mSystemInterface.isRinging());
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
ProfileService.println(
sb, "==== StateMachine for " + stateMachine.getDevice() + " ====");
stateMachine.dump(sb);
}
}
}
/** Enable SWB Codec. */
void enableSwbCodec(int swbCodec, boolean enable, BluetoothDevice device) {
logD("enableSwbCodec: swbCodec: " + swbCodec + " enable: " + enable + " device: " + device);
boolean result = mNativeInterface.enableSwb(swbCodec, enable, device);
logD("enableSwbCodec result: " + result);
}
/** Check whether AptX SWB Codec is enabled. */
boolean isAptXSwbEnabled() {
logD("mIsAptXSwbEnabled: " + mIsAptXSwbEnabled);
return mIsAptXSwbEnabled;
}
/** Check whether AptX SWB Codec Power Management is enabled. */
boolean isAptXSwbPmEnabled() {
logD("isAptXSwbPmEnabled: " + mIsAptXSwbPmEnabled);
return mIsAptXSwbPmEnabled;
}
private static void logD(String message) {
Log.d(TAG, message);
}
public static void logScoSessionMetric(BluetoothDevice device, int state, int uuid) {
MetricsLogger.getInstance()
.logBluetoothEvent(
device,
BluetoothStatsLog
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__SCO_SESSION,
state,
uuid);
}
}