blob: 54f7073b009afe0406ff687272b8712f26f6e5db [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 android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.net.Uri;
import android.os.IDeviceIdleController;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.support.annotation.VisibleForTesting;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Bluetooth Handset StateMachine
* (Disconnected)
* | ^
* CONNECT | | DISCONNECTED
* V |
* (Connecting) (Disconnecting)
* | ^
* CONNECTED | | DISCONNECT
* V |
* (Connected)
* | ^
* CONNECT_AUDIO | | AUDIO_DISCONNECTED
* V |
* (AudioConnecting) (AudioDiconnecting)
* | ^
* AUDIO_CONNECTED | | DISCONNECT_AUDIO
* V |
* (AudioOn)
*/
final class HeadsetStateMachine extends StateMachine {
private static final String TAG = "HeadsetStateMachine";
private static final boolean DBG = false;
private static final String HEADSET_NAME = "bt_headset_name";
private static final String HEADSET_NREC = "bt_headset_nrec";
private static final String HEADSET_WBS = "bt_wbs";
/* Telephone URI scheme */
private static final String SCHEME_TEL = "tel";
static final int CONNECT = 1;
static final int DISCONNECT = 2;
static final int CONNECT_AUDIO = 3;
static final int DISCONNECT_AUDIO = 4;
static final int VOICE_RECOGNITION_START = 5;
static final int VOICE_RECOGNITION_STOP = 6;
// message.obj is an intent AudioManager.VOLUME_CHANGED_ACTION
// EXTRA_VOLUME_STREAM_TYPE is STREAM_BLUETOOTH_SCO
static final int INTENT_SCO_VOLUME_CHANGED = 7;
static final int INTENT_CONNECTION_ACCESS_REPLY = 8;
static final int CALL_STATE_CHANGED = 9;
static final int DEVICE_STATE_CHANGED = 11;
static final int SEND_CCLC_RESPONSE = 12;
static final int SEND_VENDOR_SPECIFIC_RESULT_CODE = 13;
static final int VIRTUAL_CALL_START = 14;
static final int VIRTUAL_CALL_STOP = 15;
static final int STACK_EVENT = 101;
private static final int DIALING_OUT_TIMEOUT = 102;
private static final int START_VR_TIMEOUT = 103;
private static final int CLCC_RSP_TIMEOUT = 104;
private static final int CONNECT_TIMEOUT = 201;
private static final int DIALING_OUT_TIMEOUT_VALUE = 10000;
private static final int START_VR_TIMEOUT_VALUE = 5000;
private static final int CLCC_RSP_TIMEOUT_VALUE = 5000;
// NOTE: the value is not "final" - it is modified in the unit tests
@VisibleForTesting static int sConnectTimeoutMillis = 30000;
private BluetoothDevice mCurrentDevice;
// State machine states
private final Disconnected mDisconnected = new Disconnected();
private final Connecting mConnecting = new Connecting();
private final Disconnecting mDisconnecting = new Disconnecting();
private final Connected mConnected = new Connected();
private final AudioOn mAudioOn = new AudioOn();
private final AudioConnecting mAudioConnecting = new AudioConnecting();
private final AudioDisconnecting mAudioDisconnecting = new AudioDisconnecting();
private HeadsetStateBase mPrevState;
// Run time dependencies
private final HeadsetService mService;
private final HeadsetNativeInterface mNativeInterface;
private final HeadsetSystemInterface mSystemInterface;
private final BluetoothAdapter mAdapter;
// Runtime states
private boolean mVirtualCallStarted;
private boolean mVoiceRecognitionStarted;
private boolean mWaitingForVoiceRecognition;
private boolean mDialingOut;
private int mSpeakerVolume;
private int mMicVolume;
// Indicates whether audio can be routed to the device.
private boolean mAudioRouteAllowed = true;
// Indicates whether SCO audio needs to be forced to open regardless ANY OTHER restrictions
private boolean mForceScoAudio;
// Audio Parameters like NREC
private final HashMap<String, Integer> mAudioParams = new HashMap<>();
// AT Phone book keeps a group of states used by AT+CPBR commands
private final AtPhonebook mPhonebook;
private static final ParcelUuid[] HEADSET_UUIDS = {
BluetoothUuid.HSP, BluetoothUuid.Handsfree,
};
// Keys are AT commands, and values are the company IDs.
private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
// Intent that get sent during voice recognition events.
private static final Intent VOICE_COMMAND_INTENT;
static {
VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID = new HashMap<>();
VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT,
BluetoothAssignedNumbers.PLANTRONICS);
VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID,
BluetoothAssignedNumbers.GOOGLE);
VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XAPL,
BluetoothAssignedNumbers.APPLE);
VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put(
BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV,
BluetoothAssignedNumbers.APPLE);
VOICE_COMMAND_INTENT = new Intent(Intent.ACTION_VOICE_COMMAND);
VOICE_COMMAND_INTENT.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
private HeadsetStateMachine(Looper looper, HeadsetService service,
HeadsetNativeInterface nativeInterface, HeadsetSystemInterface systemInterface) {
super(TAG, looper);
// Enable/Disable StateMachine debug logs
setDbg(DBG);
mService = service;
mNativeInterface = nativeInterface;
mSystemInterface = systemInterface;
// Connect to system services and construct helper objects
mAdapter = BluetoothAdapter.getDefaultAdapter();
mPhonebook = new AtPhonebook(mService, mNativeInterface);
// Initialize state machine
addState(mDisconnected);
addState(mConnecting);
addState(mDisconnecting);
addState(mConnected);
addState(mAudioOn);
addState(mAudioConnecting);
addState(mAudioDisconnecting);
setInitialState(mDisconnected);
}
static HeadsetStateMachine make(Looper looper, HeadsetService service,
HeadsetNativeInterface nativeInterface, HeadsetSystemInterface systemInterface) {
Log.i(TAG, "make");
HeadsetStateMachine stateMachine =
new HeadsetStateMachine(looper, service, nativeInterface, systemInterface);
stateMachine.start();
return stateMachine;
}
static void destroy(HeadsetStateMachine stateMachine) {
Log.i(TAG, "destroy");
if (stateMachine == null) {
Log.w(TAG, "destroy(), stateMachine is null");
return;
}
stateMachine.quitNow();
stateMachine.cleanup();
}
public void cleanup() {
if (mPhonebook != null) {
mPhonebook.cleanup();
}
mAudioParams.clear();
}
public void dump(StringBuilder sb) {
ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
ProfileService.println(sb, "mVirtualCallStarted: " + mVirtualCallStarted);
ProfileService.println(sb, "mVoiceRecognitionStarted: " + mVoiceRecognitionStarted);
ProfileService.println(sb, "mWaitingForVoiceRecognition: " + mWaitingForVoiceRecognition);
ProfileService.println(sb, "mForceScoAudio: " + mForceScoAudio);
ProfileService.println(sb, "mDialingOut: " + mDialingOut);
ProfileService.println(sb, "mAudioRouteAllowed: " + mAudioRouteAllowed);
ProfileService.println(sb, "StateMachine: " + this);
ProfileService.println(sb, "PreviousState: " + mPrevState);
ProfileService.println(sb, "mAudioState: " + getAudioState());
// Dump the state machine logs
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
super.dump(new FileDescriptor(), printWriter, new String[]{});
printWriter.flush();
stringWriter.flush();
ProfileService.println(sb, "StateMachineLog: " + stringWriter.toString());
}
/**
* Base class for states used in this state machine to share common infrastructures
*/
private abstract class HeadsetStateBase extends State {
@Override
public void enter() {
// Crash if current device is null and state is not Disconnected
if (!(this instanceof Disconnected) && mCurrentDevice == null) {
throw new IllegalStateException("mCurrentDevice is null on enter()");
}
// Crash if mPrevState is null and state is not Disconnected
if (!(this instanceof Disconnected) && mPrevState == null) {
throw new IllegalStateException("mPrevState is null on enter()");
}
enforceValidConnectionStateTransition();
}
@Override
public void exit() {
Message message = getCurrentMessage();
if (message != null && !isQuit(message) && mCurrentDevice == null) {
throw new IllegalStateException(
"mCurrentDevice is null on exit() to non-quitting state");
}
mPrevState = this;
}
@Override
public String toString() {
return getName();
}
/**
* Broadcast audio and connection state changes to the system. This should be called at the
* end of enter() method after all the setup is done
*/
void broadcastStateTransitions() {
if (mPrevState == null || mCurrentDevice == null) {
return;
}
// TODO: Add STATE_AUDIO_DISCONNECTING constant to get rid of the 2nd part of this logic
if (getAudioStateInt() != mPrevState.getAudioStateInt() || (
mPrevState instanceof AudioDisconnecting && this instanceof AudioOn)) {
stateLogD("audio state changed: " + mCurrentDevice + ": " + mPrevState + " -> "
+ this);
broadcastAudioState(mCurrentDevice, mPrevState.getAudioStateInt(),
getAudioStateInt());
}
if (getConnectionStateInt() != mPrevState.getConnectionStateInt()) {
stateLogD("connection state changed: " + mCurrentDevice + ": " + mPrevState + " -> "
+ this);
broadcastConnectionState(mCurrentDevice, mPrevState.getConnectionStateInt(),
getConnectionStateInt());
}
}
// Should not be called from enter() method
void broadcastConnectionState(BluetoothDevice device, int fromState, int toState) {
stateLogD("broadcastConnectionState " + device + ": " + fromState + "->" + toState);
if (fromState == BluetoothProfile.STATE_CONNECTED) {
// Headset is disconnecting, stop Virtual call if active.
terminateScoUsingVirtualVoiceCall();
}
mService.connectionStateChanged(device, fromState, toState);
Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
}
// Should not be called from enter() method
void broadcastAudioState(BluetoothDevice device, int fromState, int toState) {
stateLogD("broadcastAudioState: " + device + ": " + fromState + "->" + toState);
if (fromState == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
// When SCO gets disconnected during call transfer, Virtual call
// needs to be cleaned up.So call terminateScoUsingVirtualVoiceCall.
terminateScoUsingVirtualVoiceCall();
}
Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, fromState);
intent.putExtra(BluetoothProfile.EXTRA_STATE, toState);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
}
/**
* Verify if the current state transition is legal. This is supposed to be called from
* enter() method and crash if the state transition is out of the specification
*
* Note:
* This method uses state objects to verify transition because these objects should be final
* and any other instances are invalid
*/
void enforceValidConnectionStateTransition() {
boolean result = false;
if (this == mDisconnected) {
result = mPrevState == null || mPrevState == mConnecting
|| mPrevState == mDisconnecting
// TODO: edges to be removed after native stack refactoring
// all transitions to disconnected state should go through a pending state
// also, states should not go directly from an active audio state to
// disconnected state
|| mPrevState == mConnected || mPrevState == mAudioOn
|| mPrevState == mAudioConnecting || mPrevState == mAudioDisconnecting;
} else if (this == mConnecting) {
result = mPrevState == mDisconnected;
} else if (this == mDisconnecting) {
result = mPrevState == mConnected
// TODO: edges to be removed after native stack refactoring
// all transitions to disconnecting state should go through connected state
|| mPrevState == mAudioConnecting || mPrevState == mAudioOn
|| mPrevState == mAudioDisconnecting;
} else if (this == mConnected) {
result = mPrevState == mConnecting || mPrevState == mAudioDisconnecting
|| mPrevState == mDisconnecting || mPrevState == mAudioConnecting
// TODO: edges to be removed after native stack refactoring
// all transitions to connected state should go through a pending state
|| mPrevState == mAudioOn || mPrevState == mDisconnected;
} else if (this == mAudioConnecting) {
result = mPrevState == mConnected;
} else if (this == mAudioDisconnecting) {
result = mPrevState == mAudioOn;
} else if (this == mAudioOn) {
result = mPrevState == mAudioConnecting || mPrevState == mAudioDisconnecting
// TODO: edges to be removed after native stack refactoring
// all transitions to audio connected state should go through a pending
// state
|| mPrevState == mConnected;
}
if (!result) {
throw new IllegalStateException(
"Invalid state transition from " + mPrevState + " to " + this
+ " for device " + mCurrentDevice);
}
}
void stateLogD(String msg) {
log(getName() + ": " + msg);
}
void stateLogW(String msg) {
logw(getName() + ": " + msg);
}
void stateLogE(String msg) {
loge(getName() + ": " + msg);
}
void stateLogV(String msg) {
logv(getName() + ": " + msg);
}
void stateLogI(String msg) {
logi(getName() + ": " + msg);
}
void stateLogWtfStack(String msg) {
Log.wtfStack(TAG, getName() + ": " + msg);
}
/**
* Process connection event
*
* @param message the current message for the event
* @param state connection state to transition to
* @param device associated device
*/
public abstract void processConnectionEvent(Message message, int state,
BluetoothDevice device);
/**
* Get a state value from {@link BluetoothProfile} that represents the connection state of
* this headset state
*
* @return a value in {@link BluetoothProfile#STATE_DISCONNECTED},
* {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED}, or
* {@link BluetoothProfile#STATE_DISCONNECTING}
*/
abstract int getConnectionStateInt();
/**
* Get an audio state value from {@link BluetoothHeadset}
* @return a value in {@link BluetoothHeadset#STATE_AUDIO_DISCONNECTED},
* {@link BluetoothHeadset#STATE_AUDIO_CONNECTING}, or
* {@link BluetoothHeadset#STATE_AUDIO_CONNECTED}
*/
abstract int getAudioStateInt();
}
class Disconnected extends HeadsetStateBase {
@Override
int getConnectionStateInt() {
return BluetoothProfile.STATE_DISCONNECTED;
}
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
@Override
public void enter() {
super.enter();
mPhonebook.resetAtState();
mSystemInterface.getHeadsetPhoneState().listenForPhoneState(false);
mVoiceRecognitionStarted = false;
mWaitingForVoiceRecognition = false;
mAudioParams.clear();
processWBSEvent(HeadsetHalConstants.BTHF_WBS_NO);
broadcastStateTransitions();
mCurrentDevice = null;
}
@Override
public boolean processMessage(Message message) {
if (mCurrentDevice != null) {
stateLogE("mCurrentDevice is not null");
return NOT_HANDLED;
}
switch (message.what) {
case CONNECT:
BluetoothDevice device = (BluetoothDevice) message.obj;
stateLogD("Connecting to " + device);
if (!mNativeInterface.connectHfp(device)) {
// No state transition is involved, fire broadcast immediately
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
mCurrentDevice = device;
transitionTo(mConnecting);
break;
case DISCONNECT:
// ignore
break;
case CALL_STATE_CHANGED:
processCallState((HeadsetCallState) message.obj, message.arg1 == 1);
break;
case DEVICE_STATE_CHANGED:
stateLogD("Ignoring DEVICE_STATE_CHANGED event");
break;
case STACK_EVENT:
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
switch (event.type) {
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt, event.device);
break;
default:
stateLogE("Unexpected stack event: " + event);
break;
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
// in Disconnected state
@Override
public void processConnectionEvent(Message message, int state, BluetoothDevice device) {
stateLogD("processConnectionEvent, state=" + state + ", device=" + device);
switch (state) {
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED:
stateLogW("ignore DISCONNECTED event, device=" + device);
break;
// Both events result in Connecting state as SLC establishment is still required
case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
case HeadsetHalConstants.CONNECTION_STATE_CONNECTING:
if (okToAcceptConnection(device)) {
stateLogI("connected/connecting incoming HF, device=" + device);
mCurrentDevice = device;
transitionTo(mConnecting);
} else {
stateLogI("rejected incoming HF, priority=" + mService.getPriority(device)
+ " bondState=" + device.getBondState() + ", device=" + device);
// Reject the connection and stay in Disconnected state itself
if (!mNativeInterface.disconnectHfp(device)) {
stateLogE("Failed to disconnect from " + device);
}
// Indicate rejection to other components.
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
}
break;
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING:
stateLogW("Ignore DISCONNECTING event, device=" + device);
break;
default:
stateLogE("Incorrect state: " + state);
break;
}
}
}
// Per HFP 1.7.1 spec page 23/144, Pending state needs to handle
// AT+BRSF, AT+CIND, AT+CMER, AT+BIND, +CHLD
// commands during SLC establishment
class Connecting extends HeadsetStateBase {
@Override
int getConnectionStateInt() {
return BluetoothProfile.STATE_CONNECTING;
}
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
@Override
public void enter() {
super.enter();
sendMessageDelayed(CONNECT_TIMEOUT, mCurrentDevice, sConnectTimeoutMillis);
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT:
case CONNECT_AUDIO:
case DISCONNECT:
deferMessage(message);
break;
case CONNECT_TIMEOUT:
// We timed out trying to connect, transition to Disconnected state
stateLogW("Connection timeout for " + mCurrentDevice);
transitionTo(mDisconnected);
break;
case CALL_STATE_CHANGED:
processCallState((HeadsetCallState) message.obj, message.arg1 == 1);
break;
case DEVICE_STATE_CHANGED:
stateLogD("ignoring DEVICE_STATE_CHANGED event");
break;
case STACK_EVENT:
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
switch (event.type) {
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CHLD:
processAtChld(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CIND:
processAtCind(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_WBS:
processWBSEvent(event.valueInt);
break;
case HeadsetStackEvent.EVENT_TYPE_BIND:
processAtBind(event.valueString, event.device);
break;
// Unexpected AT commands, we only handle them for comparability reasons
case HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED:
stateLogW("Unexpected VR event, device=" + event.device + ", state="
+ event.valueInt);
processVrEvent(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_DIAL_CALL:
stateLogW("Unexpected dial event, device=" + event.device);
processDialCall(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST:
stateLogW("Unexpected subscriber number event for" + event.device
+ ", state=" + event.valueInt);
processSubscriberNumberRequest(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_COPS:
stateLogW("Unexpected COPS event for " + event.device);
processAtCops(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CLCC:
Log.w(TAG, "Connecting: Unexpected CLCC event for" + event.device);
processAtClcc(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT:
stateLogW("Unexpected unknown AT event for" + event.device + ", cmd="
+ event.valueString);
processUnknownAt(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_KEY_PRESSED:
stateLogW("Unexpected key-press event for " + event.device);
processKeyPressed(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_BIEV:
stateLogW("Unexpected BIEV event for " + event.device + ", indId="
+ event.valueInt + ", indVal=" + event.valueInt2);
processAtBiev(event.valueInt, event.valueInt2, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_VOLUME_CHANGED:
stateLogW("Unexpected volume event for " + event.device);
processVolumeEvent(event.valueInt, event.valueInt2, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_ANSWER_CALL:
stateLogW("Unexpected answer event for " + event.device);
mSystemInterface.answerCall(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_HANGUP_CALL:
stateLogW("Unexpected hangup event for " + event.device);
mSystemInterface.hangupCall(event.device, isVirtualCallInProgress());
break;
default:
stateLogE("Unexpected event: " + event);
break;
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void processConnectionEvent(Message message, int state, BluetoothDevice device) {
stateLogD("processConnectionEvent, state=" + state + ", device=" + device);
switch (state) {
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED:
if (!mCurrentDevice.equals(device)) {
stateLogW("Unknown device disconnected" + device);
break;
}
transitionTo(mDisconnected);
break;
case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
stateLogD("RFCOMM connected for " + device);
if (!mCurrentDevice.equals(device)) {
stateLogW("Reject connection from unknown device " + device);
if (!mNativeInterface.disconnectHfp(device)) {
stateLogE("Disconnect from " + device + " failed");
}
}
break;
case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED:
stateLogD("SLC connected for " + device);
if (!mCurrentDevice.equals(device)) {
stateLogW("Reject SLC from unknown device " + device);
if (!mNativeInterface.disconnectHfp(device)) {
stateLogE("Disconnect SLC from " + device + " failed");
}
break;
}
configAudioParameters(device);
mSystemInterface.queryPhoneState();
transitionTo(mConnected);
break;
case HeadsetHalConstants.CONNECTION_STATE_CONNECTING:
// Ignored
break;
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING:
stateLogD("Disconnecting for " + device);
if (mCurrentDevice.equals(device)) {
stateLogW("Current device disconnecting");
// ignored, wait for it to be disconnected
}
break;
default:
stateLogE("Incorrect state " + state);
break;
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
super.exit();
}
}
class Disconnecting extends HeadsetStateBase {
@Override
int getConnectionStateInt() {
return BluetoothProfile.STATE_DISCONNECTING;
}
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
@Override
public void enter() {
super.enter();
sendMessageDelayed(CONNECT_TIMEOUT, mCurrentDevice, sConnectTimeoutMillis);
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT:
case CONNECT_AUDIO:
case DISCONNECT:
deferMessage(message);
break;
case CONNECT_TIMEOUT:
stateLogE("timeout");
transitionTo(mDisconnected);
break;
case STACK_EVENT:
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
switch (event.type) {
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt, event.device);
break;
default:
stateLogE("Unexpected event: " + event);
break;
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
// in Disconnecting state
@Override
public void processConnectionEvent(Message message, int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
stateLogW("processConnectionEvent, unknown device " + device);
return;
}
switch (state) {
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED:
stateLogD("Device disconnected, device=" + device);
transitionTo(mDisconnected);
break;
case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED:
stateLogD("Device connected, device=" + device);
transitionTo(mConnected);
break;
default:
stateLogE("Device: " + device + " bad state: " + state);
break;
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
super.exit();
}
}
/**
* Base class for Connected, AudioConnecting, AudioOn, AudioDisconnecting states
*/
private abstract class ConnectedBase extends HeadsetStateBase {
@Override
int getConnectionStateInt() {
return BluetoothProfile.STATE_CONNECTED;
}
/**
* Handle common messages in connected states. However, state specific messages must be
* handled individually.
*
* @param message Incoming message to handle
* @return True if handled successfully, False otherwise
*/
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT:
case DISCONNECT:
case CONNECT_AUDIO:
case DISCONNECT_AUDIO:
case CONNECT_TIMEOUT:
stateLogWtfStack("Illegal message in generic handler: " + message);
break;
case VOICE_RECOGNITION_START:
processLocalVrEvent(HeadsetHalConstants.VR_STATE_STARTED,
(BluetoothDevice) message.obj);
break;
case VOICE_RECOGNITION_STOP:
processLocalVrEvent(HeadsetHalConstants.VR_STATE_STOPPED,
(BluetoothDevice) message.obj);
break;
case CALL_STATE_CHANGED:
processCallState((HeadsetCallState) message.obj, message.arg1 == 1);
break;
case DEVICE_STATE_CHANGED:
processDeviceStateChanged((HeadsetDeviceState) message.obj);
break;
case SEND_CCLC_RESPONSE:
processSendClccResponse((HeadsetClccResponse) message.obj);
break;
case CLCC_RSP_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
mNativeInterface.clccResponse(device, 0, 0, 0, 0, false, "", 0);
}
break;
case SEND_VENDOR_SPECIFIC_RESULT_CODE:
processSendVendorSpecificResultCode(
(HeadsetVendorSpecificResultCode) message.obj);
break;
case DIALING_OUT_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (mDialingOut) {
mDialingOut = false;
mNativeInterface.atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
break;
case VIRTUAL_CALL_START:
initiateScoUsingVirtualVoiceCall();
break;
case VIRTUAL_CALL_STOP:
terminateScoUsingVirtualVoiceCall();
break;
case START_VR_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (mWaitingForVoiceRecognition) {
device = (BluetoothDevice) message.obj;
mWaitingForVoiceRecognition = false;
stateLogE("Timeout waiting for voice recognition to start");
mNativeInterface.atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
break;
case INTENT_CONNECTION_ACCESS_REPLY:
handleAccessPermissionResult((Intent) message.obj);
break;
case STACK_EVENT:
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
switch (event.type) {
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
processAudioEvent(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED:
processVrEvent(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_ANSWER_CALL:
mSystemInterface.answerCall(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_HANGUP_CALL:
mSystemInterface.hangupCall(event.device, mVirtualCallStarted);
break;
case HeadsetStackEvent.EVENT_TYPE_VOLUME_CHANGED:
processVolumeEvent(event.valueInt, event.valueInt2, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_DIAL_CALL:
processDialCall(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_SEND_DTMF:
mSystemInterface.sendDtmf(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_NOICE_REDUCTION:
processNoiseReductionEvent(event.valueInt == 1, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_WBS:
processWBSEvent(event.valueInt);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CHLD:
processAtChld(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST:
processSubscriberNumberRequest(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CIND:
processAtCind(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_COPS:
processAtCops(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CLCC:
processAtClcc(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT:
processUnknownAt(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_KEY_PRESSED:
processKeyPressed(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_BIND:
processAtBind(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_BIEV:
processAtBiev(event.valueInt, event.valueInt2, event.device);
break;
default:
stateLogE("Unknown stack event: " + event);
break;
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
@Override
public void processConnectionEvent(Message message, int state, BluetoothDevice device) {
stateLogD("processConnectionEvent, state=" + state + ", device=" + device);
switch (state) {
case HeadsetHalConstants.CONNECTION_STATE_CONNECTED:
if (mCurrentDevice.equals(device)) {
stateLogE("Same device connect RFCOMM again, should never happen");
break;
}
// reject the connection and stay in Connected state itself
stateLogI("Incoming Hf rejected. priority=" + mService.getPriority(device)
+ " bondState=" + device.getBondState());
if (!mNativeInterface.disconnectHfp(device)) {
stateLogW("Fail to disconnect " + device);
break;
}
break;
case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED:
// Should have been rejected in CONNECTION_STATE_CONNECTED
if (mCurrentDevice.equals(device)) {
stateLogE("Same device connected SLC again");
break;
}
// reject the connection and stay in Connected state itself
stateLogI("Incoming Hf SLC rejected. priority=" + mService.getPriority(device)
+ " bondState=" + device.getBondState());
if (!mNativeInterface.disconnectHfp(device)) {
stateLogW("Fail to disconnect " + device);
break;
}
break;
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING:
if (!mCurrentDevice.equals(device)) {
stateLogW("Unknown device disconnecting, device=" + device);
break;
}
stateLogI("Current device disconnecting " + mCurrentDevice);
transitionTo(mDisconnecting);
break;
case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED:
if (!mCurrentDevice.equals(device)) {
stateLogW("Unknown device disconnected " + device);
break;
}
stateLogI("Current device disconnected " + mCurrentDevice);
transitionTo(mDisconnected);
break;
default:
stateLogE("Connection State Device: " + device + " bad state: " + state);
break;
}
}
/**
* Each state should handle audio events differently
*
* @param state audio state
* @param device associated device
*/
public abstract void processAudioEvent(int state, BluetoothDevice device);
}
class Connected extends ConnectedBase {
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
@Override
public void enter() {
super.enter();
// start phone state listener here so that the CIND response as part of SLC can be
// responded to, correctly.
// listenForPhoneState(boolean) internally handles multiple calls to start listen
mSystemInterface.getHeadsetPhoneState().listenForPhoneState(true);
if (mPrevState == mConnecting) {
// Remove pending connection attempts that were deferred during the pending
// state. This is to prevent auto connect attempts from disconnecting
// devices that previously successfully connected.
removeDeferredMessages(CONNECT);
}
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
stateLogI("CONNECT, device " + device);
if (mCurrentDevice.equals(device)) {
stateLogW("CONNECT, device " + device + " is already connected");
break;
}
stateLogD("CONNECT, disconnect current device " + mCurrentDevice);
if (!mNativeInterface.disconnectHfp(mCurrentDevice)) {
stateLogW("CONNECT, Failed to disconnect " + mCurrentDevice);
// broadcast immediately as no state transition is involved
// TODO: to be removed with multi-HFP implementation
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
// Defer connect message to future state
deferMessage(message);
transitionTo(mDisconnecting);
}
break;
case DISCONNECT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
stateLogD("DISCONNECT from device=" + device);
if (!mCurrentDevice.equals(device)) {
stateLogW("DISCONNECT, device " + device + " not connected");
break;
}
if (!mNativeInterface.disconnectHfp(device)) {
// broadcast immediately as no state transition is involved
stateLogE("DISCONNECT from " + device + " failed");
broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTED);
break;
}
transitionTo(mDisconnecting);
}
break;
case CONNECT_AUDIO:
stateLogD("CONNECT_AUDIO, device=" + mCurrentDevice);
if (!isScoAcceptable()) {
stateLogW("CONNECT_AUDIO No Active/Held call, no call setup, and no "
+ "in-band ringing, not allowing SCO, device=" + mCurrentDevice);
break;
}
if (!mNativeInterface.connectAudio(mCurrentDevice)) {
stateLogE("Failed to connect SCO audio for " + mCurrentDevice);
// No state change involved, fire broadcast immediately
broadcastAudioState(mCurrentDevice,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
break;
}
transitionTo(mAudioConnecting);
break;
case DISCONNECT_AUDIO:
stateLogD("DISCONNECT_AUDIO, device=" + mCurrentDevice);
// ignore
break;
default:
return super.processMessage(message);
}
return HANDLED;
}
@Override
public void processAudioEvent(int state, BluetoothDevice device) {
stateLogD("processAudioEvent, state=" + state + ", device=" + device);
if (!mCurrentDevice.equals(device)) {
// Crash if audio is connected for unknown device
stateLogWtfStack("Audio changed on unknown device: " + device);
return;
}
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
if (!isScoAcceptable()) {
stateLogW("Rejecting incoming audio connection from " + device);
if (!mNativeInterface.disconnectAudio(device)) {
stateLogE("Fail to disconnect audio for " + device);
}
break;
}
stateLogI("Audio connected for " + device);
transitionTo(mAudioOn);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
if (!isScoAcceptable()) {
stateLogW("Rejecting incoming pending audio connection from " + device);
if (!mNativeInterface.disconnectAudio(device)) {
stateLogE("Fail to disconnect audio for " + device);
}
break;
}
stateLogI("Audio connecting for " + device);
transitionTo(mAudioConnecting);
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
// ignore
break;
default:
stateLogE("Audio State Device: " + device + " bad state: " + state);
break;
}
}
}
class AudioConnecting extends ConnectedBase {
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_CONNECTING;
}
@Override
public void enter() {
super.enter();
sendMessageDelayed(CONNECT_TIMEOUT, mCurrentDevice, sConnectTimeoutMillis);
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT:
case DISCONNECT:
case CONNECT_AUDIO:
case DISCONNECT_AUDIO:
deferMessage(message);
break;
case CONNECT_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(device)) {
stateLogW("CONNECT_TIMEOUT for unknown device " + device);
break;
}
stateLogW("CONNECT_TIMEOUT");
transitionTo(mConnected);
break;
}
default:
return super.processMessage(message);
}
return HANDLED;
}
@Override
public void processAudioEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
// Crash on unknown device audio state change
stateLogWtfStack("Audio state changed on unknown device: " + device);
return;
}
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
stateLogW("Audio connection failed");
transitionTo(mConnected);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
// ignore, already in audio connecting state
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
// ignore, there is no BluetoothHeadset.STATE_AUDIO_DISCONNECTING
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
stateLogI("Audio connected for device " + device);
transitionTo(mAudioOn);
break;
default:
stateLogE("Audio State Device: " + device + " bad state: " + state);
break;
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
super.exit();
}
}
class AudioOn extends ConnectedBase {
@Override
int getAudioStateInt() {
return BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
@Override
public void enter() {
super.enter();
removeDeferredMessages(CONNECT_AUDIO);
setAudioParameters(mCurrentDevice);
mSystemInterface.getAudioManager().setBluetoothScoOn(true);
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
stateLogD("CONNECT, device=" + device);
if (mCurrentDevice.equals(device)) {
stateLogW("CONNECT, device " + device + " is connected");
break;
}
// When connecting separate device, disconnect the current one first
// Disconnect audio and then disconnect SLC
stateLogD("Disconnecting SCO, device=" + mCurrentDevice);
if (!mNativeInterface.disconnectAudio(mCurrentDevice)) {
stateLogE("Disconnect SCO failed, device=" + mCurrentDevice
+ ", abort connection to " + device);
broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTED);
break;
}
deferMessage(message);
transitionTo(mAudioDisconnecting);
break;
}
case DISCONNECT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
stateLogD("DISCONNECT, device=" + device);
if (!mCurrentDevice.equals(device)) {
stateLogW("DISCONNECT, device " + device + " not connected");
break;
}
// Disconnect BT SCO first
if (!mNativeInterface.disconnectAudio(mCurrentDevice)) {
stateLogW("DISCONNECT failed, device=" + mCurrentDevice);
// if disconnect BT SCO failed, transition to mConnected state to force
// disconnect device
}
deferMessage(obtainMessage(DISCONNECT, mCurrentDevice));
transitionTo(mConnected);
break;
}
case CONNECT_AUDIO: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(device)) {
stateLogW("CONNECT_AUDIO device is not connected " + device);
break;
}
stateLogW("CONNECT_AUDIO device auido is already connected " + device);
break;
}
case DISCONNECT_AUDIO:
if (mNativeInterface.disconnectAudio(mCurrentDevice)) {
stateLogD("DISCONNECT_AUDIO, device=" + mCurrentDevice);
transitionTo(mAudioDisconnecting);
} else {
stateLogW("DISCONNECT_AUDIO failed, device=" + mCurrentDevice);
}
break;
case VOICE_RECOGNITION_START:
processLocalVrEvent(HeadsetHalConstants.VR_STATE_STARTED,
(BluetoothDevice) message.obj);
break;
case VOICE_RECOGNITION_STOP:
processLocalVrEvent(HeadsetHalConstants.VR_STATE_STOPPED,
(BluetoothDevice) message.obj);
break;
case INTENT_SCO_VOLUME_CHANGED:
processIntentScoVolume((Intent) message.obj, mCurrentDevice);
break;
case CALL_STATE_CHANGED:
processCallState((HeadsetCallState) message.obj, message.arg1 == 1);
break;
case DEVICE_STATE_CHANGED:
processDeviceStateChanged((HeadsetDeviceState) message.obj);
break;
case SEND_CCLC_RESPONSE:
processSendClccResponse((HeadsetClccResponse) message.obj);
break;
case CLCC_RSP_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
mNativeInterface.clccResponse(device, 0, 0, 0, 0, false, "", 0);
break;
}
case SEND_VENDOR_SPECIFIC_RESULT_CODE:
processSendVendorSpecificResultCode(
(HeadsetVendorSpecificResultCode) message.obj);
break;
case VIRTUAL_CALL_START:
initiateScoUsingVirtualVoiceCall();
break;
case VIRTUAL_CALL_STOP:
terminateScoUsingVirtualVoiceCall();
break;
case DIALING_OUT_TIMEOUT: {
if (mDialingOut) {
BluetoothDevice device = (BluetoothDevice) message.obj;
mDialingOut = false;
mNativeInterface.atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
break;
}
case START_VR_TIMEOUT: {
if (mWaitingForVoiceRecognition) {
BluetoothDevice device = (BluetoothDevice) message.obj;
mWaitingForVoiceRecognition = false;
stateLogE("Timeout waiting for voice recognition to start");
mNativeInterface.atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
break;
}
case STACK_EVENT:
HeadsetStackEvent event = (HeadsetStackEvent) message.obj;
stateLogD("STACK_EVENT: " + event);
switch (event.type) {
case HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
processConnectionEvent(message, event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
processAudioEvent(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_VR_STATE_CHANGED:
processVrEvent(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_ANSWER_CALL:
mSystemInterface.answerCall(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_HANGUP_CALL:
mSystemInterface.hangupCall(event.device, mVirtualCallStarted);
break;
case HeadsetStackEvent.EVENT_TYPE_VOLUME_CHANGED:
processVolumeEvent(event.valueInt, event.valueInt2, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_DIAL_CALL:
processDialCall(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_SEND_DTMF:
mSystemInterface.sendDtmf(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_NOICE_REDUCTION:
processNoiseReductionEvent(event.valueInt == 1, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CHLD:
processAtChld(event.valueInt, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST:
processSubscriberNumberRequest(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CIND:
processAtCind(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_COPS:
processAtCops(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_AT_CLCC:
processAtClcc(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_UNKNOWN_AT:
processUnknownAt(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_KEY_PRESSED:
processKeyPressed(event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_BIND:
processAtBind(event.valueString, event.device);
break;
case HeadsetStackEvent.EVENT_TYPE_BIEV:
processAtBiev(event.valueInt, event.valueInt2, event.device);
break;
default:
stateLogE("Unknown stack event: " + event);
break;
}
break;
default:
stateLogE("Unexpected msg " + getMessageName(message.what) + ": " + message);
return NOT_HANDLED;
}
return HANDLED;
}
// in AudioOn state
@Override
public void processAudioEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
stateLogE("Audio changed on unknown device: " + device);
return;
}
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
stateLogI("Audio disconnected by remote");
transitionTo(mConnected);
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
stateLogI("Audio being disconnected by remote");
transitionTo(mAudioDisconnecting);
break;
default:
stateLogE("Audio State Device: " + device + " bad state: " + state);
break;
}
}
private void processIntentScoVolume(Intent intent, BluetoothDevice device) {
int volumeValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
if (mSpeakerVolume != volumeValue) {
mSpeakerVolume = volumeValue;
mNativeInterface.setVolume(device, HeadsetHalConstants.VOLUME_TYPE_SPK,
mSpeakerVolume);
}
}
@Override
public void exit() {
mSystemInterface.getAudioManager().setBluetoothScoOn(false);
super.exit();
}
}
class AudioDisconnecting extends ConnectedBase {
@Override
int getAudioStateInt() {
// TODO: need BluetoothHeadset.STATE_AUDIO_DISCONNECTING
return BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
@Override
public void enter() {
super.enter();
sendMessageDelayed(CONNECT_TIMEOUT, mCurrentDevice, sConnectTimeoutMillis);
broadcastStateTransitions();
}
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CONNECT:
case DISCONNECT:
case CONNECT_AUDIO:
case DISCONNECT_AUDIO:
deferMessage(message);
break;
case CONNECT_TIMEOUT: {
BluetoothDevice device = (BluetoothDevice) message.obj;
if (!mCurrentDevice.equals(device)) {
stateLogW("CONNECT_TIMEOUT for unknown device " + device);
break;
}
stateLogW("CONNECT_TIMEOUT");
transitionTo(mConnected);
break;
}
default:
return super.processMessage(message);
}
return HANDLED;
}
@Override
public void processAudioEvent(int state, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
// Crash if audio state change happen for unknown device
stateLogWtfStack("Audio changed on unknown device: " + device);
return;
}
switch (state) {
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED:
stateLogI("Audio disconnected for " + device);
transitionTo(mConnected);
break;
case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING:
// ignore
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
stateLogW("Audio disconnection failed for " + device);
transitionTo(mAudioOn);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
// ignore, see if it goes into connected state, otherwise, timeout
break;
default:
stateLogE("Audio State Device: " + device + " bad state: " + state);
break;
}
}
@Override
public void exit() {
removeMessages(CONNECT_TIMEOUT);
super.exit();
}
}
synchronized BluetoothDevice getCurrentDevice() {
return mCurrentDevice;
}
synchronized int getConnectionState(BluetoothDevice device) {
if (mCurrentDevice == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
if (!mCurrentDevice.equals(device)) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return ((HeadsetStateBase) getCurrentState()).getConnectionStateInt();
}
List<BluetoothDevice> getConnectedDevices() {
List<BluetoothDevice> devices = new ArrayList<>();
synchronized (this) {
if (getCurrentState() instanceof ConnectedBase) {
devices.add(mCurrentDevice);
}
}
return devices;
}
void setAudioRouteAllowed(boolean allowed) {
mAudioRouteAllowed = allowed;
mNativeInterface.setScoAllowed(allowed);
}
boolean getAudioRouteAllowed() {
return mAudioRouteAllowed;
}
void setForceScoAudio(boolean forced) {
mForceScoAudio = forced;
}
synchronized int getAudioState() {
return ((HeadsetStateBase) getCurrentState()).getAudioStateInt();
}
private void processVrEvent(int state, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processVrEvent device is null");
return;
}
Log.d(TAG, "processVrEvent: state=" + state + " mVoiceRecognitionStarted: "
+ mVoiceRecognitionStarted + " mWaitingforVoiceRecognition: "
+ mWaitingForVoiceRecognition + " isInCall: " + isInCall());
if (state == HeadsetHalConstants.VR_STATE_STARTED) {
if (!isVirtualCallInProgress() && !isInCall()) {
IDeviceIdleController dic = IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
if (dic != null) {
try {
dic.exitIdle("voice-command");
} catch (RemoteException e) {
}
}
try {
mService.startActivity(VOICE_COMMAND_INTENT);
} catch (ActivityNotFoundException e) {
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR,
0);
return;
}
expectVoiceRecognition(device);
} else {
// send error response if call is ongoing
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
} else if (state == HeadsetHalConstants.VR_STATE_STOPPED) {
if (mVoiceRecognitionStarted || mWaitingForVoiceRecognition) {
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
mVoiceRecognitionStarted = false;
mWaitingForVoiceRecognition = false;
if (!isInCall() && (getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED)) {
mNativeInterface.disconnectAudio(mCurrentDevice);
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
}
} else {
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
} else {
Log.e(TAG, "Bad Voice Recognition state: " + state);
}
}
private void processLocalVrEvent(int state, BluetoothDevice device1) {
BluetoothDevice device = null;
if (state == HeadsetHalConstants.VR_STATE_STARTED) {
boolean needAudio = true;
if (mVoiceRecognitionStarted || isInCall()) {
Log.e(TAG, "Voice recognition started when call is active. isInCall:" + isInCall()
+ " mVoiceRecognitionStarted: " + mVoiceRecognitionStarted);
return;
}
mVoiceRecognitionStarted = true;
if (mWaitingForVoiceRecognition) {
device = getDeviceForMessage(START_VR_TIMEOUT);
if (device == null) {
return;
}
Log.d(TAG, "Voice recognition started successfully");
mWaitingForVoiceRecognition = false;
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
removeMessages(START_VR_TIMEOUT);
} else {
Log.d(TAG, "Voice recognition started locally");
needAudio = mNativeInterface.startVoiceRecognition(mCurrentDevice);
if (mCurrentDevice != null) {
device = mCurrentDevice;
}
}
if (needAudio && getAudioState() == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
Log.d(TAG, "Initiating audio connection for Voice Recognition");
// At this stage, we need to be sure that AVDTP is not streaming. This is needed
// to be compliant with the AV+HFP Whitepaper as we cannot have A2DP in
// streaming state while a SCO connection is established.
// This is needed for VoiceDial scenario alone and not for
// incoming call/outgoing call scenarios as the phone enters MODE_RINGTONE
// or MODE_IN_CALL which shall automatically suspend the AVDTP stream if needed.
// Whereas for VoiceDial we want to activate the SCO connection but we are still
// in MODE_NORMAL and hence the need to explicitly suspend the A2DP stream
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
if (device != null) {
mNativeInterface.connectAudio(device);
} else {
Log.e(TAG, "device not found for VR");
}
}
if (mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().release();
}
} else {
Log.d(TAG, "Voice Recognition stopped. mVoiceRecognitionStarted: "
+ mVoiceRecognitionStarted + " mWaitingForVoiceRecognition: "
+ mWaitingForVoiceRecognition);
if (mVoiceRecognitionStarted || mWaitingForVoiceRecognition) {
mVoiceRecognitionStarted = false;
mWaitingForVoiceRecognition = false;
if (mNativeInterface.stopVoiceRecognition(mCurrentDevice) && !isInCall()
&& getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
mNativeInterface.disconnectAudio(mCurrentDevice);
mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
}
}
}
}
private synchronized void expectVoiceRecognition(BluetoothDevice device) {
mWaitingForVoiceRecognition = true;
Message m = obtainMessage(START_VR_TIMEOUT);
m.obj = getMatchingDevice(device);
sendMessageDelayed(m, START_VR_TIMEOUT_VALUE);
if (!mSystemInterface.getVoiceRecognitionWakeLock().isHeld()) {
mSystemInterface.getVoiceRecognitionWakeLock().acquire(START_VR_TIMEOUT_VALUE);
}
}
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
if (bondedDevices == null) {
return deviceList;
}
synchronized (this) {
for (BluetoothDevice device : bondedDevices) {
ParcelUuid[] featureUuids = device.getUuids();
if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) {
continue;
}
int connectionState = getConnectionState(device);
for (int state : states) {
if (connectionState == state) {
deviceList.add(device);
}
}
}
}
return deviceList;
}
private BluetoothDevice getDeviceForMessage(int what) {
if (what == CONNECT_TIMEOUT) {
log("getDeviceForMessage: returning mTargetDevice for what=" + what);
return mCurrentDevice;
}
if (mCurrentDevice == null) {
log("getDeviceForMessage: No connected device. what=" + what);
return null;
}
if (getHandler().hasMessages(what, mCurrentDevice)) {
log("getDeviceForMessage: returning " + mCurrentDevice);
return mCurrentDevice;
}
log("getDeviceForMessage: No matching device for " + what + ". Returning null");
return null;
}
private BluetoothDevice getMatchingDevice(BluetoothDevice device) {
if (mCurrentDevice.equals(device)) {
return mCurrentDevice;
}
return null;
}
/*
* Put the AT command, company ID, arguments, and device in an Intent and broadcast it.
*/
private void broadcastVendorSpecificEventIntent(String command, int companyId, int commandType,
Object[] arguments, BluetoothDevice device) {
log("broadcastVendorSpecificEventIntent(" + command + ")");
Intent intent = new Intent(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD, command);
intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE, commandType);
// assert: all elements of args are Serializable
intent.putExtra(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS, arguments);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "."
+ Integer.toString(companyId));
mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM);
}
private void configAudioParameters(BluetoothDevice device) {
// Reset NREC on connect event. Headset will override later
mAudioParams.put("NREC", 1);
mSystemInterface.getAudioManager()
.setParameters(
HEADSET_NAME + "=" + getCurrentDeviceName(device) + ";" + HEADSET_NREC
+ "=on");
Log.d(TAG,
"configAudioParameters for device:" + device + " are: nrec = " + mAudioParams.get(
"NREC"));
}
private void setAudioParameters(BluetoothDevice device) {
// 1. update nrec value
// 2. update headset name
int mNrec = 0;
if (!mAudioParams.isEmpty()) {
mNrec = mAudioParams.get("NREC");
} else {
Log.e(TAG, "setAudioParameters: audioParam not found");
}
if (mNrec == 1) {
Log.d(TAG, "Set NREC: 1 for device:" + device);
mSystemInterface.getAudioManager().setParameters(HEADSET_NREC + "=on");
} else {
Log.d(TAG, "Set NREC: 0 for device:" + device);
mSystemInterface.getAudioManager().setParameters(HEADSET_NREC + "=off");
}
mSystemInterface.getAudioManager()
.setParameters(HEADSET_NAME + "=" + getCurrentDeviceName(device));
}
private String parseUnknownAt(String atString) {
StringBuilder atCommand = new StringBuilder(atString.length());
String result = null;
for (int i = 0; i < atString.length(); i++) {
char c = atString.charAt(i);
if (c == '"') {
int j = atString.indexOf('"', i + 1); // search for closing "
if (j == -1) { // unmatched ", insert one.
atCommand.append(atString.substring(i, atString.length()));
atCommand.append('"');
break;
}
atCommand.append(atString.substring(i, j + 1));
i = j;
} else if (c != ' ') {
atCommand.append(Character.toUpperCase(c));
}
}
result = atCommand.toString();
return result;
}
private int getAtCommandType(String atCommand) {
int commandType = AtPhonebook.TYPE_UNKNOWN;
String atString = null;
atCommand = atCommand.trim();
if (atCommand.length() > 5) {
atString = atCommand.substring(5);
if (atString.startsWith("?")) { // Read
commandType = AtPhonebook.TYPE_READ;
} else if (atString.startsWith("=?")) { // Test
commandType = AtPhonebook.TYPE_TEST;
} else if (atString.startsWith("=")) { // Set
commandType = AtPhonebook.TYPE_SET;
} else {
commandType = AtPhonebook.TYPE_UNKNOWN;
}
}
return commandType;
}
/* Method to check if Virtual Call in Progress */
private boolean isVirtualCallInProgress() {
return mVirtualCallStarted;
}
private void setVirtualCallInProgress(boolean state) {
mVirtualCallStarted = state;
}
/* NOTE: Currently the VirtualCall API does not support handling of
call transfers. If it is initiated from the handsfree device,
HeadsetStateMachine will end the virtual call by calling
terminateScoUsingVirtualVoiceCall() in broadcastAudioState() */
private synchronized boolean initiateScoUsingVirtualVoiceCall() {
log("initiateScoUsingVirtualVoiceCall: Received");
// 1. Check if the SCO state is idle
if (isInCall() || mVoiceRecognitionStarted) {
Log.e(TAG, "initiateScoUsingVirtualVoiceCall: Call in progress.");
return false;
}
// 2. Send virtual phone state changed to initialize SCO
processCallState(new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_DIALING, "", 0),
true);
processCallState(new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_ALERTING, "", 0),
true);
processCallState(new HeadsetCallState(1, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0),
true);
setVirtualCallInProgress(true);
// Done
log("initiateScoUsingVirtualVoiceCall: Done");
return true;
}
private synchronized boolean terminateScoUsingVirtualVoiceCall() {
log("terminateScoUsingVirtualVoiceCall: Received");
if (!isVirtualCallInProgress()) {
Log.w(TAG, "terminateScoUsingVirtualVoiceCall: No present call to terminate");
return false;
}
// 2. Send virtual phone state changed to close SCO
processCallState(new HeadsetCallState(0, 0, HeadsetHalConstants.CALL_STATE_IDLE, "", 0),
true);
setVirtualCallInProgress(false);
// Done
log("terminateScoUsingVirtualVoiceCall: Done");
return true;
}
private void processDialCall(String number, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processDialCall device is null");
return;
}
String dialNumber;
if (mDialingOut) {
log("processDialCall, already dialling");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
if ((number == null) || (number.length() == 0)) {
dialNumber = mPhonebook.getLastDialledNumber();
if (dialNumber == null) {
log("processDialCall, last dial number null");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
} else if (number.charAt(0) == '>') {
// Yuck - memory dialling requested.
// Just dial last number for now
if (number.startsWith(">9999")) { // for PTS test
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
log("processDialCall, memory dial do last dial for now");
dialNumber = mPhonebook.getLastDialledNumber();
if (dialNumber == null) {
log("processDialCall, last dial number null");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
} else {
// Remove trailing ';'
if (number.charAt(number.length() - 1) == ';') {
number = number.substring(0, number.length() - 1);
}
dialNumber = PhoneNumberUtils.convertPreDial(number);
}
// Check for virtual call to terminate before sending Call Intent
terminateScoUsingVirtualVoiceCall();
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts(SCHEME_TEL, dialNumber, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mService.startActivity(intent);
// TODO(BT) continue send OK reults code after call starts
// hold wait lock, start a timer, set wait call flag
// Get call started indication from bluetooth phone
mDialingOut = true;
Message m = obtainMessage(DIALING_OUT_TIMEOUT);
m.obj = getMatchingDevice(device);
sendMessageDelayed(m, DIALING_OUT_TIMEOUT_VALUE);
}
private void processVolumeEvent(int volumeType, int volume, BluetoothDevice device) {
if (!mCurrentDevice.equals(device)) {
Log.w(TAG, "processVolumeEvent, ignored for unknown device " + device);
return;
}
// When there is an active call, only device in audio focus can change SCO volume
if (mSystemInterface.getHeadsetPhoneState().isInCall()
&& getAudioState() != BluetoothHeadset.STATE_AUDIO_CONNECTED) {
Log.w(TAG, "processVolumeEvent, ignored because " + mCurrentDevice
+ " does not have audio focus");
}
if (volumeType == HeadsetHalConstants.VOLUME_TYPE_SPK) {
mSpeakerVolume = volume;
int flag = (getCurrentState() == mAudioOn) ? AudioManager.FLAG_SHOW_UI : 0;
mSystemInterface.getAudioManager()
.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, volume, flag);
} else if (volumeType == HeadsetHalConstants.VOLUME_TYPE_MIC) {
// Not used currently
mMicVolume = volume;
} else {
Log.e(TAG, "Bad voluem type: " + volumeType);
}
}
private void processCallState(HeadsetCallState callState, boolean isVirtualCall) {
mSystemInterface.getHeadsetPhoneState().setNumActiveCall(callState.mNumActive);
mSystemInterface.getHeadsetPhoneState().setNumHeldCall(callState.mNumHeld);
mSystemInterface.getHeadsetPhoneState().setCallState(callState.mCallState);
if (mDialingOut) {
if (callState.mCallState == HeadsetHalConstants.CALL_STATE_DIALING) {
BluetoothDevice device = getDeviceForMessage(DIALING_OUT_TIMEOUT);
if (device == null) {
return;
}
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
removeMessages(DIALING_OUT_TIMEOUT);
} else if (callState.mCallState == HeadsetHalConstants.CALL_STATE_ACTIVE
|| callState.mCallState == HeadsetHalConstants.CALL_STATE_IDLE) {
mDialingOut = false;
}
}
log("mNumActive: " + callState.mNumActive + " mNumHeld: " + callState.mNumHeld
+ " mCallState: " + callState.mCallState);
log("mNumber: " + callState.mNumber + " mType: " + callState.mType);
if (isVirtualCall) {
// virtual call state update
if (getCurrentState() != mDisconnected) {
mNativeInterface.phoneStateChange(callState);
}
} else {
// circuit-switch voice call update
// stop virtual voice call if there is a CSV call ongoing
if (callState.mNumActive > 0 || callState.mNumHeld > 0
|| callState.mCallState != HeadsetHalConstants.CALL_STATE_IDLE) {
terminateScoUsingVirtualVoiceCall();
}
// Specific handling for case of starting MO/MT call while VOIP
// ongoing, terminateScoUsingVirtualVoiceCall() resets callState
// INCOMING/DIALING to IDLE. Some HS send AT+CIND? to read call
// and get wrong value of callsetup. This case is hit only
// SCO for VOIP call is not terminated via SDK API call.
if (mSystemInterface.getHeadsetPhoneState().getCallState() != callState.mCallState) {
mSystemInterface.getHeadsetPhoneState().setCallState(callState.mCallState);
}
// at this step: if there is virtual call ongoing, it means there is no CSV call
// let virtual call continue and skip phone state update
if (!isVirtualCallInProgress()) {
if (getCurrentState() != mDisconnected) {
mNativeInterface.phoneStateChange(callState);
}
}
}
}
private void processNoiseReductionEvent(boolean enable, BluetoothDevice device) {
if (!mAudioParams.isEmpty()) {
if (enable) {
mAudioParams.put("NREC", 1);
} else {
mAudioParams.put("NREC", 0);
}
log("NREC value for device :" + device + " is: " + mAudioParams.get("NREC"));
} else {
Log.e(TAG, "processNoiseReductionEvent: audioParamNrec is null ");
}
if (mCurrentDevice != null && mCurrentDevice.equals(device)
&& getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
setAudioParameters(device);
}
}
private void processWBSEvent(int wbsConfig) {
switch (wbsConfig) {
case HeadsetHalConstants.BTHF_WBS_YES:
Log.d(TAG, "AudioManager.setParameters: bt_wbs=on");
mSystemInterface.getAudioManager().setParameters(HEADSET_WBS + "=on");
break;
case HeadsetHalConstants.BTHF_WBS_NO:
case HeadsetHalConstants.BTHF_WBS_NONE:
Log.d(TAG, "AudioManager.setParameters: bt_wbs=off, wbsConfig=" + wbsConfig);
mSystemInterface.getAudioManager().setParameters(HEADSET_WBS + "=off");
break;
default:
Log.e(TAG, "processWBSEvent, unknown wbsConfig " + wbsConfig);
}
}
private void processAtChld(int chld, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processAtChld device is null");
return;
}
if (mSystemInterface.processChld(chld)) {
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
} else {
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
private void processSubscriberNumberRequest(BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processSubscriberNumberRequest device is null");
return;
}
String number = mSystemInterface.getSubscriberNumber();
if (number != null) {
mNativeInterface.atResponseString(device,
"+CNUM: ,\"" + number + "\"," + PhoneNumberUtils.toaFromString(number) + ",,4");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
} else {
Log.e(TAG, "getSubscriberNumber returns null");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
private void processAtCind(BluetoothDevice device) {
int call, callSetup;
if (device == null) {
Log.w(TAG, "processAtCind device is null");
return;
}
final HeadsetPhoneState phoneState = mSystemInterface.getHeadsetPhoneState();
/* Handsfree carkits expect that +CIND is properly responded to
Hence we ensure that a proper response is sent
for the virtual call too.*/
if (isVirtualCallInProgress()) {
call = 1;
callSetup = 0;
} else {
// regular phone call
call = phoneState.getNumActiveCall();
callSetup = phoneState.getNumHeldCall();
}
mNativeInterface.cindResponse(device, phoneState.getCindService(), call, callSetup,
phoneState.getCallState(), phoneState.getCindSignal(), phoneState.getCindRoam(),
phoneState.getCindBatteryCharge());
}
private void processAtCops(BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processAtCops device is null");
return;
}
String operatorName = mSystemInterface.getNetworkOperator();
if (operatorName == null) {
operatorName = "";
}
mNativeInterface.copsResponse(device, operatorName);
}
private void processAtClcc(BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processAtClcc device is null");
return;
}
if (isVirtualCallInProgress()) {
// In virtual call, send our phone number instead of remote phone number
String phoneNumber = mSystemInterface.getSubscriberNumber();
if (phoneNumber == null) {
phoneNumber = "";
}
int type = PhoneNumberUtils.toaFromString(phoneNumber);
mNativeInterface.clccResponse(device, 1, 0, 0, 0, false, phoneNumber, type);
mNativeInterface.clccResponse(device, 0, 0, 0, 0, false, "", 0);
} else {
// In Telecom call, ask Telecom to send send remote phone number
if (!mSystemInterface.listCurrentCalls()) {
Log.e(TAG, "processAtClcc: failed to list current calls for " + device);
mNativeInterface.clccResponse(device, 0, 0, 0, 0, false, "", 0);
} else {
Message m = obtainMessage(CLCC_RSP_TIMEOUT);
m.obj = getMatchingDevice(device);
sendMessageDelayed(m, CLCC_RSP_TIMEOUT_VALUE);
}
}
}
private void processAtCscs(String atString, int type, BluetoothDevice device) {
log("processAtCscs - atString = " + atString);
if (mPhonebook != null) {
mPhonebook.handleCscsCommand(atString, type, device);
} else {
Log.e(TAG, "Phonebook handle null for At+CSCS");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
private void processAtCpbs(String atString, int type, BluetoothDevice device) {
log("processAtCpbs - atString = " + atString);
if (mPhonebook != null) {
mPhonebook.handleCpbsCommand(atString, type, device);
} else {
Log.e(TAG, "Phonebook handle null for At+CPBS");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
private void processAtCpbr(String atString, int type, BluetoothDevice device) {
log("processAtCpbr - atString = " + atString);
if (mPhonebook != null) {
mPhonebook.handleCpbrCommand(atString, type, device);
} else {
Log.e(TAG, "Phonebook handle null for At+CPBR");
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
}
}
/**
* Find a character ch, ignoring quoted sections.
* Return input.length() if not found.
*/
private static int findChar(char ch, String input, int fromIndex) {
for (int i = fromIndex; i < input.length(); i++) {
char c = input.charAt(i);
if (c == '"') {
i = input.indexOf('"', i + 1);
if (i == -1) {
return input.length();
}
} else if (c == ch) {
return i;
}
}
return input.length();
}
/**
* Break an argument string into individual arguments (comma delimited).
* Integer arguments are turned into Integer objects. Otherwise a String
* object is used.
*/
private static Object[] generateArgs(String input) {
int i = 0;
int j;
ArrayList<Object> out = new ArrayList<Object>();
while (i <= input.length()) {
j = findChar(',', input, i);
String arg = input.substring(i, j);
try {
out.add(new Integer(arg));
} catch (NumberFormatException e) {
out.add(arg);
}
i = j + 1; // move past comma
}
return out.toArray();
}
/**
* Process vendor specific AT commands
*
* @param atString AT command after the "AT+" prefix
* @param device Remote device that has sent this command
*/
private void processVendorSpecificAt(String atString, BluetoothDevice device) {
log("processVendorSpecificAt - atString = " + atString);
// Currently we accept only SET type commands.
int indexOfEqual = atString.indexOf("=");
if (indexOfEqual == -1) {
Log.e(TAG, "processVendorSpecificAt: command type error in " + atString);
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
String command = atString.substring(0, indexOfEqual);
Integer companyId = VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.get(command);
if (companyId == null) {
Log.e(TAG, "processVendorSpecificAt: unsupported command: " + atString);
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
String arg = atString.substring(indexOfEqual + 1);
if (arg.startsWith("?")) {
Log.e(TAG, "processVendorSpecificAt: command type error in " + atString);
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_ERROR, 0);
return;
}
Object[] args = generateArgs(arg);
if (command.equals(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_XAPL)) {
processAtXapl(args, device);
}
broadcastVendorSpecificEventIntent(command, companyId, BluetoothHeadset.AT_CMD_TYPE_SET,
args, device);
mNativeInterface.atResponseCode(device, HeadsetHalConstants.AT_RESPONSE_OK, 0);
}
/**
* Process AT+XAPL AT command
*
* @param args command arguments after the equal sign
* @param device Remote device that has sent this command
*/
private void processAtXapl(Object[] args, BluetoothDevice device) {
if (args.length != 2) {
Log.w(TAG, "processAtXapl() args length must be 2: " + String.valueOf(args.length));
return;
}
if (!(args[0] instanceof String) || !(args[1] instanceof Integer)) {
Log.w(TAG, "processAtXapl() argument types not match");
return;
}
// feature = 2 indicates that we support battery level reporting only
mNativeInterface.atResponseString(device, "+XAPL=iPhone," + String.valueOf(2));
}
private void processUnknownAt(String atString, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processUnknownAt device is null");
return;
}
log("processUnknownAt - atString = " + atString);
String atCommand = parseUnknownAt(atString);
int commandType = getAtCommandType(atCommand);
if (atCommand.startsWith("+CSCS")) {
processAtCscs(atCommand.substring(5), commandType, device);
} else if (atCommand.startsWith("+CPBS")) {
processAtCpbs(atCommand.substring(5), commandType, device);
} else if (atCommand.startsWith("+CPBR")) {
processAtCpbr(atCommand.substring(5), commandType, device);
} else {
processVendorSpecificAt(atCommand, device);
}
}
private void processKeyPressed(BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "processKeyPressed device is null");
return;
}
final HeadsetPhoneState phoneState = mSystemInterface.getHeadsetPhoneState();
if (phoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING) {
mSystemInterface.answerCall(device);
} else if (phoneState.getNumActiveCall() > 0) {
if (getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) {
mNativeInterface.connectAudio(mCurrentDevice);
} else {
mSystemInterface.hangupCall(device, false);
}
} else {
String dialNumber = mPhonebook.getLastDialledNumber();
if (dialNumber == null) {
log("processKeyPressed, last dial number null");
return;
}
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts(SCHEME_TEL, dialNumber, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mService.startActivity(intent);
}
}
/**
* Send HF indicator value changed intent
*
* @param device Device whose HF indicator value has changed
* @param indId Indicator ID [0-65535]
* @param indValue Indicator Value [0-65535], -1 means invalid but indId is supported
*/
private void sendIndicatorIntent(BluetoothDevice device, int indId, int indValue) {
Intent intent = new Intent(BluetoothHeadset.ACTION_HF_INDICATORS_VALUE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_ID, indId);
intent.putExtra(BluetoothHeadset.EXTRA_HF_INDICATORS_IND_VALUE, indValue);
mService.sendBroadcast(intent, HeadsetService.BLUETOOTH_PERM);
}
private void processAtBind(String atString, BluetoothDevice device) {
log("processAtBind: " + atString);
// Parse the AT String to find the Indicator Ids that are supported
int indId = 0;
int iter = 0;
int iter1 = 0;
while (iter < atString.length()) {
iter1 = findChar(',', atString, iter);
String id = atString.substring(iter, iter1);
try {
indId = Integer.valueOf(id);
} catch (NumberFormatException e) {
Log.e(TAG, Log.getStackTraceString(new Throwable()));
}
switch (indId) {
case HeadsetHalConstants.HF_INDICATOR_ENHANCED_DRIVER_SAFETY:
log("Send Broadcast intent for the Enhanced Driver Safety indicator.");
sendIndicatorIntent(device, indId, -1);
break;
case HeadsetHalConstants.HF_INDICATOR_BATTERY_LEVEL_STATUS:
log("Send Broadcast intent for the Battery Level indicator.");
sendIndicatorIntent(device, indId, -1);
break;
default:
log("Invalid HF Indicator Received");
break;
}
iter = iter1 + 1; // move past comma
}
}
private void processAtBiev(int indId, int indValue, BluetoothDevice device) {
log("processAtBiev: ind_id=" + indId + ", ind_value=" + indValue);
sendIndicatorIntent(device, indId, indValue);
}
private void processDeviceStateChanged(HeadsetDeviceState deviceState) {
mNativeInterface.notifyDeviceStatus(deviceState);
}
private void processSendClccResponse(HeadsetClccResponse clcc) {
BluetoothDevice device = getDeviceForMessage(CLCC_RSP_TIMEOUT);
if (device == null) {
return;
}
if (clcc.mIndex == 0) {
removeMessages(CLCC_RSP_TIMEOUT);
}
mNativeInterface.clccResponse(device, clcc.mIndex, clcc.mDirection, clcc.mStatus,
clcc.mMode, clcc.mMpty, clcc.mNumber, clcc.mType);
}
private void processSendVendorSpecificResultCode(HeadsetVendorSpecificResultCode resultCode) {
String stringToSend = resultCode.mCommand + ": ";
if (resultCode.mArg != null) {
stringToSend += resultCode.mArg;
}
mNativeInterface.atResponseString(resultCode.mDevice, stringToSend);
}
private String getCurrentDeviceName(BluetoothDevice device) {
String defaultName = "<unknown>";
if (device == null) {
return defaultName;
}
String deviceName = device.getName();
if (deviceName == null) {
return defaultName;
}
return deviceName;
}
private boolean isInCall() {
final HeadsetPhoneState phoneState = mSystemInterface.getHeadsetPhoneState();
return ((phoneState.getNumActiveCall() > 0) || (phoneState.getNumHeldCall() > 0) || (
(phoneState.getCallState() != HeadsetHalConstants.CALL_STATE_IDLE) && (
phoneState.getCallState() != HeadsetHalConstants.CALL_STATE_INCOMING)));
}
private boolean isRinging() {
return mSystemInterface.getHeadsetPhoneState().getCallState()
== HeadsetHalConstants.CALL_STATE_INCOMING;
}
// Accept incoming SCO only when there is in-band ringing, incoming call,
// active call, VR activated, active VOIP call
private boolean isScoAcceptable() {
if (mForceScoAudio) {
return true;
}
if (!mService.getAudioRouteAllowed()) {
return false;
}
if (isInCall() || mVoiceRecognitionStarted) {
return true;
}
if (isRinging() && BluetoothHeadset.isInbandRingingSupported(mService)) {
return true;
}
return false;
}
private boolean okToAcceptConnection(BluetoothDevice device) {
AdapterService adapterService = AdapterService.getAdapterService();
// check if this is an incoming connection in Quiet mode.
if (adapterService == null) {
Log.e(TAG, "okToAcceptConnection, cannot get adapterService");
return false;
}
if (adapterService.isQuietModeEnabled() && mCurrentDevice == null) {
Log.i(TAG, "okToAcceptConnection, quiet mode enabled and current device is null");
return false;
}
// check priority and accept or reject the connection. if priority is undefined
// it is likely that our SDP has not completed and peer is initiating the
// connection. Allow this connection, provided the device is bonded
int priority = mService.getPriority(device);
int bondState = device.getBondState();
if ((priority > BluetoothProfile.PRIORITY_OFF) || (
(priority == BluetoothProfile.PRIORITY_UNDEFINED)
&& bondState != BluetoothDevice.BOND_NONE)) {
return true;
}
Log.i(TAG, "okToAcceptConnection, rejected, priority=" + priority + ", bondState="
+ bondState);
return false;
}
@Override
protected void log(String msg) {
if (DBG) {
super.log(msg);
}
}
private void handleAccessPermissionResult(Intent intent) {
log("handleAccessPermissionResult");
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (!mPhonebook.getCheckingAccessPermission()) {
return;
}
int atCommandResult = 0;
int atCommandErrorCode = 0;
// HeadsetBase headset = mHandsfree.getHeadset();
// ASSERT: (headset != null) && headSet.isConnected()
// REASON: mCheckingAccessPermission is true, otherwise resetAtState
// has set mCheckingAccessPermission to false
if (intent.getAction().equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT,
BluetoothDevice.CONNECTION_ACCESS_NO)
== BluetoothDevice.CONNECTION_ACCESS_YES) {
if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
mCurrentDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
}
atCommandResult = mPhonebook.processCpbrCommand(device);
} else {
if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) {
mCurrentDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
}
}
}
mPhonebook.setCpbrIndex(-1);
mPhonebook.setCheckingAccessPermission(false);
if (atCommandResult >= 0) {
mNativeInterface.atResponseCode(device, atCommandResult, atCommandErrorCode);
} else {
log("handleAccessPermissionResult - RESULT_NONE");
}
}
static String getMessageName(int what) {
switch (what) {
case CONNECT:
return "CONNECT";
case DISCONNECT:
return "DISCONNECT";
case CONNECT_AUDIO:
return "CONNECT_AUDIO";
case DISCONNECT_AUDIO:
return "DISCONNECT_AUDIO";
case VOICE_RECOGNITION_START:
return "VOICE_RECOGNITION_START";
case VOICE_RECOGNITION_STOP:
return "VOICE_RECOGNITION_STOP";
case INTENT_SCO_VOLUME_CHANGED:
return "INTENT_SCO_VOLUME_CHANGED";
case INTENT_CONNECTION_ACCESS_REPLY:
return "INTENT_CONNECTION_ACCESS_REPLY";
case CALL_STATE_CHANGED:
return "CALL_STATE_CHANGED";
case DEVICE_STATE_CHANGED:
return "DEVICE_STATE_CHANGED";
case SEND_CCLC_RESPONSE:
return "SEND_CCLC_RESPONSE";
case SEND_VENDOR_SPECIFIC_RESULT_CODE:
return "SEND_VENDOR_SPECIFIC_RESULT_CODE";
case VIRTUAL_CALL_START:
return "VIRTUAL_CALL_START";
case VIRTUAL_CALL_STOP:
return "VIRTUAL_CALL_STOP";
case STACK_EVENT:
return "STACK_EVENT";
case DIALING_OUT_TIMEOUT:
return "DIALING_OUT_TIMEOUT";
case START_VR_TIMEOUT:
return "START_VR_TIMEOUT";
case CLCC_RSP_TIMEOUT:
return "CLCC_RSP_TIMEOUT";
case CONNECT_TIMEOUT:
return "CONNECT_TIMEOUT";
default:
return "UNKNOWN";
}
}
}