/* * 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. */ /** * Bluetooth Handset StateMachine * (Disconnected) * | ^ * CONNECT | | DISCONNECTED * V | * (Pending) * | ^ * CONNECTED | | CONNECT * V | * (Connected) * | ^ * CONNECT_AUDIO | | DISCONNECT_AUDIO * V | * (AudioOn) */ 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.bluetooth.IBluetooth; import android.bluetooth.IBluetoothHeadsetPhone; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.ActivityNotFoundException; import android.media.AudioManager; import android.net.Uri; import android.os.IBinder; import android.os.IDeviceIdleController; import android.os.Message; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ServiceManager; import android.os.PowerManager; import android.os.UserHandle; import android.os.PowerManager.WakeLock; import android.telephony.PhoneNumberUtils; import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.internal.util.IState; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import android.os.SystemProperties; final class HeadsetStateMachine extends StateMachine { private static final String TAG = "HeadsetStateMachine"; private static final boolean DBG = false; //For Debugging only private static int sRefCount=0; 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"; 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 SET_MIC_VOLUME = 8; static final int CALL_STATE_CHANGED = 9; static final int INTENT_BATTERY_CHANGED = 10; 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 ENABLE_WBS = 16; static final int DISABLE_WBS = 17; private 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; // Max number of HF connections at any time private int max_hf_connections = 1; private static final int NBS_CODEC = 1; private static final int WBS_CODEC = 2; // Keys are AT commands, and values are the company IDs. private static final Map VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID; // Hash for storing the Audio Parameters like NREC for connected headsets private HashMap mHeadsetAudioParam = new HashMap(); // Hash for storing the Remotedevice BRSF private HashMap mHeadsetBrsf = new HashMap(); private static final ParcelUuid[] HEADSET_UUIDS = { BluetoothUuid.HSP, BluetoothUuid.Handsfree, }; private Disconnected mDisconnected; private Pending mPending; private Connected mConnected; private AudioOn mAudioOn; // Multi HFP: add new class object private MultiHFPending mMultiHFPending; private HeadsetService mService; private PowerManager mPowerManager; private boolean mVirtualCallStarted = false; private boolean mVoiceRecognitionStarted = false; private boolean mWaitingForVoiceRecognition = false; private WakeLock mStartVoiceRecognitionWakeLock; // held while waiting for voice recognition private boolean mDialingOut = false; private AudioManager mAudioManager; private AtPhonebook mPhonebook; private static Intent sVoiceCommandIntent; private HeadsetPhoneState mPhoneState; private int mAudioState; private BluetoothAdapter mAdapter; private IBluetoothHeadsetPhone mPhoneProxy; private boolean mNativeAvailable; // Indicates whether audio can be routed to the device. private boolean mAudioRouteAllowed = true; // mCurrentDevice is the device connected before the state changes // mTargetDevice is the device to be connected // mIncomingDevice is the device connecting to us, valid only in Pending state // when mIncomingDevice is not null, both mCurrentDevice // and mTargetDevice are null // when either mCurrentDevice or mTargetDevice is not null, // mIncomingDevice is null // Stable states // No connection, Disconnected state // both mCurrentDevice and mTargetDevice are null // Connected, Connected state // mCurrentDevice is not null, mTargetDevice is null // Interim states // Connecting to a device, Pending // mCurrentDevice is null, mTargetDevice is not null // Disconnecting device, Connecting to new device // Pending // Both mCurrentDevice and mTargetDevice are not null // Disconnecting device Pending // mCurrentDevice is not null, mTargetDevice is null // Incoming connections Pending // Both mCurrentDevice and mTargetDevice are null private BluetoothDevice mCurrentDevice = null; private BluetoothDevice mTargetDevice = null; private BluetoothDevice mIncomingDevice = null; private BluetoothDevice mActiveScoDevice = null; private BluetoothDevice mMultiDisconnectDevice = null; // Multi HFP: Connected devices list holds all currently connected headsets private ArrayList mConnectedDevicesList = new ArrayList(); static { classInitNative(); VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID = new HashMap(); VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put("+XEVENT", BluetoothAssignedNumbers.PLANTRONICS); VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID.put("+ANDROID", BluetoothAssignedNumbers.GOOGLE); } private HeadsetStateMachine(HeadsetService context) { super(TAG); mService = context; mVoiceRecognitionStarted = false; mWaitingForVoiceRecognition = false; mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mStartVoiceRecognitionWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":VoiceRecognition"); mStartVoiceRecognitionWakeLock.setReferenceCounted(false); mDialingOut = false; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mPhonebook = new AtPhonebook(mService, this); mPhoneState = new HeadsetPhoneState(context, this); mAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED; mAdapter = BluetoothAdapter.getDefaultAdapter(); Intent intent = new Intent(IBluetoothHeadsetPhone.class.getName()); intent.setComponent(intent.resolveSystemService(context.getPackageManager(), 0)); if (intent.getComponent() == null || !context.bindService(intent, mConnection, 0)) { Log.e(TAG, "Could not bind to Bluetooth Headset Phone Service"); } String max_hfp_clients = SystemProperties.get("bt.max.hfpclient.connections"); if (!max_hfp_clients.isEmpty() && (Integer.parseInt(max_hfp_clients) == 2)) max_hf_connections = Integer.parseInt(max_hfp_clients); Log.d(TAG, "max_hf_connections = " + max_hf_connections); initializeNative(max_hf_connections); mNativeAvailable=true; mDisconnected = new Disconnected(); mPending = new Pending(); mConnected = new Connected(); mAudioOn = new AudioOn(); // Multi HFP: initialise new class variable mMultiHFPending = new MultiHFPending(); if (sVoiceCommandIntent == null) { sVoiceCommandIntent = new Intent(Intent.ACTION_VOICE_COMMAND); sVoiceCommandIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } addState(mDisconnected); addState(mPending); addState(mConnected); addState(mAudioOn); // Multi HFP: add State addState(mMultiHFPending); setInitialState(mDisconnected); } static HeadsetStateMachine make(HeadsetService context) { Log.d(TAG, "make"); HeadsetStateMachine hssm = new HeadsetStateMachine(context); hssm.start(); return hssm; } public void doQuit() { quitNow(); } public void cleanup() { if (mPhoneProxy != null) { if (DBG) Log.d(TAG,"Unbinding service..."); synchronized (mConnection) { try { mPhoneProxy = null; mService.unbindService(mConnection); } catch (Exception re) { Log.e(TAG,"Error unbinding from IBluetoothHeadsetPhone",re); } } } if (mPhoneState != null) { mPhoneState.listenForPhoneState(false); mPhoneState.cleanup(); } if (mPhonebook != null) { mPhonebook.cleanup(); } if (mHeadsetAudioParam != null) { mHeadsetAudioParam.clear(); } if (mHeadsetBrsf != null) { mHeadsetBrsf.clear(); } if (mConnectedDevicesList != null) { mConnectedDevicesList.clear(); } if (mNativeAvailable) { cleanupNative(); mNativeAvailable = false; } } public void dump(StringBuilder sb) { ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice); ProfileService.println(sb, "mTargetDevice: " + mTargetDevice); ProfileService.println(sb, "mIncomingDevice: " + mIncomingDevice); ProfileService.println(sb, "mActiveScoDevice: " + mActiveScoDevice); ProfileService.println(sb, "mMultiDisconnectDevice: " + mMultiDisconnectDevice); ProfileService.println(sb, "mVirtualCallStarted: " + mVirtualCallStarted); ProfileService.println(sb, "mVoiceRecognitionStarted: " + mVoiceRecognitionStarted); ProfileService.println(sb, "mWaitingForVoiceRecognition: " + mWaitingForVoiceRecognition); ProfileService.println(sb, "StateMachine: " + this.toString()); ProfileService.println(sb, "mPhoneState: " + mPhoneState); ProfileService.println(sb, "mAudioState: " + mAudioState); } private class Disconnected extends State { @Override public void enter() { log("Enter Disconnected: " + getCurrentMessage().what + ", size: " + mConnectedDevicesList.size()); mPhonebook.resetAtState(); mPhoneState.listenForPhoneState(false); mVoiceRecognitionStarted = false; mWaitingForVoiceRecognition = false; } @Override public boolean processMessage(Message message) { log("Disconnected process message: " + message.what + ", size: " + mConnectedDevicesList.size()); if (mConnectedDevicesList.size() != 0 || mTargetDevice != null || mIncomingDevice != null) { Log.e(TAG, "ERROR: mConnectedDevicesList is not empty," + "target, or mIncomingDevice not null in Disconnected"); return NOT_HANDLED; } boolean retValue = HANDLED; switch(message.what) { case CONNECT: BluetoothDevice device = (BluetoothDevice) message.obj; broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!connectHfpNative(getByteAddress(device)) ) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; transitionTo(mPending); } // TODO(BT) remove CONNECT_TIMEOUT when the stack // sends back events consistently Message m = obtainMessage(CONNECT_TIMEOUT); m.obj = device; sendMessageDelayed(m, 30000); break; case DISCONNECT: // ignore break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj, ((message.arg1 == 1)?true:false)); break; case STACK_EVENT: StackEvent event = (StackEvent) message.obj; if (DBG) { log("event type: " + event.type); } switch (event.type) { case EVENT_TYPE_CONNECTION_STATE_CHANGED: processConnectionEvent(event.valueInt, event.device); break; default: Log.e(TAG, "Unexpected stack event: " + event.type); break; } break; default: return NOT_HANDLED; } return retValue; } @Override public void exit() { log("Exit Disconnected: " + getCurrentMessage().what); } // in Disconnected state private void processConnectionEvent(int state, BluetoothDevice device) { Log.d(TAG, "processConnectionEvent state = " + state + ", device = " + device); switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: Log.w(TAG, "Ignore HF DISCONNECTED event, device: " + device); break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: if (okToConnect(device)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { mIncomingDevice = device; transitionTo(mPending); } } else { Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device)+ " bondState=" + device.getBondState()); //reject the connection and stay in Disconnected state itself disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: Log.w(TAG, "HFP Connected from Disconnected state"); if (okToConnect(device)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { if (!mConnectedDevicesList.contains(device)) { mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is adding in Disconnected state"); } mCurrentDevice = device; transitionTo(mConnected); } configAudioParameters(device); } else { //reject the connection and stay in Disconnected state itself Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device) + " bondState=" + device.getBondState()); disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } break; case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING: Log.w(TAG, "Ignore HF DISCONNECTING event, device: " + device); break; default: Log.e(TAG, "Incorrect state: " + state); break; } } } private class Pending extends State { @Override public void enter() { log("Enter Pending: " + getCurrentMessage().what); } @Override public boolean processMessage(Message message) { log("Pending process message: " + message.what + ", size: " + mConnectedDevicesList.size()); boolean retValue = HANDLED; switch(message.what) { case CONNECT: case CONNECT_AUDIO: deferMessage(message); break; case CONNECT_TIMEOUT: onConnectionStateChanged(HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, getByteAddress(mTargetDevice)); break; case DISCONNECT: BluetoothDevice device = (BluetoothDevice) message.obj; if (mCurrentDevice != null && mTargetDevice != null && mTargetDevice.equals(device) ) { // cancel connection to the mTargetDevice broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; } } else { deferMessage(message); } break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj, ((message.arg1 == 1)?true:false)); break; case STACK_EVENT: StackEvent event = (StackEvent) message.obj; if (DBG) { log("event type: " + event.type); } switch (event.type) { case EVENT_TYPE_CONNECTION_STATE_CHANGED: BluetoothDevice device1 = getDeviceForMessage(CONNECT_TIMEOUT); if (device1 != null && device1.equals(event.device)) { Log.d(TAG, "remove connect timeout for device = " + device1); removeMessages(CONNECT_TIMEOUT); } processConnectionEvent(event.valueInt, event.device); break; default: Log.e(TAG, "Unexpected event: " + event.type); break; } break; default: return NOT_HANDLED; } return retValue; } // in Pending state private void processConnectionEvent(int state, BluetoothDevice device) { Log.d(TAG, "processConnectionEvent state = " + state + ", device = " + device); switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mConnectedDevicesList.contains(device)) { synchronized (HeadsetStateMachine.this) { mConnectedDevicesList.remove(device); mHeadsetAudioParam.remove(device); mHeadsetBrsf.remove(device); Log.d(TAG, "device " + device.getAddress() + " is removed in Pending state"); } broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); synchronized (HeadsetStateMachine.this) { mCurrentDevice = null; } processWBSEvent(0, device); /* disable WBS audio parameters */ if (mTargetDevice != null) { if (!connectHfpNative(getByteAddress(mTargetDevice))) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; transitionTo(mDisconnected); } } } else { synchronized (HeadsetStateMachine.this) { mIncomingDevice = null; if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { processMultiHFConnected(device); } } } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { // outgoing connection failed broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { transitionTo(mConnected); } } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mIncomingDevice = null; if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { transitionTo(mConnected); } } } else { Log.e(TAG, "Unknown device Disconnected: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: if (mConnectedDevicesList.contains(device)) { // disconnection failed broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTING); if (mTargetDevice != null) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); } synchronized (HeadsetStateMachine.this) { mTargetDevice = null; transitionTo(mConnected); } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in Pending state"); mTargetDevice = null; transitionTo(mConnected); } broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); configAudioParameters(device); } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in Pending state"); mIncomingDevice = null; transitionTo(mConnected); } broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); configAudioParameters(device); } else { Log.w(TAG, "Some other incoming HF connected in Pending state"); if (okToConnect(device)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in Pending state"); } configAudioParameters(device); } else { //reject the connection and stay in Pending state itself Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device) + " bondState=" + device.getBondState()); disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) { log("current device tries to connect back"); // TODO(BT) ignore or reject } else if (mTargetDevice != null && mTargetDevice.equals(device)) { // The stack is connecting to target device or // there is an incoming connection from the target device at the same time // we already broadcasted the intent, doing nothing here if (DBG) { log("Stack and target device are connecting"); } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { Log.e(TAG, "Another connecting event on the incoming device"); } else { // We get an incoming connecting request while Pending // TODO(BT) is stack handing this case? let's ignore it for now log("Incoming connection while pending, ignore"); } break; case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING: if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) { // we already broadcasted the intent, doing nothing here if (DBG) { log("stack is disconnecting mCurrentDevice"); } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { Log.e(TAG, "TargetDevice is getting disconnected"); } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { Log.e(TAG, "IncomingDevice is getting disconnected"); } else { Log.e(TAG, "Disconnecting unknow device: " + device); } break; default: Log.e(TAG, "Incorrect state: " + state); break; } } private void processMultiHFConnected(BluetoothDevice device) { log("Pending state: processMultiHFConnected"); /* Assign the current activedevice again if the disconnected device equals to the current active device*/ if (mCurrentDevice != null && mCurrentDevice.equals(device)) { transitionTo(mConnected); int deviceSize = mConnectedDevicesList.size(); mCurrentDevice = mConnectedDevicesList.get(deviceSize-1); } else { // The disconnected device is not current active device if (mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mAudioOn); else transitionTo(mConnected); } log("processMultiHFConnected , the latest mCurrentDevice is:" + mCurrentDevice); log("Pending state: processMultiHFConnected ," + "fake broadcasting for mCurrentDevice"); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); } } private class Connected extends State { @Override public void enter() { // 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. // TODO: This needs to check for multiple HFP connections, once supported... removeDeferredMessages(CONNECT); log("Enter Connected: " + getCurrentMessage().what + ", size: " + mConnectedDevicesList.size()); // start phone state listener here so that the CIND response as part of SLC can be // responded to, correctly. // we may enter Connected from Disconnected/Pending/AudioOn. listenForPhoneState // internally handles multiple calls to start listen mPhoneState.listenForPhoneState(true); } @Override public boolean processMessage(Message message) { log("Connected process message: " + message.what + ", size: " + mConnectedDevicesList.size()); if (DBG) { if (mConnectedDevicesList.size() == 0) { log("ERROR: mConnectedDevicesList is empty in Connected"); return NOT_HANDLED; } } boolean retValue = HANDLED; switch(message.what) { case CONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (device == null) { break; } if (mConnectedDevicesList.contains(device)) { Log.e(TAG, "ERROR: Connect received for already connected device, Ignore"); break; } if (mConnectedDevicesList.size() >= max_hf_connections) { BluetoothDevice DisconnectConnectedDevice = null; IState CurrentAudioState = getCurrentState(); Log.d(TAG, "Reach to max size, disconnect one of them first"); /* TODO: Disconnect based on CoD */ DisconnectConnectedDevice = mConnectedDevicesList.get(0); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!disconnectHfpNative(getByteAddress(DisconnectConnectedDevice))) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } else { broadcastConnectionState(DisconnectConnectedDevice, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; if (max_hf_connections == 1) { transitionTo(mPending); } else { mMultiDisconnectDevice = DisconnectConnectedDevice; transitionTo(mMultiHFPending); } DisconnectConnectedDevice = null; } }else if (mConnectedDevicesList.size() < max_hf_connections) { broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!connectHfpNative(getByteAddress(device))) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; // Transtion to MultiHFPending state for Multi HF connection transitionTo(mMultiHFPending); } } Message m = obtainMessage(CONNECT_TIMEOUT); m.obj = device; sendMessageDelayed(m, 30000); } break; case DISCONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (!mConnectedDevicesList.contains(device)) { break; } broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); if (!disconnectHfpNative(getByteAddress(device))) { broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); break; } if (mConnectedDevicesList.size() > 1) { mMultiDisconnectDevice = device; transitionTo(mMultiHFPending); } else { transitionTo(mPending); } } break; case CONNECT_AUDIO: { BluetoothDevice device = mCurrentDevice; // TODO(BT) when failure, broadcast audio connecting to disconnected intent // check if device matches mCurrentDevice if (mActiveScoDevice != null) { log("connectAudioNative in Connected; mActiveScoDevice is not null"); device = mActiveScoDevice; } log("connectAudioNative in Connected for device = " + device); connectAudioNative(getByteAddress(device)); } break; case VOICE_RECOGNITION_START: processLocalVrEvent(HeadsetHalConstants.VR_STATE_STARTED); break; case VOICE_RECOGNITION_STOP: processLocalVrEvent(HeadsetHalConstants.VR_STATE_STOPPED); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj, ((message.arg1==1)?true:false)); break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); 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; clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } 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; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } break; case VIRTUAL_CALL_START: initiateScoUsingVirtualVoiceCall(); break; case VIRTUAL_CALL_STOP: terminateScoUsingVirtualVoiceCall(); break; case ENABLE_WBS: { BluetoothDevice device = (BluetoothDevice) message.obj; configureWBSNative(getByteAddress(device),WBS_CODEC); } break; case DISABLE_WBS: { BluetoothDevice device = (BluetoothDevice) message.obj; configureWBSNative(getByteAddress(device),NBS_CODEC); } break; case START_VR_TIMEOUT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (mWaitingForVoiceRecognition) { device = (BluetoothDevice) message.obj; mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice recognition to start"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } break; case STACK_EVENT: StackEvent event = (StackEvent) message.obj; if (DBG) { log("event type: " + event.type + "event device : " + event.device); } switch (event.type) { case EVENT_TYPE_CONNECTION_STATE_CHANGED: processConnectionEvent(event.valueInt, event.device); break; case EVENT_TYPE_AUDIO_STATE_CHANGED: processAudioEvent(event.valueInt, event.device); break; case EVENT_TYPE_VR_STATE_CHANGED: processVrEvent(event.valueInt, event.device); break; case EVENT_TYPE_ANSWER_CALL: // TODO(BT) could answer call happen on Connected state? processAnswerCall(event.device); break; case EVENT_TYPE_HANGUP_CALL: // TODO(BT) could hangup call happen on Connected state? processHangupCall(event.device); break; case EVENT_TYPE_VOLUME_CHANGED: processVolumeEvent(event.valueInt, event.valueInt2, event.device); break; case EVENT_TYPE_DIAL_CALL: processDialCall(event.valueString, event.device); break; case EVENT_TYPE_SEND_DTMF: processSendDtmf(event.valueInt, event.device); break; case EVENT_TYPE_NOICE_REDUCTION: processNoiceReductionEvent(event.valueInt, event.device); break; case EVENT_TYPE_WBS: Log.d(TAG, "EVENT_TYPE_WBS codec is "+event.valueInt); processWBSEvent(event.valueInt, event.device); break; case EVENT_TYPE_AT_CHLD: processAtChld(event.valueInt, event.device); break; case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST: processSubscriberNumberRequest(event.device); break; case EVENT_TYPE_AT_CIND: processAtCind(event.device); break; case EVENT_TYPE_AT_COPS: processAtCops(event.device); break; case EVENT_TYPE_AT_CLCC: processAtClcc(event.device); break; case EVENT_TYPE_UNKNOWN_AT: processUnknownAt(event.valueString, event.device); break; case EVENT_TYPE_KEY_PRESSED: processKeyPressed(event.device); break; default: Log.e(TAG, "Unknown stack event: " + event.type); break; } break; default: return NOT_HANDLED; } return retValue; } // in Connected state private void processConnectionEvent(int state, BluetoothDevice device) { Log.d(TAG, "processConnectionEvent state = " + state + ", device = " + device); switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mConnectedDevicesList.contains(device)) { processWBSEvent(0, device); /* disable WBS audio parameters */ synchronized (HeadsetStateMachine.this) { mConnectedDevicesList.remove(device); mHeadsetAudioParam.remove(device); mHeadsetBrsf.remove(device); Log.d(TAG, "device " + device.getAddress() + " is removed in Connected state"); if (mConnectedDevicesList.size() == 0) { mCurrentDevice = null; transitionTo(mDisconnected); } else { processMultiHFConnected(device); } } broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); } else { Log.e(TAG, "Disconnected from unknown device: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED: processSlcConnected(); break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: if (mConnectedDevicesList.contains(device)) { mIncomingDevice = null; mTargetDevice = null; break; } Log.w(TAG, "HFP to be Connected in Connected state"); if (okToConnect(device) && (mConnectedDevicesList.size() < max_hf_connections)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { if(!mConnectedDevicesList.contains(device)) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in Connected state"); } transitionTo(mConnected); } configAudioParameters(device); } else { // reject the connection and stay in Connected state itself Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device) + " bondState=" + device.getBondState()); disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } break; default: Log.e(TAG, "Connection State Device: " + device + " bad state: " + state); break; } } // in Connected state private void processAudioEvent(int state, BluetoothDevice device) { if (!mConnectedDevicesList.contains(device)) { Log.e(TAG, "Audio changed on disconnected device: " + device); return; } switch (state) { case HeadsetHalConstants.AUDIO_STATE_CONNECTED: if (!isScoAcceptable()) { Log.e(TAG,"Audio Connected without any listener"); disconnectAudioNative(getByteAddress(device)); break; } // TODO(BT) should I save the state for next broadcast as the prevState? mAudioState = BluetoothHeadset.STATE_AUDIO_CONNECTED; setAudioParameters(device); /*Set proper Audio Paramters.*/ mAudioManager.setBluetoothScoOn(true); broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING); mActiveScoDevice = device; transitionTo(mAudioOn); break; case HeadsetHalConstants.AUDIO_STATE_CONNECTING: mAudioState = BluetoothHeadset.STATE_AUDIO_CONNECTING; broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); break; // TODO(BT) process other states default: Log.e(TAG, "Audio State Device: " + device + " bad state: " + state); break; } } private void processSlcConnected() { if (mPhoneProxy != null) { try { mPhoneProxy.queryPhoneState(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for query phone state"); } } private void processMultiHFConnected(BluetoothDevice device) { log("Connect state: processMultiHFConnected"); if (mActiveScoDevice != null && mActiveScoDevice.equals(device)) { log ("mActiveScoDevice is disconnected, setting it to null"); mActiveScoDevice = null; } /* Assign the current activedevice again if the disconnected device equals to the current active device */ if (mCurrentDevice != null && mCurrentDevice.equals(device)) { transitionTo(mConnected); int deviceSize = mConnectedDevicesList.size(); mCurrentDevice = mConnectedDevicesList.get(deviceSize-1); } else { // The disconnected device is not current active device transitionTo(mConnected); } log("processMultiHFConnected , the latest mCurrentDevice is:" + mCurrentDevice); log("Connect state: processMultiHFConnected ," + "fake broadcasting for mCurrentDevice"); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); } } private class AudioOn extends State { @Override public void enter() { log("Enter AudioOn: " + getCurrentMessage().what + ", size: " + mConnectedDevicesList.size()); } @Override public boolean processMessage(Message message) { log("AudioOn process message: " + message.what + ", size: " + mConnectedDevicesList.size()); if (DBG) { if (mConnectedDevicesList.size() == 0) { log("ERROR: mConnectedDevicesList is empty in AudioOn"); return NOT_HANDLED; } } boolean retValue = HANDLED; switch(message.what) { case CONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (device == null) { break; } if (mConnectedDevicesList.contains(device)) { break; } if (max_hf_connections == 1) { deferMessage(obtainMessage(DISCONNECT, mCurrentDevice)); deferMessage(obtainMessage(CONNECT, device)); if (disconnectAudioNative(getByteAddress(mCurrentDevice))) { Log.d(TAG, "Disconnecting SCO audio for device = " + mCurrentDevice); } else { Log.e(TAG, "disconnectAudioNative failed"); } break; } if (mConnectedDevicesList.size() >= max_hf_connections) { BluetoothDevice DisconnectConnectedDevice = null; IState CurrentAudioState = getCurrentState(); Log.d(TAG, "Reach to max size, disconnect " + "one of them first"); DisconnectConnectedDevice = mConnectedDevicesList.get(0); if (mActiveScoDevice.equals(DisconnectConnectedDevice)) { DisconnectConnectedDevice = mConnectedDevicesList.get(1); } broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!disconnectHfpNative(getByteAddress(DisconnectConnectedDevice))) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } else { broadcastConnectionState(DisconnectConnectedDevice, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; mMultiDisconnectDevice = DisconnectConnectedDevice; transitionTo(mMultiHFPending); DisconnectConnectedDevice = null; } } else if(mConnectedDevicesList.size() < max_hf_connections) { broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!connectHfpNative(getByteAddress(device))) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; // Transtion to MultilHFPending state for Multi handsfree connection transitionTo(mMultiHFPending); } } Message m = obtainMessage(CONNECT_TIMEOUT); m.obj = device; sendMessageDelayed(m, 30000); } break; case CONNECT_TIMEOUT: onConnectionStateChanged(HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, getByteAddress(mTargetDevice)); break; case DISCONNECT: { BluetoothDevice device = (BluetoothDevice)message.obj; if (!mConnectedDevicesList.contains(device)) { break; } if (mActiveScoDevice != null && mActiveScoDevice.equals(device)) { // The disconnected device is active SCO device Log.d(TAG, "AudioOn, the disconnected device" + "is active SCO device"); deferMessage(obtainMessage(DISCONNECT, message.obj)); // Disconnect BT SCO first if (disconnectAudioNative(getByteAddress(mActiveScoDevice))) { log("Disconnecting SCO audio"); } else { // if disconnect BT SCO failed, transition to mConnected state transitionTo(mConnected); } } else { /* Do not disconnect BT SCO if the disconnected device is not active SCO device */ Log.d(TAG, "AudioOn, the disconnected device" + "is not active SCO device"); broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); // Should be still in AudioOn state if (!disconnectHfpNative(getByteAddress(device))) { Log.w(TAG, "AudioOn, disconnect device failed"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTING); break; } /* Transtion to MultiHFPending state for Multi handsfree connection */ if (mConnectedDevicesList.size() > 1) { mMultiDisconnectDevice = device; transitionTo(mMultiHFPending); } } } break; case DISCONNECT_AUDIO: if (mActiveScoDevice != null) { if (disconnectAudioNative(getByteAddress(mActiveScoDevice))) { log("Disconnecting SCO audio for device = " + mActiveScoDevice); } else { Log.e(TAG, "disconnectAudioNative failed" + "for device = " + mActiveScoDevice); } } break; case VOICE_RECOGNITION_START: processLocalVrEvent(HeadsetHalConstants.VR_STATE_STARTED); break; case VOICE_RECOGNITION_STOP: processLocalVrEvent(HeadsetHalConstants.VR_STATE_STOPPED); break; case INTENT_SCO_VOLUME_CHANGED: if (mActiveScoDevice != null) { processIntentScoVolume((Intent) message.obj, mActiveScoDevice); } break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj, ((message.arg1 == 1)?true:false)); break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); 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; clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } 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; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } break; case START_VR_TIMEOUT: { if (mWaitingForVoiceRecognition) { BluetoothDevice device = (BluetoothDevice)message.obj; mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice recognition" + "to start"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } break; case STACK_EVENT: StackEvent event = (StackEvent) message.obj; if (DBG) { log("event type: " + event.type); } switch (event.type) { case EVENT_TYPE_CONNECTION_STATE_CHANGED: BluetoothDevice device1 = getDeviceForMessage(CONNECT_TIMEOUT); if (device1 != null && device1.equals(event.device)) { Log.d(TAG, "remove connect timeout for device = " + device1); removeMessages(CONNECT_TIMEOUT); } processConnectionEvent(event.valueInt, event.device); break; case EVENT_TYPE_AUDIO_STATE_CHANGED: processAudioEvent(event.valueInt, event.device); break; case EVENT_TYPE_VR_STATE_CHANGED: processVrEvent(event.valueInt, event.device); break; case EVENT_TYPE_ANSWER_CALL: processAnswerCall(event.device); break; case EVENT_TYPE_HANGUP_CALL: processHangupCall(event.device); break; case EVENT_TYPE_VOLUME_CHANGED: processVolumeEvent(event.valueInt, event.valueInt2, event.device); break; case EVENT_TYPE_DIAL_CALL: processDialCall(event.valueString, event.device); break; case EVENT_TYPE_SEND_DTMF: processSendDtmf(event.valueInt, event.device); break; case EVENT_TYPE_NOICE_REDUCTION: processNoiceReductionEvent(event.valueInt, event.device); break; case EVENT_TYPE_AT_CHLD: processAtChld(event.valueInt, event.device); break; case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST: processSubscriberNumberRequest(event.device); break; case EVENT_TYPE_AT_CIND: processAtCind(event.device); break; case EVENT_TYPE_AT_COPS: processAtCops(event.device); break; case EVENT_TYPE_AT_CLCC: processAtClcc(event.device); break; case EVENT_TYPE_UNKNOWN_AT: processUnknownAt(event.valueString, event.device); break; case EVENT_TYPE_KEY_PRESSED: processKeyPressed(event.device); break; default: Log.e(TAG, "Unknown stack event: " + event.type); break; } break; default: return NOT_HANDLED; } return retValue; } // in AudioOn state. Some headsets disconnect RFCOMM prior to SCO down. Handle this private void processConnectionEvent(int state, BluetoothDevice device) { Log.d(TAG, "processConnectionEvent state = " + state + ", device = " + device); switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mConnectedDevicesList.contains(device)) { if (mActiveScoDevice != null && mActiveScoDevice.equals(device)&& mAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { processAudioEvent( HeadsetHalConstants.AUDIO_STATE_DISCONNECTED, device); } synchronized (HeadsetStateMachine.this) { mConnectedDevicesList.remove(device); mHeadsetAudioParam.remove(device); mHeadsetBrsf.remove(device); Log.d(TAG, "device " + device.getAddress() + " is removed in AudioOn state"); broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); processWBSEvent(0, device); /* disable WBS audio parameters */ if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { processMultiHFConnected(device); } } } else { Log.e(TAG, "Disconnected from unknown device: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED: processSlcConnected(); break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: if (mConnectedDevicesList.contains(device)) { mIncomingDevice = null; mTargetDevice = null; break; } Log.w(TAG, "HFP to be Connected in AudioOn state"); if (okToConnect(device) && (mConnectedDevicesList.size() < max_hf_connections) ) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { if (!mConnectedDevicesList.contains(device)) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in AudioOn state"); } } configAudioParameters(device); } else { // reject the connection and stay in Connected state itself Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device) + " bondState=" + device.getBondState()); disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } break; default: Log.e(TAG, "Connection State Device: " + device + " bad state: " + state); break; } } // in AudioOn state private void processAudioEvent(int state, BluetoothDevice device) { if (!mConnectedDevicesList.contains(device)) { Log.e(TAG, "Audio changed on disconnected device: " + device); return; } switch (state) { case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED: if (mAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED; mAudioManager.setBluetoothScoOn(false); broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED); } transitionTo(mConnected); break; case HeadsetHalConstants.AUDIO_STATE_DISCONNECTING: // TODO(BT) adding STATE_AUDIO_DISCONNECTING in BluetoothHeadset? //broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_DISCONNECTING, // BluetoothHeadset.STATE_AUDIO_CONNECTED); break; default: Log.e(TAG, "Audio State Device: " + device + " bad state: " + state); break; } } private void processSlcConnected() { if (mPhoneProxy != null) { try { mPhoneProxy.queryPhoneState(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for query phone state"); } } private void processIntentScoVolume(Intent intent, BluetoothDevice device) { int volumeValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); if (mPhoneState.getSpeakerVolume() != volumeValue) { mPhoneState.setSpeakerVolume(volumeValue); setVolumeNative(HeadsetHalConstants.VOLUME_TYPE_SPK, volumeValue, getByteAddress(device)); } } private void processMultiHFConnected(BluetoothDevice device) { log("AudioOn state: processMultiHFConnected"); /* Assign the current activedevice again if the disconnected device equals to the current active device */ if (mCurrentDevice != null && mCurrentDevice.equals(device)) { int deviceSize = mConnectedDevicesList.size(); mCurrentDevice = mConnectedDevicesList.get(deviceSize-1); } if (mAudioState != BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mConnected); log("processMultiHFConnected , the latest mCurrentDevice is:" + mCurrentDevice); log("AudioOn state: processMultiHFConnected ," + "fake broadcasting for mCurrentDevice"); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); } } /* Add MultiHFPending state when atleast 1 HS is connected and disconnect/connect new HS */ private class MultiHFPending extends State { @Override public void enter() { log("Enter MultiHFPending: " + getCurrentMessage().what + ", size: " + mConnectedDevicesList.size()); } @Override public boolean processMessage(Message message) { log("MultiHFPending process message: " + message.what + ", size: " + mConnectedDevicesList.size()); boolean retValue = HANDLED; switch(message.what) { case CONNECT: deferMessage(message); break; case CONNECT_AUDIO: if (mCurrentDevice != null) { connectAudioNative(getByteAddress(mCurrentDevice)); } break; case CONNECT_TIMEOUT: onConnectionStateChanged(HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED, getByteAddress(mTargetDevice)); break; case DISCONNECT_AUDIO: if (mActiveScoDevice != null) { if (disconnectAudioNative(getByteAddress(mActiveScoDevice))) { Log.d(TAG, "MultiHFPending, Disconnecting SCO audio for " + mActiveScoDevice); } else { Log.e(TAG, "disconnectAudioNative failed" + "for device = " + mActiveScoDevice); } } break; case DISCONNECT: BluetoothDevice device = (BluetoothDevice) message.obj; if (mConnectedDevicesList.contains(device) && mTargetDevice != null && mTargetDevice.equals(device)) { // cancel connection to the mTargetDevice broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; } } else { deferMessage(message); } break; case VOICE_RECOGNITION_START: device = (BluetoothDevice) message.obj; if (mConnectedDevicesList.contains(device)) { processLocalVrEvent(HeadsetHalConstants.VR_STATE_STARTED); } break; case VOICE_RECOGNITION_STOP: device = (BluetoothDevice) message.obj; if (mConnectedDevicesList.contains(device)) { processLocalVrEvent(HeadsetHalConstants.VR_STATE_STOPPED); } break; case INTENT_SCO_VOLUME_CHANGED: if (mActiveScoDevice != null) { processIntentScoVolume((Intent) message.obj, mActiveScoDevice); } break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj, ((message.arg1 == 1)?true:false)); break; case DEVICE_STATE_CHANGED: processDeviceStateChanged((HeadsetDeviceState) message.obj); break; case SEND_CCLC_RESPONSE: processSendClccResponse((HeadsetClccResponse) message.obj); break; case CLCC_RSP_TIMEOUT: { device = (BluetoothDevice) message.obj; clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } break; case DIALING_OUT_TIMEOUT: if (mDialingOut) { device = (BluetoothDevice) message.obj; mDialingOut= false; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } break; case VIRTUAL_CALL_START: device = (BluetoothDevice) message.obj; if(mConnectedDevicesList.contains(device)) { initiateScoUsingVirtualVoiceCall(); } break; case VIRTUAL_CALL_STOP: device = (BluetoothDevice) message.obj; if (mConnectedDevicesList.contains(device)) { terminateScoUsingVirtualVoiceCall(); } break; case START_VR_TIMEOUT: if (mWaitingForVoiceRecognition) { device = (BluetoothDevice) message.obj; mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice" + "recognition to start"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } break; case STACK_EVENT: StackEvent event = (StackEvent) message.obj; if (DBG) { log("event type: " + event.type); } switch (event.type) { case EVENT_TYPE_CONNECTION_STATE_CHANGED: BluetoothDevice device1 = getDeviceForMessage(CONNECT_TIMEOUT); if (device1 != null && device1.equals(event.device)) { Log.d(TAG, "remove connect timeout for device = " + device1); removeMessages(CONNECT_TIMEOUT); } processConnectionEvent(event.valueInt, event.device); break; case EVENT_TYPE_AUDIO_STATE_CHANGED: processAudioEvent(event.valueInt, event.device); break; case EVENT_TYPE_VR_STATE_CHANGED: processVrEvent(event.valueInt,event.device); break; case EVENT_TYPE_ANSWER_CALL: //TODO(BT) could answer call happen on Connected state? processAnswerCall(event.device); break; case EVENT_TYPE_HANGUP_CALL: // TODO(BT) could hangup call happen on Connected state? processHangupCall(event.device); break; case EVENT_TYPE_VOLUME_CHANGED: processVolumeEvent(event.valueInt, event.valueInt2, event.device); break; case EVENT_TYPE_DIAL_CALL: processDialCall(event.valueString, event.device); break; case EVENT_TYPE_SEND_DTMF: processSendDtmf(event.valueInt, event.device); break; case EVENT_TYPE_NOICE_REDUCTION: processNoiceReductionEvent(event.valueInt, event.device); break; case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST: processSubscriberNumberRequest(event.device); break; case EVENT_TYPE_AT_CIND: processAtCind(event.device); break; case EVENT_TYPE_AT_CHLD: processAtChld(event.valueInt, event.device); break; case EVENT_TYPE_AT_COPS: processAtCops(event.device); break; case EVENT_TYPE_AT_CLCC: processAtClcc(event.device); break; case EVENT_TYPE_UNKNOWN_AT: processUnknownAt(event.valueString,event.device); break; case EVENT_TYPE_KEY_PRESSED: processKeyPressed(event.device); break; default: Log.e(TAG, "Unexpected event: " + event.type); break; } break; default: return NOT_HANDLED; } return retValue; } // in MultiHFPending state private void processConnectionEvent(int state, BluetoothDevice device) { Log.d(TAG, "processConnectionEvent state = " + state + ", device = " + device); switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mConnectedDevicesList.contains(device)) { if (mMultiDisconnectDevice != null && mMultiDisconnectDevice.equals(device)) { mMultiDisconnectDevice = null; synchronized (HeadsetStateMachine.this) { mConnectedDevicesList.remove(device); mHeadsetAudioParam.remove(device); mHeadsetBrsf.remove(device); Log.d(TAG, "device " + device.getAddress() + " is removed in MultiHFPending state"); broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); } if (mTargetDevice != null) { if (!connectHfpNative(getByteAddress(mTargetDevice))) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; if (mConnectedDevicesList.size() == 0) { // Should be not in this state since it has at least // one HF connected in MultiHFPending state Log.d(TAG, "Should be not in this state, error handling"); transitionTo(mDisconnected); } else { processMultiHFConnected(device); } } } } else { synchronized (HeadsetStateMachine.this) { mIncomingDevice = null; if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { processMultiHFConnected(device); } } } } else { /* Another HF disconnected when one HF is connecting */ synchronized (HeadsetStateMachine.this) { mConnectedDevicesList.remove(device); mHeadsetAudioParam.remove(device); mHeadsetBrsf.remove(device); Log.d(TAG, "device " + device.getAddress() + " is removed in MultiHFPending state"); } broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; if (mConnectedDevicesList.size() == 0) { transitionTo(mDisconnected); } else { if (mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mAudioOn); else transitionTo(mConnected); } } } else { Log.e(TAG, "Unknown device Disconnected: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: /* Outgoing disconnection for device failed */ if (mConnectedDevicesList.contains(device)) { broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTING); if (mTargetDevice != null) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); } synchronized (HeadsetStateMachine.this) { mTargetDevice = null; if (mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mAudioOn); else transitionTo(mConnected); } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in MultiHFPending state"); mTargetDevice = null; if (mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mAudioOn); else transitionTo(mConnected); } broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); configAudioParameters(device); } else { Log.w(TAG, "Some other incoming HF connected" + "in Multi Pending state"); if (okToConnect(device) && (mConnectedDevicesList.size() < max_hf_connections)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { if (!mConnectedDevicesList.contains(device)) { mCurrentDevice = device; mConnectedDevicesList.add(device); Log.d(TAG, "device " + device.getAddress() + " is added in MultiHFPending state"); } } configAudioParameters(device); } else { // reject the connection and stay in Pending state itself Log.i(TAG,"Incoming Hf rejected. priority=" + mService.getPriority(device) + " bondState=" + device.getBondState()); disconnectHfpNative(getByteAddress(device)); // the other profile connection should be initiated AdapterService adapterService = AdapterService.getAdapterService(); if (adapterService != null) { adapterService.connectOtherProfile(device, AdapterService.PROFILE_CONN_REJECTED); } } } break; case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED: processSlcConnected(); break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: if (mConnectedDevicesList.contains(device)) { Log.e(TAG, "current device tries to connect back"); } else if (mTargetDevice != null && mTargetDevice.equals(device)) { if (DBG) { log("Stack and target device are connecting"); } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { Log.e(TAG, "Another connecting event on" + "the incoming device"); } break; case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTING: if (mConnectedDevicesList.contains(device)) { if (DBG) { log("stack is disconnecting mCurrentDevice"); } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { Log.e(TAG, "TargetDevice is getting disconnected"); } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { Log.e(TAG, "IncomingDevice is getting disconnected"); } else { Log.e(TAG, "Disconnecting unknow device: " + device); } break; default: Log.e(TAG, "Incorrect state: " + state); break; } } private void processAudioEvent(int state, BluetoothDevice device) { if (!mConnectedDevicesList.contains(device)) { Log.e(TAG, "Audio changed on disconnected device: " + device); return; } switch (state) { case HeadsetHalConstants.AUDIO_STATE_CONNECTED: if (!isScoAcceptable()) { Log.e(TAG,"Audio Connected without any listener"); disconnectAudioNative(getByteAddress(device)); break; } mAudioState = BluetoothHeadset.STATE_AUDIO_CONNECTED; setAudioParameters(device); /* Set proper Audio Parameters. */ mAudioManager.setBluetoothScoOn(true); mActiveScoDevice = device; broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING); /* The state should be still in MultiHFPending state when audio connected since other device is still connecting/ disconnecting */ break; case HeadsetHalConstants.AUDIO_STATE_CONNECTING: mAudioState = BluetoothHeadset.STATE_AUDIO_CONNECTING; broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); break; case HeadsetHalConstants.AUDIO_STATE_DISCONNECTED: if (mAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { mAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED; mAudioManager.setBluetoothScoOn(false); broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED); } /* The state should be still in MultiHFPending state when audio disconnected since other device is still connecting/ disconnecting */ break; default: Log.e(TAG, "Audio State Device: " + device + " bad state: " + state); break; } } private void processSlcConnected() { if (mPhoneProxy != null) { try { mPhoneProxy.queryPhoneState(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for query phone state"); } } private void processMultiHFConnected(BluetoothDevice device) { log("MultiHFPending state: processMultiHFConnected"); if (mActiveScoDevice != null && mActiveScoDevice.equals(device)) { log ("mActiveScoDevice is disconnected, setting it to null"); mActiveScoDevice = null; } /* Assign the current activedevice again if the disconnected device equals to the current active device */ if (mCurrentDevice != null && mCurrentDevice.equals(device)) { int deviceSize = mConnectedDevicesList.size(); mCurrentDevice = mConnectedDevicesList.get(deviceSize-1); } // The disconnected device is not current active device if (mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) transitionTo(mAudioOn); else transitionTo(mConnected); log("processMultiHFConnected , the latest mCurrentDevice is:" + mCurrentDevice); log("MultiHFPending state: processMultiHFConnected ," + "fake broadcasting for mCurrentDevice"); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); } private void processIntentScoVolume(Intent intent, BluetoothDevice device) { int volumeValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); if (mPhoneState.getSpeakerVolume() != volumeValue) { mPhoneState.setSpeakerVolume(volumeValue); setVolumeNative(HeadsetHalConstants.VOLUME_TYPE_SPK, volumeValue, getByteAddress(device)); } } } private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if (DBG) Log.d(TAG, "Proxy object connected"); mPhoneProxy = IBluetoothHeadsetPhone.Stub.asInterface(service); } public void onServiceDisconnected(ComponentName className) { if (DBG) Log.d(TAG, "Proxy object disconnected"); mPhoneProxy = null; } }; // HFP Connection state of the device could be changed by the state machine // in separate thread while this method is executing. int getConnectionState(BluetoothDevice device) { if (getCurrentState() == mDisconnected) { if (DBG) Log.d(TAG, "currentState is Disconnected"); return BluetoothProfile.STATE_DISCONNECTED; } synchronized (this) { IState currentState = getCurrentState(); if (DBG) Log.d(TAG, "currentState = " + currentState); if (currentState == mPending) { if ((mTargetDevice != null) && mTargetDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; } if (mConnectedDevicesList.contains(device)) { return BluetoothProfile.STATE_DISCONNECTING; } if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; // incoming connection } return BluetoothProfile.STATE_DISCONNECTED; } if (currentState == mMultiHFPending) { if ((mTargetDevice != null) && mTargetDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; } if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; // incoming connection } if (mConnectedDevicesList.contains(device)) { if ((mMultiDisconnectDevice != null) && (!mMultiDisconnectDevice.equals(device))) { // The device is still connected return BluetoothProfile.STATE_CONNECTED; } return BluetoothProfile.STATE_DISCONNECTING; } return BluetoothProfile.STATE_DISCONNECTED; } if (currentState == mConnected || currentState == mAudioOn) { if (mConnectedDevicesList.contains(device)) { return BluetoothProfile.STATE_CONNECTED; } return BluetoothProfile.STATE_DISCONNECTED; } else { Log.e(TAG, "Bad currentState: " + currentState); return BluetoothProfile.STATE_DISCONNECTED; } } } List getConnectedDevices() { List devices = new ArrayList(); synchronized(this) { for (int i = 0; i < mConnectedDevicesList.size(); i++) devices.add(mConnectedDevicesList.get(i)); } return devices; } boolean isAudioOn() { return (getCurrentState() == mAudioOn); } boolean isAudioConnected(BluetoothDevice device) { synchronized(this) { /* Additional check for audio state included for the case when PhoneApp queries Bluetooth Audio state, before we receive the close event from the stack for the sco disconnect issued in AudioOn state. This was causing a mismatch in the Incall screen UI. */ if (getCurrentState() == mAudioOn && mCurrentDevice.equals(device) && mAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { return true; } } return false; } public void setAudioRouteAllowed(boolean allowed) { mAudioRouteAllowed = allowed; } public boolean getAudioRouteAllowed() { return mAudioRouteAllowed; } int getAudioState(BluetoothDevice device) { synchronized(this) { if (mConnectedDevicesList.size() == 0) { return BluetoothHeadset.STATE_AUDIO_DISCONNECTED; } } return mAudioState; } 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(sVoiceCommandIntent); } catch (ActivityNotFoundException e) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); return; } expectVoiceRecognition(device); } else { // send error response if call is ongoing atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); return; } } else if (state == HeadsetHalConstants.VR_STATE_STOPPED) { if (mVoiceRecognitionStarted || mWaitingForVoiceRecognition) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(device)); mVoiceRecognitionStarted = false; mWaitingForVoiceRecognition = false; if (!isInCall() && (mActiveScoDevice != null)) { disconnectAudioNative(getByteAddress(mActiveScoDevice)); mAudioManager.setParameters("A2dpSuspended=false"); } } else { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } else { Log.e(TAG, "Bad Voice Recognition state: " + state); } } private void processLocalVrEvent(int state) { 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; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(device)); removeMessages(START_VR_TIMEOUT); } else { Log.d(TAG, "Voice recognition started locally"); needAudio = startVoiceRecognitionNative(getByteAddress(mCurrentDevice)); if (mCurrentDevice != null) device = mCurrentDevice; } if (needAudio && !isAudioOn()) { 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 mAudioManager.setParameters("A2dpSuspended=true"); if (device != null) { connectAudioNative(getByteAddress(device)); } else { Log.e(TAG, "device not found for VR"); } } if (mStartVoiceRecognitionWakeLock.isHeld()) { mStartVoiceRecognitionWakeLock.release(); } } else { Log.d(TAG, "Voice Recognition stopped. mVoiceRecognitionStarted: " + mVoiceRecognitionStarted + " mWaitingForVoiceRecognition: " + mWaitingForVoiceRecognition); if (mVoiceRecognitionStarted || mWaitingForVoiceRecognition) { mVoiceRecognitionStarted = false; mWaitingForVoiceRecognition = false; if (stopVoiceRecognitionNative(getByteAddress(mCurrentDevice)) && !isInCall() && mActiveScoDevice != null) { disconnectAudioNative(getByteAddress(mActiveScoDevice)); mAudioManager.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 (!mStartVoiceRecognitionWakeLock.isHeld()) { mStartVoiceRecognitionWakeLock.acquire(START_VR_TIMEOUT_VALUE); } } List getDevicesMatchingConnectionStates(int[] states) { List deviceList = new ArrayList(); Set bondedDevices = mAdapter.getBondedDevices(); int connectionState; synchronized (this) { for (BluetoothDevice device : bondedDevices) { ParcelUuid[] featureUuids = device.getUuids(); if (!BluetoothUuid.containsAnyUuid(featureUuids, HEADSET_UUIDS)) { continue; } connectionState = getConnectionState(device); for(int i = 0; i < states.length; i++) { if (connectionState == states[i]) { deviceList.add(device); } } } } return deviceList; } private BluetoothDevice getDeviceForMessage(int what) { if (what == CONNECT_TIMEOUT) { log("getDeviceForMessage: returning mTargetDevice for what=" + what); return mTargetDevice; } if (mConnectedDevicesList.size() == 0) { log("getDeviceForMessage: No connected device. what=" + what); return null; } for (BluetoothDevice device : mConnectedDevicesList) { if (getHandler().hasMessages(what, device)) { log("getDeviceForMessage: returning " + device); return device; } } log("getDeviceForMessage: No matching device for " + what + ". Returning null"); return null; } private BluetoothDevice getMatchingDevice(BluetoothDevice device) { for (BluetoothDevice matchingDevice : mConnectedDevicesList) { if (matchingDevice.equals(device)) { return matchingDevice; } } return null; } // This method does not check for error conditon (newState == prevState) private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) { log("Connection state " + device + ": " + prevState + "->" + newState); if(prevState == BluetoothProfile.STATE_CONNECTED) { // Headset is disconnecting, stop Virtual call if active. terminateScoUsingVirtualVoiceCall(); } /* Notifying the connection state change of the profile before sending the intent for connection state change, as it was causing a race condition, with the UI not being updated with the correct connection state. */ mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.HEADSET, newState, prevState); Intent intent = new Intent(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); intent.putExtra(BluetoothProfile.EXTRA_STATE, newState); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM); } private void broadcastAudioState(BluetoothDevice device, int newState, int prevState) { if(prevState == 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, prevState); intent.putExtra(BluetoothProfile.EXTRA_STATE, newState); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); mService.sendBroadcastAsUser(intent, UserHandle.ALL, HeadsetService.BLUETOOTH_PERM); log("Audio state " + device + ": " + prevState + "->" + newState); } /* * 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 HashMap AudioParamConfig = new HashMap(); AudioParamConfig.put("NREC", 1); mHeadsetAudioParam.put(device, AudioParamConfig); mAudioManager.setParameters(HEADSET_NAME + "=" + getCurrentDeviceName(device) + ";" + HEADSET_NREC + "=on"); Log.d(TAG, "configAudioParameters for device:" + device + " are: nrec = " + AudioParamConfig.get("NREC")); } private void setAudioParameters(BluetoothDevice device) { // 1. update nrec value // 2. update headset name int mNrec = 0; HashMap AudioParam = mHeadsetAudioParam.get(device); if (AudioParam != null && !AudioParam.isEmpty()) { mNrec = AudioParam.get("NREC"); } else { Log.e(TAG,"setAudioParameters: AudioParam not found"); } if (mNrec == 1) { Log.d(TAG, "Set NREC: 1 for device:" + device); mAudioManager.setParameters(HEADSET_NREC + "=on"); } else { Log.d(TAG, "Set NREC: 0 for device:" + device); mAudioManager.setParameters(HEADSET_NREC + "=off"); } mAudioManager.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 = mPhonebook.TYPE_UNKNOWN; String atString = null; atCommand = atCommand.trim(); if (atCommand.length() > 5) { atString = atCommand.substring(5); if (atString.startsWith("?")) // Read commandType = mPhonebook.TYPE_READ; else if (atString.startsWith("=?")) // Test commandType = mPhonebook.TYPE_TEST; else if (atString.startsWith("=")) // Set commandType = mPhonebook.TYPE_SET; else commandType = mPhonebook.TYPE_UNKNOWN; } return commandType; } /* Method to check if Virtual Call in Progress */ private boolean isVirtualCallInProgress() { return mVirtualCallStarted; } 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() */ synchronized boolean initiateScoUsingVirtualVoiceCall() { if (DBG) 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 if (DBG) log("initiateScoUsingVirtualVoiceCall: Done"); return true; } synchronized boolean terminateScoUsingVirtualVoiceCall() { if (DBG) log("terminateScoUsingVirtualVoiceCall: Received"); if (!isVirtualCallInProgress()) { Log.e(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 if (DBG) log("terminateScoUsingVirtualVoiceCall: Done"); return true; } private void processAnswerCall(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processAnswerCall device is null"); return; } if (mPhoneProxy != null) { try { mPhoneProxy.answerCall(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for answering call"); } } private void processHangupCall(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processHangupCall device is null"); return; } // Close the virtual call if active. Virtual call should be // terminated for CHUP callback event if (isVirtualCallInProgress()) { terminateScoUsingVirtualVoiceCall(); } else { if (mPhoneProxy != null) { try { mPhoneProxy.hangupCall(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for hanging up call"); } } } private void processDialCall(String number, BluetoothDevice device) { if(device == null) { Log.w(TAG, "processDialCall device is null"); return; } String dialNumber; if (mDialingOut) { if (DBG) log("processDialCall, already dialling"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); return; } if ((number == null) || (number.length() == 0)) { dialNumber = mPhonebook.getLastDialledNumber(); if (dialNumber == null) { if (DBG) log("processDialCall, last dial number null"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); return; } } else if (number.charAt(0) == '>') { // Yuck - memory dialling requested. // Just dial last number for now if (number.startsWith(">9999")) { // for PTS test atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); return; } if (DBG) log("processDialCall, memory dial do last dial for now"); dialNumber = mPhonebook.getLastDialledNumber(); if (dialNumber == null) { if (DBG) log("processDialCall, last dial number null"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); 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(device != null && !device.equals(mActiveScoDevice) && mPhoneState.isInCall()) { Log.w(TAG, "ignore processVolumeEvent"); return; } if (volumeType == HeadsetHalConstants.VOLUME_TYPE_SPK) { mPhoneState.setSpeakerVolume(volume); int flag = (getCurrentState() == mAudioOn) ? AudioManager.FLAG_SHOW_UI : 0; mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, volume, flag); } else if (volumeType == HeadsetHalConstants.VOLUME_TYPE_MIC) { mPhoneState.setMicVolume(volume); } else { Log.e(TAG, "Bad voluem type: " + volumeType); } } private void processSendDtmf(int dtmf, BluetoothDevice device) { if(device == null) { Log.w(TAG, "processSendDtmf device is null"); return; } if (mPhoneProxy != null) { try { mPhoneProxy.sendDtmf(dtmf); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for sending DTMF"); } } private void processCallState(HeadsetCallState callState) { processCallState(callState, false); } private void processCallState(HeadsetCallState callState, boolean isVirtualCall) { mPhoneState.setNumActiveCall(callState.mNumActive); mPhoneState.setNumHeldCall(callState.mNumHeld); mPhoneState.setCallState(callState.mCallState); if (mDialingOut) { if (callState.mCallState == HeadsetHalConstants.CALL_STATE_DIALING) { BluetoothDevice device = getDeviceForMessage(DIALING_OUT_TIMEOUT); if (device == null) { return; } atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(device)); removeMessages(DIALING_OUT_TIMEOUT); } else if (callState.mCallState == HeadsetHalConstants.CALL_STATE_ACTIVE || callState.mCallState == HeadsetHalConstants.CALL_STATE_IDLE) { mDialingOut = false; } } /* Set ActiveScoDevice to null when call ends */ if ((mActiveScoDevice != null) && !isInCall() && callState.mCallState == HeadsetHalConstants.CALL_STATE_IDLE) mActiveScoDevice = null; 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) { phoneStateChangeNative(callState.mNumActive, callState.mNumHeld, callState.mCallState, callState.mNumber, callState.mType); } } 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 (mPhoneState.getCallState() != callState.mCallState) { mPhoneState.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) { phoneStateChangeNative(callState.mNumActive, callState.mNumHeld, callState.mCallState, callState.mNumber, callState.mType); } } } } // 1 enable noice reduction // 0 disable noice reduction private void processNoiceReductionEvent(int enable, BluetoothDevice device) { HashMap AudioParamNrec = mHeadsetAudioParam.get(device); if (AudioParamNrec != null && !AudioParamNrec.isEmpty()) { if (enable == 1) AudioParamNrec.put("NREC", 1); else AudioParamNrec.put("NREC", 0); log("NREC value for device :" + device + " is: " + AudioParamNrec.get("NREC")); } else { Log.e(TAG,"processNoiceReductionEvent: AudioParamNrec is null "); } if (mActiveScoDevice != null && mActiveScoDevice.equals(device) && mAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED) { setAudioParameters(device); } } // 2 - WBS on // 1 - NBS on private void processWBSEvent(int enable, BluetoothDevice device) { if (enable == 2) { Log.d(TAG, "AudioManager.setParameters bt_wbs=on for " + device.getName() + " - " + device.getAddress()); mAudioManager.setParameters(HEADSET_WBS + "=on"); } else { Log.d(TAG, "AudioManager.setParameters bt_wbs=off for " + device.getName() + " - " + device.getAddress()); mAudioManager.setParameters(HEADSET_WBS + "=off"); } } private void processAtChld(int chld, BluetoothDevice device) { if(device == null) { Log.w(TAG, "processAtChld device is null"); return; } if (mPhoneProxy != null) { try { if (mPhoneProxy.processChld(chld)) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(device)); } else { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } else { Log.e(TAG, "Handsfree phone proxy null for At+Chld"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } private void processSubscriberNumberRequest(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processSubscriberNumberRequest device is null"); return; } if (mPhoneProxy != null) { try { String number = mPhoneProxy.getSubscriberNumber(); if (number != null) { atResponseStringNative("+CNUM: ,\"" + number + "\"," + PhoneNumberUtils.toaFromString(number) + ",,4", getByteAddress(device)); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(device)); } else { Log.e(TAG, "getSubscriberNumber returns null"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } else { Log.e(TAG, "Handsfree phone proxy null for At+CNUM"); } } private void processAtCind(BluetoothDevice device) { int call, call_setup; if(device == null) { Log.w(TAG, "processAtCind device is null"); return; } /* 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; call_setup = 0; } else { // regular phone call call = mPhoneState.getNumActiveCall(); call_setup = mPhoneState.getNumHeldCall(); } cindResponseNative(mPhoneState.getService(), call, call_setup, mPhoneState.getCallState(), mPhoneState.getSignal(), mPhoneState.getRoam(), mPhoneState.getBatteryCharge(), getByteAddress(device)); } private void processAtCops(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processAtCops device is null"); return; } if (mPhoneProxy != null) { try { String operatorName = mPhoneProxy.getNetworkOperator(); if (operatorName == null) { operatorName = ""; } copsResponseNative(operatorName, getByteAddress(device)); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); copsResponseNative("", getByteAddress(device)); } } else { Log.e(TAG, "Handsfree phone proxy null for At+COPS"); copsResponseNative("", getByteAddress(device)); } } private void processAtClcc(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processAtClcc device is null"); return; } if (mPhoneProxy != null) { try { if(isVirtualCallInProgress()) { String phoneNumber = ""; int type = PhoneNumberUtils.TOA_Unknown; try { phoneNumber = mPhoneProxy.getSubscriberNumber(); type = PhoneNumberUtils.toaFromString(phoneNumber); } catch (RemoteException ee) { Log.e(TAG, "Unable to retrieve phone number"+ "using IBluetoothHeadsetPhone proxy"); phoneNumber = ""; } clccResponseNative(1, 0, 0, 0, false, phoneNumber, type, getByteAddress(device)); clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } else if (!mPhoneProxy.listCurrentCalls()) { clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } else { Log.d(TAG, "Starting CLCC response timeout for device: " + device); Message m = obtainMessage(CLCC_RSP_TIMEOUT); m.obj = getMatchingDevice(device); sendMessageDelayed(m, CLCC_RSP_TIMEOUT_VALUE); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } } else { Log.e(TAG, "Handsfree phone proxy null for At+CLCC"); clccResponseNative(0, 0, 0, 0, false, "", 0, getByteAddress(device)); } } 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"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } 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"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } 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"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } /** * Find a character ch, ignoring quoted sections. * Return input.length() if not found. */ static private 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. */ static private Object[] generateArgs(String input) { int i = 0; int j; ArrayList out = new ArrayList(); 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(); } /** * @return {@code true} if the given string is a valid vendor-specific AT command. */ private boolean processVendorSpecificAt(String atString) { 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); return false; } 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); return false; } String arg = atString.substring(indexOfEqual + 1); if (arg.startsWith("?")) { Log.e(TAG, "processVendorSpecificAt: command type error in " + atString); return false; } Object[] args = generateArgs(arg); broadcastVendorSpecificEventIntent(command, companyId, BluetoothHeadset.AT_CMD_TYPE_SET, args, mCurrentDevice); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0, getByteAddress(mCurrentDevice)); return true; } private void processUnknownAt(String atString, BluetoothDevice device) { if(device == null) { Log.w(TAG, "processUnknownAt device is null"); return; } // TODO (BT) 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 if (!processVendorSpecificAt(atCommand)) atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } private void processKeyPressed(BluetoothDevice device) { if(device == null) { Log.w(TAG, "processKeyPressed device is null"); return; } if (mPhoneState.getCallState() == HeadsetHalConstants.CALL_STATE_INCOMING) { if (mPhoneProxy != null) { try { mPhoneProxy.answerCall(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for answering call"); } } else if (mPhoneState.getNumActiveCall() > 0) { if (!isAudioOn()) { connectAudioNative(getByteAddress(mCurrentDevice)); } else { if (mPhoneProxy != null) { try { mPhoneProxy.hangupCall(); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); } } else { Log.e(TAG, "Handsfree phone proxy null for hangup call"); } } } else { String dialNumber = mPhonebook.getLastDialledNumber(); if (dialNumber == null) { if (DBG) 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); } } private void onConnectionStateChanged(int state, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_CONNECTION_STATE_CHANGED); event.valueInt = state; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAudioStateChanged(int state, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_AUDIO_STATE_CHANGED); event.valueInt = state; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onVrStateChanged(int state, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_VR_STATE_CHANGED); event.valueInt = state; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAnswerCall(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_ANSWER_CALL); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onHangupCall(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_HANGUP_CALL); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onVolumeChanged(int type, int volume, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_VOLUME_CHANGED); event.valueInt = type; event.valueInt2 = volume; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onDialCall(String number, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_DIAL_CALL); event.valueString = number; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onSendDtmf(int dtmf, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_SEND_DTMF); event.valueInt = dtmf; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onNoiceReductionEnable(boolean enable, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_NOICE_REDUCTION); event.valueInt = enable ? 1 : 0; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onWBS(int codec, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_WBS); event.valueInt = codec; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAtChld(int chld, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_AT_CHLD); event.valueInt = chld; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAtCnum(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAtCind(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_AT_CIND); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAtCops(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_AT_COPS); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onAtClcc(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_AT_CLCC); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onUnknownAt(String atString, byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_UNKNOWN_AT); event.valueString = atString; event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void onKeyPressed(byte[] address) { StackEvent event = new StackEvent(EVENT_TYPE_KEY_PRESSED); event.device = getDevice(address); sendMessage(STACK_EVENT, event); } private void processIntentBatteryChanged(Intent intent) { int batteryLevel = intent.getIntExtra("level", -1); int scale = intent.getIntExtra("scale", -1); if (batteryLevel == -1 || scale == -1 || scale == 0) { Log.e(TAG, "Bad Battery Changed intent: " + batteryLevel + "," + scale); return; } batteryLevel = batteryLevel * 5 / scale; mPhoneState.setBatteryCharge(batteryLevel); } private void processDeviceStateChanged(HeadsetDeviceState deviceState) { notifyDeviceStatusNative(deviceState.mService, deviceState.mRoam, deviceState.mSignal, deviceState.mBatteryCharge); } private void processSendClccResponse(HeadsetClccResponse clcc) { BluetoothDevice device = getDeviceForMessage(CLCC_RSP_TIMEOUT); if (device == null) { return; } if (clcc.mIndex == 0) { removeMessages(CLCC_RSP_TIMEOUT); } clccResponseNative(clcc.mIndex, clcc.mDirection, clcc.mStatus, clcc.mMode, clcc.mMpty, clcc.mNumber, clcc.mType, getByteAddress(device)); } private void processSendVendorSpecificResultCode(HeadsetVendorSpecificResultCode resultCode) { String stringToSend = resultCode.mCommand + ": "; if (resultCode.mArg != null) { stringToSend += resultCode.mArg; } atResponseStringNative(stringToSend, getByteAddress(resultCode.mDevice)); } private String getCurrentDeviceName(BluetoothDevice device) { String defaultName = ""; if(device == null) { return defaultName; } String deviceName = device.getName(); if (deviceName == null) { return defaultName; } return deviceName; } private byte[] getByteAddress(BluetoothDevice device) { return Utils.getBytesFromAddress(device.getAddress()); } private BluetoothDevice getDevice(byte[] address) { return mAdapter.getRemoteDevice(Utils.getAddressStringFromByte(address)); } private boolean isInCall() { return ((mPhoneState.getNumActiveCall() > 0) || (mPhoneState.getNumHeldCall() > 0) || (mPhoneState.getCallState() != HeadsetHalConstants.CALL_STATE_IDLE)); } // Accept incoming SCO only when there is active call, VR activated, // active VOIP call private boolean isScoAcceptable() { return mAudioRouteAllowed && (mVoiceRecognitionStarted || isInCall()); } boolean isConnected() { IState currentState = getCurrentState(); return (currentState == mConnected || currentState == mAudioOn); } boolean okToConnect(BluetoothDevice device) { AdapterService adapterService = AdapterService.getAdapterService(); int priority = mService.getPriority(device); boolean ret = false; //check if this is an incoming connection in Quiet mode. if((adapterService == null) || ((adapterService.isQuietModeEnabled() == true) && (mTargetDevice == null))){ ret = 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 else if((BluetoothProfile.PRIORITY_OFF < priority) || ((BluetoothProfile.PRIORITY_UNDEFINED == priority) && (device.getBondState() != BluetoothDevice.BOND_NONE))){ ret= true; } return ret; } @Override protected void log(String msg) { if (DBG) { super.log(msg); } } public void handleAccessPermissionResult(Intent intent) { log("handleAccessPermissionResult"); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (mPhonebook != null) { 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) { atResponseCodeNative(atCommandResult, atCommandErrorCode, getByteAddress(device)); } else { log("handleAccessPermissionResult - RESULT_NONE"); } } else { Log.e(TAG, "Phonebook handle null"); if (device != null) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0, getByteAddress(device)); } } } private static final String SCHEME_TEL = "tel"; // Event types for STACK_EVENT message final private static int EVENT_TYPE_NONE = 0; final private static int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1; final private static int EVENT_TYPE_AUDIO_STATE_CHANGED = 2; final private static int EVENT_TYPE_VR_STATE_CHANGED = 3; final private static int EVENT_TYPE_ANSWER_CALL = 4; final private static int EVENT_TYPE_HANGUP_CALL = 5; final private static int EVENT_TYPE_VOLUME_CHANGED = 6; final private static int EVENT_TYPE_DIAL_CALL = 7; final private static int EVENT_TYPE_SEND_DTMF = 8; final private static int EVENT_TYPE_NOICE_REDUCTION = 9; final private static int EVENT_TYPE_AT_CHLD = 10; final private static int EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST = 11; final private static int EVENT_TYPE_AT_CIND = 12; final private static int EVENT_TYPE_AT_COPS = 13; final private static int EVENT_TYPE_AT_CLCC = 14; final private static int EVENT_TYPE_UNKNOWN_AT = 15; final private static int EVENT_TYPE_KEY_PRESSED = 16; final private static int EVENT_TYPE_WBS = 17; private class StackEvent { int type = EVENT_TYPE_NONE; int valueInt = 0; int valueInt2 = 0; String valueString = null; BluetoothDevice device = null; private StackEvent(int type) { this.type = type; } } /*package*/native boolean atResponseCodeNative(int responseCode, int errorCode, byte[] address); /*package*/ native boolean atResponseStringNative(String responseString, byte[] address); private native static void classInitNative(); private native void initializeNative(int max_hf_clients); private native void cleanupNative(); private native boolean connectHfpNative(byte[] address); private native boolean disconnectHfpNative(byte[] address); private native boolean connectAudioNative(byte[] address); private native boolean disconnectAudioNative(byte[] address); private native boolean startVoiceRecognitionNative(byte[] address); private native boolean stopVoiceRecognitionNative(byte[] address); private native boolean setVolumeNative(int volumeType, int volume, byte[] address); private native boolean cindResponseNative(int service, int numActive, int numHeld, int callState, int signal, int roam, int batteryCharge, byte[] address); private native boolean notifyDeviceStatusNative(int networkState, int serviceType, int signal, int batteryCharge); private native boolean clccResponseNative(int index, int dir, int status, int mode, boolean mpty, String number, int type, byte[] address); private native boolean copsResponseNative(String operatorName, byte[] address); private native boolean phoneStateChangeNative(int numActive, int numHeld, int callState, String number, int type); private native boolean configureWBSNative(byte[] address,int condec_config); }