blob: 8f9adaeb01f19cac6fe1a74210de1d53d59998b8 [file] [log] [blame]
/*
* 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.MODIFY_PHONE_STATE;
import android.annotation.Nullable;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothHeadset;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.HandlerThread;
import android.os.IDeviceIdleController;
import android.os.Looper;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.telecom.PhoneAccount;
import android.util.Log;
import android.util.StatsLog;
import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
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;
/**
* 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";
private static final boolean DBG = false;
private static final String DISABLE_INBAND_RINGING_PROPERTY =
"persist.bluetooth.disableinbandringing";
private static final ParcelUuid[] HEADSET_UUIDS = {BluetoothUuid.HSP, BluetoothUuid.Handsfree};
private static final int[] CONNECTING_CONNECTED_STATES =
{BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED};
private static final int DIALING_OUT_TIMEOUT_MS = 10000;
private int mMaxHeadsetConnections = 1;
private BluetoothDevice mActiveDevice;
private AdapterService mAdapterService;
private HandlerThread mStateMachinesThread;
// This is also used as a lock for shared data in HeadsetService
private final HashMap<BluetoothDevice, HeadsetStateMachine> mStateMachines = new HashMap<>();
private HeadsetNativeInterface mNativeInterface;
private HeadsetSystemInterface mSystemInterface;
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 boolean mStarted;
private boolean mCreated;
private static HeadsetService sHeadsetService;
@Override
public IProfileServiceBinder initBinder() {
return new BluetoothHeadsetBinder(this);
}
@Override
protected void create() {
Log.i(TAG, "create()");
if (mCreated) {
throw new IllegalStateException("create() called twice");
}
mCreated = true;
}
@Override
protected boolean start() {
Log.i(TAG, "start()");
if (mStarted) {
throw new IllegalStateException("start() called twice");
}
// Step 1: Get adapter service, should never be null
mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
"AdapterService cannot be null when HeadsetService starts");
// Step 2: Start handler thread for state machines
mStateMachinesThread = new HandlerThread("HeadsetService.StateMachines");
mStateMachinesThread.start();
// Step 3: Initialize system interface
mSystemInterface = HeadsetObjectsFactory.getInstance().makeSystemInterface(this);
mSystemInterface.init();
// Step 4: Initialize native interface
mMaxHeadsetConnections = mAdapterService.getMaxConnectedAudioDevices();
mNativeInterface = HeadsetObjectsFactory.getInstance().getNativeInterface();
// Add 1 to allow a pending device to be connecting or disconnecting
mNativeInterface.init(mMaxHeadsetConnections + 1, isInbandRingingEnabled());
// Step 5: Check if state machine table is empty, crash if not
if (mStateMachines.size() > 0) {
throw new IllegalStateException(
"start(): mStateMachines is not empty, " + mStateMachines.size()
+ " is already created. Was stop() called properly?");
}
// Step 6: Setup broadcast receivers
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(mHeadsetReceiver, filter);
// Step 7: Mark service as started
setHeadsetService(this);
mStarted = true;
return true;
}
@Override
protected boolean stop() {
Log.i(TAG, "stop()");
if (!mStarted) {
Log.w(TAG, "stop() called before start()");
// Still return true because it is considered "stopped" and doesn't have any functional
// impact on the user
return true;
}
// Step 7: Mark service as stopped
mStarted = false;
setHeadsetService(null);
// Step 6: Tear down broadcast receivers
unregisterReceiver(mHeadsetReceiver);
synchronized (mStateMachines) {
// Reset active device to null
mActiveDevice = null;
mInbandRingingRuntimeDisable = false;
mForceScoAudio = false;
mAudioRouteAllowed = true;
mMaxHeadsetConnections = 1;
mVoiceRecognitionStarted = false;
mVirtualCallStarted = false;
if (mDialingOutTimeoutEvent != null) {
mStateMachinesThread.getThreadHandler().removeCallbacks(mDialingOutTimeoutEvent);
mDialingOutTimeoutEvent = null;
}
if (mVoiceRecognitionTimeoutEvent != null) {
mStateMachinesThread.getThreadHandler()
.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().release();
}
}
// Step 5: Destroy state machines
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
HeadsetObjectsFactory.getInstance().destroyStateMachine(stateMachine);
}
mStateMachines.clear();
}
// Step 4: Destroy native interface
mNativeInterface.cleanup();
// Step 3: Destroy system interface
mSystemInterface.stop();
// Step 2: Stop handler thread
mStateMachinesThread.quitSafely();
mStateMachinesThread = null;
// Step 1: Clear
synchronized (mStateMachines) {
mAdapterService = null;
}
return true;
}
@Override
protected void cleanup() {
Log.i(TAG, "cleanup");
if (!mCreated) {
Log.w(TAG, "cleanup() called before create()");
}
mCreated = 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() && mCreated && mStarted;
}
/**
* 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));
}
}
}
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) {
Objects.requireNonNull(stackEvent.device,
"Device should never be null, event: " + stackEvent);
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,
mStateMachinesThread.getLooper(), 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.VOLUME_CHANGED_ACTION: {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
if (streamType == AudioManager.STREAM_BLUETOOTH_SCO) {
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.wtfStack(TAG, "Cannot find state machine for " + device);
return;
}
stateMachine.sendMessage(
HeadsetStateMachine.INTENT_CONNECTION_ACCESS_REPLY, intent);
}
}
break;
}
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
BluetoothDevice device = Objects.requireNonNull(
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE),
"ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
logD("Bond state changed for device: " + device + " state: " + state);
if (state != BluetoothDevice.BOND_NONE) {
break;
}
synchronized (mStateMachines) {
HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
break;
}
if (stateMachine.getConnectionState()
!= BluetoothProfile.STATE_DISCONNECTED) {
break;
}
removeStateMachine(device);
}
break;
}
default:
Log.w(TAG, "Unknown action " + action);
}
}
};
/**
* Handlers for incoming service calls
*/
private static class BluetoothHeadsetBinder extends IBluetoothHeadset.Stub
implements IProfileServiceBinder {
private volatile HeadsetService mService;
BluetoothHeadsetBinder(HeadsetService svc) {
mService = svc;
}
@Override
public void cleanup() {
mService = null;
}
private HeadsetService getService() {
final HeadsetService service = mService;
if (!Utils.checkCallerAllowManagedProfiles(service)) {
Log.w(TAG, "Headset call not allowed for non-active user");
return null;
}
if (service == null) {
Log.w(TAG, "Service is null");
return null;
}
if (!service.isAlive()) {
Log.w(TAG, "Service is not alive");
return null;
}
return service;
}
@Override
public boolean connect(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.connect(device);
}
@Override
public boolean disconnect(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.disconnect(device);
}
@Override
public List<BluetoothDevice> getConnectedDevices() {
HeadsetService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(0);
}
return service.getConnectedDevices();
}
@Override
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
HeadsetService service = getService();
if (service == null) {
return new ArrayList<BluetoothDevice>(0);
}
return service.getDevicesMatchingConnectionStates(states);
}
@Override
public int getConnectionState(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return service.getConnectionState(device);
}
@Override
public boolean setPriority(BluetoothDevice device, int priority) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.setPriority(device, priority);
}
@Override
public int getPriority(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return BluetoothProfile.PRIORITY_UNDEFINED;
}
return service.getPriority(device);
}
@Override
public boolean startVoiceRecognition(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.startVoiceRecognition(device);
}
@Override
public boolean stopVoiceRecognition(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.stopVoiceRecognition(device);
}
@Override
public boolean isAudioOn() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.isAudioOn();
}
@Override
public boolean isAudioConnected(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.isAudioConnected(device);
}
@Override
public int getAudioState(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
return service.getAudioState(device);
}
@Override
public boolean connectAudio() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.connectAudio();
}
@Override
public boolean disconnectAudio() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.disconnectAudio();
}
@Override
public void setAudioRouteAllowed(boolean allowed) {
HeadsetService service = getService();
if (service == null) {
return;
}
service.setAudioRouteAllowed(allowed);
}
@Override
public boolean getAudioRouteAllowed() {
HeadsetService service = getService();
if (service != null) {
return service.getAudioRouteAllowed();
}
return false;
}
@Override
public void setForceScoAudio(boolean forced) {
HeadsetService service = getService();
if (service == null) {
return;
}
service.setForceScoAudio(forced);
}
@Override
public boolean startScoUsingVirtualVoiceCall() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.startScoUsingVirtualVoiceCall();
}
@Override
public boolean stopScoUsingVirtualVoiceCall() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.stopScoUsingVirtualVoiceCall();
}
@Override
public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
int type, String name) {
HeadsetService service = getService();
if (service == null) {
return;
}
service.phoneStateChanged(numActive, numHeld, callState, number, type, name, false);
}
@Override
public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
String number, int type) {
HeadsetService service = getService();
if (service == null) {
return;
}
service.clccResponse(index, direction, status, mode, mpty, number, type);
}
@Override
public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
String arg) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.sendVendorSpecificResultCode(device, command, arg);
}
@Override
public boolean setActiveDevice(BluetoothDevice device) {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.setActiveDevice(device);
}
@Override
public BluetoothDevice getActiveDevice() {
HeadsetService service = getService();
if (service == null) {
return null;
}
return service.getActiveDevice();
}
@Override
public boolean isInbandRingingEnabled() {
HeadsetService service = getService();
if (service == null) {
return false;
}
return service.isInbandRingingEnabled();
}
}
// 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;
}
logD("getHeadsetService(): returning " + sHeadsetService);
return sHeadsetService;
}
private static synchronized void setHeadsetService(HeadsetService instance) {
logD("setHeadsetService(): set to: " + instance);
sHeadsetService = instance;
}
public boolean connect(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
Log.w(TAG, "connect: PRIORITY_OFF, device=" + device + ", " + Utils.getUidPidString());
return false;
}
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, mStateMachinesThread.getLooper(), this,
mAdapterService, mNativeInterface, mSystemInterface);
mStateMachines.put(device, stateMachine);
}
int connectionState = stateMachine.getConnectionState();
if (connectionState == BluetoothProfile.STATE_CONNECTED
|| connectionState == BluetoothProfile.STATE_CONNECTING) {
Log.w(TAG, "connect: device " + device
+ " is already connected/connecting, connectionState=" + connectionState);
return false;
}
List<BluetoothDevice> 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) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
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 != BluetoothProfile.STATE_CONNECTED
&& connectionState != BluetoothProfile.STATE_CONNECTING) {
Log.w(TAG, "disconnect: device " + device
+ " not connected/connecting, connectionState=" + connectionState);
return false;
}
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT, device);
}
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
ArrayList<BluetoothDevice> devices = new ArrayList<>();
synchronized (mStateMachines) {
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
if (stateMachine.getConnectionState() == BluetoothProfile.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
*/
@VisibleForTesting
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
ArrayList<BluetoothDevice> devices = new ArrayList<>();
synchronized (mStateMachines) {
if (states == null || mAdapterService == 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) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return stateMachine.getConnectionState();
}
}
public boolean setPriority(BluetoothDevice device, int priority) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
Log.i(TAG, "setPriority: device=" + device + ", priority=" + priority + ", "
+ Utils.getUidPidString());
mAdapterService.getDatabase()
.setProfilePriority(device, BluetoothProfile.HEADSET, priority);
return true;
}
public int getPriority(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return mAdapterService.getDatabase()
.getProfilePriority(device, BluetoothProfile.HEADSET);
}
boolean startVoiceRecognition(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
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
boolean 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;
}
if (device == null) {
Log.i(TAG, "device is null, use active device " + mActiveDevice + " instead");
device = mActiveDevice;
}
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;
}
mStateMachinesThread.getThreadHandler()
.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().release();
}
pendingRequestByHeadset = true;
}
if (!Objects.equals(device, 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 != BluetoothProfile.STATE_CONNECTED
&& connectionState != BluetoothProfile.STATE_CONNECTING) {
Log.w(TAG, "startVoiceRecognition: " + device + " is not connected or connecting");
return false;
}
mVoiceRecognitionStarted = true;
if (pendingRequestByHeadset) {
stateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_RESULT,
1 /* success */, 0, device);
} else {
stateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_START, device);
}
stateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO, device);
}
return true;
}
boolean stopVoiceRecognition(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
Log.i(TAG, "stopVoiceRecognition: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
if (!Objects.equals(mActiveDevice, device)) {
Log.w(TAG, "startVoiceRecognition: 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 != BluetoothProfile.STATE_CONNECTED
&& connectionState != BluetoothProfile.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);
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, device);
}
return true;
}
boolean isAudioOn() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return getNonIdleAudioDevices().size() > 0;
}
boolean isAudioConnected(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return false;
}
return stateMachine.getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
}
int getAudioState(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
synchronized (mStateMachines) {
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
return stateMachine.getAudioState();
}
}
public void setAudioRouteAllowed(boolean allowed) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
Log.i(TAG, "setAudioRouteAllowed: allowed=" + allowed + ", " + Utils.getUidPidString());
mAudioRouteAllowed = allowed;
mNativeInterface.setScoAllowed(allowed);
}
public boolean getAudioRouteAllowed() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return mAudioRouteAllowed;
}
public void setForceScoAudio(boolean forced) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
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<HeadsetStateMachine> stateMachines = new ArrayList<>();
synchronized (mStateMachines) {
List<BluetoothDevice> 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;
}
/**
* Set the active device.
*
* @param device the active device
* @return true on success, otherwise false
*/
public boolean setActiveDevice(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
Log.i(TAG, "setActiveDevice: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
if (device == null) {
// Clear the active device
if (mVoiceRecognitionStarted) {
if (!stopVoiceRecognition(mActiveDevice)) {
Log.w(TAG, "setActiveDevice: fail to stopVoiceRecognition from "
+ mActiveDevice);
}
}
if (mVirtualCallStarted) {
if (!stopScoUsingVirtualVoiceCall()) {
Log.w(TAG, "setActiveDevice: fail to stopScoUsingVirtualVoiceCall from "
+ mActiveDevice);
}
}
if (getAudioState(mActiveDevice) != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
if (!disconnectAudio(mActiveDevice)) {
Log.w(TAG, "setActiveDevice: disconnectAudio failed on " + mActiveDevice);
}
}
mActiveDevice = null;
broadcastActiveDevice(null);
return true;
}
if (device.equals(mActiveDevice)) {
Log.i(TAG, "setActiveDevice: device " + device + " is already active");
return true;
}
if (getConnectionState(device) != BluetoothProfile.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 (getAudioState(previousActiveDevice) != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
if (!disconnectAudio(previousActiveDevice)) {
Log.e(TAG, "setActiveDevice: fail to disconnectAudio from "
+ previousActiveDevice);
mActiveDevice = previousActiveDevice;
mNativeInterface.setActiveDevice(previousActiveDevice);
return false;
}
broadcastActiveDevice(mActiveDevice);
} else if (shouldPersistAudio()) {
broadcastActiveDevice(mActiveDevice);
if (!connectAudio(mActiveDevice)) {
Log.e(TAG, "setActiveDevice: fail to connectAudio to " + mActiveDevice);
mActiveDevice = previousActiveDevice;
mNativeInterface.setActiveDevice(previousActiveDevice);
return false;
}
} else {
broadcastActiveDevice(mActiveDevice);
}
}
return true;
}
/**
* Get the active device.
*
* @return the active device or null if no device is active
*/
public BluetoothDevice getActiveDevice() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
synchronized (mStateMachines) {
return mActiveDevice;
}
}
boolean connectAudio() {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
synchronized (mStateMachines) {
BluetoothDevice device = mActiveDevice;
if (device == null) {
Log.w(TAG, "connectAudio: no active device, " + Utils.getUidPidString());
return false;
}
return connectAudio(device);
}
}
boolean connectAudio(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
Log.i(TAG, "connectAudio: device=" + device + ", " + Utils.getUidPidString());
synchronized (mStateMachines) {
if (!isScoAcceptable(device)) {
Log.w(TAG, "connectAudio, rejected SCO request to " + device);
return false;
}
final HeadsetStateMachine stateMachine = mStateMachines.get(device);
if (stateMachine == null) {
Log.w(TAG, "connectAudio: device " + device + " was never connected/connecting");
return false;
}
if (stateMachine.getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
Log.w(TAG, "connectAudio: profile not connected");
return false;
}
if (stateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
logD("connectAudio: audio is not idle for device " + device);
return true;
}
if (isAudioOn()) {
Log.w(TAG, "connectAudio: audio is not idle, current audio devices are "
+ Arrays.toString(getNonIdleAudioDevices().toArray()));
return false;
}
stateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO, device);
}
return true;
}
private List<BluetoothDevice> getNonIdleAudioDevices() {
ArrayList<BluetoothDevice> devices = new ArrayList<>();
synchronized (mStateMachines) {
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
if (stateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
devices.add(stateMachine.getDevice());
}
}
}
return devices;
}
boolean disconnectAudio() {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
boolean result = false;
synchronized (mStateMachines) {
for (BluetoothDevice device : getNonIdleAudioDevices()) {
if (disconnectAudio(device)) {
result = true;
} else {
Log.e(TAG, "disconnectAudio() from " + device + " failed");
}
}
}
if (!result) {
logD("disconnectAudio() no active audio connection");
}
return result;
}
boolean disconnectAudio(BluetoothDevice device) {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
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 false;
}
if (stateMachine.getAudioState() == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.w(TAG, "disconnectAudio, audio is already disconnected for " + device);
return false;
}
stateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, device);
}
return true;
}
boolean isVirtualCallStarted() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
synchronized (mStateMachines) {
return mVirtualCallStarted;
}
}
private boolean startScoUsingVirtualVoiceCall() {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
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
boolean 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;
}
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);
return true;
}
}
boolean stopScoUsingVirtualVoiceCall() {
enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
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);
}
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 (!isOnStateMachineThread()) {
Log.e(TAG, "dialOutgoingCall must be called from state machine thread");
return false;
}
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);
mStateMachinesThread.getThreadHandler()
.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()) {
mSystemInterface.getVoiceRecognitionWakeLock().release();
}
mVoiceRecognitionTimeoutEvent = null;
doForStateMachine(mVoiceRecognitionDevice, stateMachine -> stateMachine.sendMessage(
HeadsetStateMachine.VOICE_RECOGNITION_RESULT, 0 /* fail */, 0,
mVoiceRecognitionDevice));
}
}
@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
boolean 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;
}
IDeviceIdleController deviceIdleController = IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
if (deviceIdleController == null) {
Log.w(TAG, "startVoiceRecognitionByHeadset: deviceIdleController is null, device="
+ fromDevice);
return false;
}
try {
deviceIdleController.exitIdle("voice-command");
} catch (RemoteException e) {
Log.w(TAG,
"startVoiceRecognitionByHeadset: failed to exit idle, device=" + fromDevice
+ ", error=" + e.getMessage());
return false;
}
if (!mSystemInterface.activateVoiceRecognition()) {
Log.w(TAG, "startVoiceRecognitionByHeadset: failed request from " + fromDevice);
return false;
}
mVoiceRecognitionTimeoutEvent = new VoiceRecognitionTimeoutEvent(fromDevice);
mStateMachinesThread.getThreadHandler()
.postDelayed(mVoiceRecognitionTimeoutEvent, sStartVrTimeoutMs);
if (!mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().acquire(sStartVrTimeoutMs);
}
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()) {
mSystemInterface.getVoiceRecognitionWakeLock().release();
}
mStateMachinesThread.getThreadHandler()
.removeCallbacks(mVoiceRecognitionTimeoutEvent);
mVoiceRecognitionTimeoutEvent = null;
}
if (mVoiceRecognitionStarted) {
if (!disconnectAudio()) {
Log.w(TAG, "stopVoiceRecognitionByHeadset: failed to disconnect audio from "
+ fromDevice);
}
mVoiceRecognitionStarted = false;
}
if (!mSystemInterface.deactivateVoiceRecognition()) {
Log.w(TAG, "stopVoiceRecognitionByHeadset: failed request from " + fromDevice);
return false;
}
return true;
}
}
private void phoneStateChanged(int numActive, int numHeld, int callState, String number,
int type, String name, boolean isVirtualCall) {
enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, "Need MODIFY_PHONE_STATE permission");
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) {
mStateMachinesThread.getThreadHandler()
.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 (!mStateMachinesThread.getThreadHandler()
.hasCallbacks(mDialingOutTimeoutEvent)) {
mDialingOutTimeoutEvent = null;
}
}
}
}
mStateMachinesThread.getThreadHandler().post(() -> {
boolean isCallIdleBefore = mSystemInterface.isCallIdle();
mSystemInterface.getHeadsetPhoneState().setNumActiveCall(numActive);
mSystemInterface.getHeadsetPhoneState().setNumHeldCall(numHeld);
mSystemInterface.getHeadsetPhoneState().setCallState(callState);
// Suspend A2DP when call about is about to become active
if (callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED
&& !mSystemInterface.isCallIdle() && isCallIdleBefore) {
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
}
});
doForEachConnectedStateMachine(
stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED,
new HeadsetCallState(numActive, numHeld, callState, number, type, name)));
mStateMachinesThread.getThreadHandler().post(() -> {
if (callState == HeadsetHalConstants.CALL_STATE_IDLE
&& mSystemInterface.isCallIdle() && !isAudioOn()) {
// Resume A2DP when call ended and SCO is not connected
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
}
});
}
private void clccResponse(int index, int direction, int status, int mode, boolean mpty,
String number, int type) {
enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, "Need MODIFY_PHONE_STATE permission");
doForEachConnectedStateMachine(
stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.SEND_CCLC_RESPONSE,
new HeadsetClccResponse(index, direction, status, mode, mpty, number,
type)));
}
private boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
String arg) {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
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 != BluetoothProfile.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;
}
boolean isInbandRingingEnabled() {
enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
return BluetoothHeadset.isInbandRingingSupported(this) && !SystemProperties.getBoolean(
DISABLE_INBAND_RINGING_PROPERTY, false) && !mInbandRingingRuntimeDisable;
}
/**
* 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) {
synchronized (mStateMachines) {
List<BluetoothDevice> audioConnectableDevices =
getDevicesMatchingConnectionStates(CONNECTING_CONNECTED_STATES);
if (fromState != BluetoothProfile.STATE_CONNECTED
&& toState == BluetoothProfile.STATE_CONNECTED) {
if (audioConnectableDevices.size() > 1) {
mInbandRingingRuntimeDisable = true;
doForEachConnectedStateMachine(
stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.SEND_BSIR,
0));
}
MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.HEADSET);
}
if (fromState != BluetoothProfile.STATE_DISCONNECTED
&& toState == BluetoothProfile.STATE_DISCONNECTED) {
if (audioConnectableDevices.size() <= 1) {
mInbandRingingRuntimeDisable = false;
doForEachConnectedStateMachine(
stateMachine -> stateMachine.sendMessage(HeadsetStateMachine.SEND_BSIR,
1));
}
if (device.equals(mActiveDevice)) {
setActiveDevice(null);
}
}
}
}
/**
* 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;
}
}
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()) {
if (!connectAudio(mActiveDevice)) {
Log.w(TAG, "onAudioStateChangedFromStateMachine, failed to connect"
+ " audio to new " + "active device " + mActiveDevice
+ ", after " + device + " is disconnected from SCO");
}
}
}
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");
}
}
// Unsuspend A2DP when SCO connection is gone and call state is idle
if (mSystemInterface.isCallIdle()) {
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
}
}
}
}
private void broadcastActiveDevice(BluetoothDevice device) {
logD("broadcastActiveDevice: " + device);
StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEADSET,
mAdapterService.obfuscateAddress(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, HeadsetService.BLUETOOTH_PERM);
}
/**
* 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) {
// 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 priority and accept or reject the connection.
int priority = getPriority(device);
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;
} else if (priority != BluetoothProfile.PRIORITY_UNDEFINED
&& priority != BluetoothProfile.PRIORITY_ON
&& priority != BluetoothProfile.PRIORITY_AUTO_CONNECT) {
// Otherwise, reject the connection if priority is not valid.
Log.w(TAG, "okToAcceptConnection: return false, priority=" + priority);
return false;
}
List<BluetoothDevice> 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
*
* @param device device for SCO to be connected
* @return true if SCO is allowed to be connected
*/
public boolean 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 false;
}
if (mForceScoAudio) {
return true;
}
if (!mAudioRouteAllowed) {
Log.w(TAG, "isScoAcceptable: rejected SCO since audio route is not allowed");
return false;
}
if (mVoiceRecognitionStarted || mVirtualCallStarted) {
return true;
}
if (shouldCallAudioBeActive()) {
return true;
}
Log.w(TAG, "isScoAcceptable: rejected SCO, inCall=" + mSystemInterface.isInCall()
+ ", voiceRecognition=" + mVoiceRecognitionStarted + ", ringing="
+ mSystemInterface.isRinging() + ", inbandRinging=" + isInbandRingingEnabled()
+ ", isVirtualCallStarted=" + mVirtualCallStarted);
return false;
}
}
/**
* 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);
}
}
private boolean isOnStateMachineThread() {
final Looper myLooper = Looper.myLooper();
return myLooper != null && (mStateMachinesThread != null) && (myLooper.getThread().getId()
== mStateMachinesThread.getId());
}
@Override
public void dump(StringBuilder sb) {
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: " + BluetoothHeadset.isInbandRingingSupported(this));
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, "mCreated: " + mCreated);
ProfileService.println(sb, "mStarted: " + mStarted);
ProfileService.println(sb,
"AudioManager.isBluetoothScoOn(): " + mSystemInterface.getAudioManager()
.isBluetoothScoOn());
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);
}
}
}
private static void logD(String message) {
if (DBG) {
Log.d(TAG, message);
}
}
}