/* * Copyright (C) 2012 Google Inc. */ /** * 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.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.Message; import android.os.ParcelUuid; import android.os.RemoteException; import android.os.ServiceManager; import android.os.PowerManager; 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.internal.util.IState; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.util.ArrayList; import java.util.List; import java.util.Set; final class HeadsetStateMachine extends StateMachine { private static final String TAG = "HeadsetStateMachine"; private static final boolean DBG = true; //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"; 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 ROAM_CHANGED = 12; static final int SEND_CCLC_RESPONSE = 13; 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 CONNECT_TIMEOUT = 201; private static final int DIALING_OUT_TIMEOUT_VALUE = 10000; private static final int START_VR_TIMEOUT_VALUE = 5000; private static final ParcelUuid[] HEADSET_UUIDS = { BluetoothUuid.HSP, BluetoothUuid.Handsfree, }; private Disconnected mDisconnected; private Pending mPending; private Connected mConnected; private AudioOn mAudioOn; private HeadsetService mService; private PowerManager mPowerManager; 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; // 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; static { classInitNative(); } 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(); if (!context.bindService(new Intent(IBluetoothHeadsetPhone.class.getName()), mConnection, 0)) { Log.e(TAG, "Could not bind to Bluetooth Headset Phone Service"); } initializeNative(); mNativeAvailable=true; mDisconnected = new Disconnected(); mPending = new Pending(); mConnected = new Connected(); mAudioOn = new AudioOn(); 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); setInitialState(mDisconnected); } 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(); mPhoneState=null; } if (mPhonebook != null) { mPhonebook.cleanup(); mPhonebook = null; } if (mNativeAvailable) { cleanupNative(); mNativeAvailable = false; } mService = null; mAdapter = null; } private class Disconnected extends State { @Override public void enter() { log("Enter Disconnected: " + getCurrentMessage().what); mPhonebook.resetAtState(); mPhoneState.listenForPhoneState(false); } @Override public boolean processMessage(Message message) { log("Disconnected process message: " + message.what); if (DBG) { if (mCurrentDevice != null || mTargetDevice != null || mIncomingDevice != null) { log("ERROR: current, 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 sendMessageDelayed(CONNECT_TIMEOUT, 30000); break; case DISCONNECT: // ignore break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case ROAM_CHANGED: processRoamChanged((Boolean) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj); 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); mPhoneState.listenForPhoneState(true); } // in Disconnected state private void processConnectionEvent(int state, BluetoothDevice device) { switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: Log.w(TAG, "Ignore HF DISCONNECTED event, device: " + device); break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTING: // check priority and accept or reject the connection // Since the state changes to Connecting or directly Connected in some cases.Have the check both in // CONNECTION_STATE_CONNECTING and CONNECTION_STATE_CONNECTED. if (BluetoothProfile.PRIORITY_OFF < mService.getPriority(device)) { Log.i(TAG,"Incoming Hf accepted"); // TODO(BT) Assume it's incoming connection // Do we need to check priority and accept/reject accordingly? broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { mIncomingDevice = device; transitionTo(mPending); } } else { Log.i(TAG,"Incoming Hf rejected"); //reject the connection and stay in Disconnected state itself disconnectHfpNative(getByteAddress(device)); } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: Log.w(TAG, "HFP Connected from Disconnected state"); if (BluetoothProfile.PRIORITY_OFF < mService.getPriority(device)) { Log.i(TAG,"Incoming Hf accepted"); broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; transitionTo(mConnected); } configAudioParameters(); } else { //reject the connection and stay in Disconnected state itself Log.d(TAG,"Incoming Hf rejected"); disconnectHfpNative(getByteAddress(device)); } 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); 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 ROAM_CHANGED: processRoamChanged((Boolean) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj); 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: 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) { switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) { broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); synchronized (HeadsetStateMachine.this) { mCurrentDevice = null; } 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; transitionTo(mDisconnected); } } } else if (mTargetDevice != null && mTargetDevice.equals(device)) { // outgoing connection failed broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mTargetDevice = null; transitionTo(mDisconnected); } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mIncomingDevice = null; transitionTo(mDisconnected); } } else { Log.e(TAG, "Unknown device Disconnected: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_CONNECTED: if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) { // disconnection failed broadcastConnectionState(mCurrentDevice, 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)) { broadcastConnectionState(mTargetDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mCurrentDevice = mTargetDevice; mTargetDevice = null; transitionTo(mConnected); } } else if (mIncomingDevice != null && mIncomingDevice.equals(device)) { broadcastConnectionState(mIncomingDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING); synchronized (HeadsetStateMachine.this) { mCurrentDevice = mIncomingDevice; mIncomingDevice = null; transitionTo(mConnected); } } else { Log.e(TAG, "Unknown device Connected: " + device); // something is wrong here, but sync our state with stack broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); synchronized (HeadsetStateMachine.this) { mCurrentDevice = device; mTargetDevice = null; mIncomingDevice = null; transitionTo(mConnected); } } configAudioParameters(); 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 class Connected extends State { @Override public void enter() { log("Enter Connected: " + getCurrentMessage().what); } @Override public boolean processMessage(Message message) { log("Connected process message: " + message.what); if (DBG) { if (mCurrentDevice == null) { log("ERROR: mCurrentDevice is null in Connected"); return NOT_HANDLED; } } boolean retValue = HANDLED; switch(message.what) { case CONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (mCurrentDevice.equals(device)) { break; } broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED); if (!disconnectHfpNative(getByteAddress(mCurrentDevice))) { broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTING); break; } synchronized (HeadsetStateMachine.this) { mTargetDevice = device; transitionTo(mPending); } } break; case DISCONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (!mCurrentDevice.equals(device)) { break; } broadcastConnectionState(device, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); if (!disconnectHfpNative(getByteAddress(device))) { broadcastConnectionState(device, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED); break; } transitionTo(mPending); } break; case CONNECT_AUDIO: // TODO(BT) when failure, broadcast audio connecting to disconnected intent // check if device matches mCurrentDevice connectAudioNative(getByteAddress(mCurrentDevice)); 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); break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case ROAM_CHANGED: processRoamChanged((Boolean) message.obj); break; case DEVICE_STATE_CHANGED: processDeviceStateChanged((HeadsetDeviceState) message.obj); break; case SEND_CCLC_RESPONSE: processSendClccResponse((HeadsetClccResponse) message.obj); break; case DIALING_OUT_TIMEOUT: if (mDialingOut) { mDialingOut= false; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } break; case START_VR_TIMEOUT: if (mWaitingForVoiceRecognition) { mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice recognition to start"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } 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; case EVENT_TYPE_AUDIO_STATE_CHANGED: processAudioEvent(event.valueInt, event.device); break; case EVENT_TYPE_VR_STATE_CHANGED: processVrEvent(event.valueInt); break; case EVENT_TYPE_ANSWER_CALL: // TODO(BT) could answer call happen on Connected state? processAnswerCall(); break; case EVENT_TYPE_HANGUP_CALL: // TODO(BT) could hangup call happen on Connected state? processHangupCall(); break; case EVENT_TYPE_VOLUME_CHANGED: processVolumeEvent(event.valueInt, event.valueInt2); break; case EVENT_TYPE_DIAL_CALL: processDialCall(event.valueString); break; case EVENT_TYPE_SEND_DTMF: processSendDtmf(event.valueInt); break; case EVENT_TYPE_NOICE_REDUCTION: processNoiceReductionEvent(event.valueInt); break; case EVENT_TYPE_AT_CHLD: processAtChld(event.valueInt); break; case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST: processSubscriberNumberRequest(); break; case EVENT_TYPE_AT_CIND: processAtCind(); break; case EVENT_TYPE_AT_COPS: processAtCops(); break; case EVENT_TYPE_AT_CLCC: processAtClcc(); break; case EVENT_TYPE_UNKNOWN_AT: processUnknownAt(event.valueString); break; case EVENT_TYPE_KEY_PRESSED: processKeyPressed(); 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) { switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); synchronized (HeadsetStateMachine.this) { mCurrentDevice = null; transitionTo(mDisconnected); } } else { Log.e(TAG, "Disconnected from unknown device: " + device); } break; case HeadsetHalConstants.CONNECTION_STATE_SLC_CONNECTED: processSlcConnected(); break; default: Log.e(TAG, "Connection State Device: " + device + " bad state: " + state); break; } } // in Connected state private void processAudioEvent(int state, BluetoothDevice device) { if (!mCurrentDevice.equals(device)) { Log.e(TAG, "Audio changed on disconnected device: " + device); return; } switch (state) { case HeadsetHalConstants.AUDIO_STATE_CONNECTED: // TODO(BT) should I save the state for next broadcast as the prevState? mAudioState = BluetoothHeadset.STATE_AUDIO_CONNECTED; mAudioManager.setBluetoothScoOn(true); broadcastAudioState(device, BluetoothHeadset.STATE_AUDIO_CONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTING); 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 class AudioOn extends State { @Override public void enter() { log("Enter AudioOn: " + getCurrentMessage().what); } @Override public boolean processMessage(Message message) { log("AudioOn process message: " + message.what); if (DBG) { if (mCurrentDevice == null) { log("ERROR: mCurrentDevice is null in AudioOn"); return NOT_HANDLED; } } boolean retValue = HANDLED; switch(message.what) { case DISCONNECT: { BluetoothDevice device = (BluetoothDevice) message.obj; if (!mCurrentDevice.equals(device)) { break; } deferMessage(obtainMessage(DISCONNECT, message.obj)); } // fall through case DISCONNECT_AUDIO: if (disconnectAudioNative(getByteAddress(mCurrentDevice))) { mAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED; mAudioManager.setBluetoothScoOn(false); broadcastAudioState(mCurrentDevice, BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED); } 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: processIntentScoVolume((Intent) message.obj); break; case CALL_STATE_CHANGED: processCallState((HeadsetCallState) message.obj); break; case INTENT_BATTERY_CHANGED: processIntentBatteryChanged((Intent) message.obj); break; case ROAM_CHANGED: processRoamChanged((Boolean) message.obj); break; case DEVICE_STATE_CHANGED: processDeviceStateChanged((HeadsetDeviceState) message.obj); break; case SEND_CCLC_RESPONSE: processSendClccResponse((HeadsetClccResponse) message.obj); break; case DIALING_OUT_TIMEOUT: if (mDialingOut) { mDialingOut= false; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } break; case START_VR_TIMEOUT: if (mWaitingForVoiceRecognition) { mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice recognition to start"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } 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; case EVENT_TYPE_AUDIO_STATE_CHANGED: processAudioEvent(event.valueInt, event.device); break; case EVENT_TYPE_VR_STATE_CHANGED: processVrEvent(event.valueInt); break; case EVENT_TYPE_ANSWER_CALL: processAnswerCall(); break; case EVENT_TYPE_HANGUP_CALL: processHangupCall(); break; case EVENT_TYPE_VOLUME_CHANGED: processVolumeEvent(event.valueInt, event.valueInt2); break; case EVENT_TYPE_DIAL_CALL: processDialCall(event.valueString); break; case EVENT_TYPE_SEND_DTMF: processSendDtmf(event.valueInt); break; case EVENT_TYPE_NOICE_REDUCTION: processNoiceReductionEvent(event.valueInt); break; case EVENT_TYPE_AT_CHLD: processAtChld(event.valueInt); break; case EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST: processSubscriberNumberRequest(); break; case EVENT_TYPE_AT_CIND: processAtCind(); break; case EVENT_TYPE_AT_COPS: processAtCops(); break; case EVENT_TYPE_AT_CLCC: processAtClcc(); break; case EVENT_TYPE_UNKNOWN_AT: processUnknownAt(event.valueString); break; case EVENT_TYPE_KEY_PRESSED: processKeyPressed(); 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) { switch (state) { case HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { processAudioEvent (HeadsetHalConstants.AUDIO_STATE_DISCONNECTED, device); broadcastConnectionState(mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); synchronized (HeadsetStateMachine.this) { mCurrentDevice = null; transitionTo(mDisconnected); } } else { Log.e(TAG, "Disconnected from unknown device: " + device); } break; default: Log.e(TAG, "Connection State Device: " + device + " bad state: " + state); break; } } // in AudioOn state private void processAudioEvent(int state, BluetoothDevice device) { if (!mCurrentDevice.equals(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 processIntentScoVolume(Intent intent) { int volumeValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); if (mPhoneState.getSpeakerVolume() != volumeValue) { mPhoneState.setSpeakerVolume(volumeValue); setVolumeNative(HeadsetHalConstants.VOLUME_TYPE_SPK, volumeValue); } } } 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) { return BluetoothProfile.STATE_DISCONNECTED; } synchronized (this) { IState currentState = getCurrentState(); if (currentState == mPending) { if ((mTargetDevice != null) && mTargetDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; } if ((mCurrentDevice != null) && mCurrentDevice.equals(device)) { return BluetoothProfile.STATE_DISCONNECTING; } if ((mIncomingDevice != null) && mIncomingDevice.equals(device)) { return BluetoothProfile.STATE_CONNECTING; // incoming connection } return BluetoothProfile.STATE_DISCONNECTED; } if (currentState == mConnected || currentState == mAudioOn) { if (mCurrentDevice.equals(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) { if (isConnected()) { devices.add(mCurrentDevice); } } 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; } int getAudioState(BluetoothDevice device) { synchronized(this) { if (mCurrentDevice == null || !mCurrentDevice.equals(device)) { return BluetoothHeadset.STATE_AUDIO_DISCONNECTED; } } return mAudioState; } private void processVrEvent(int state) { Log.d(TAG, "processVrEvent: state=" + state + " mVoiceRecognitionStarted: " + mVoiceRecognitionStarted + " mWaitingforVoiceRecognition: " + mWaitingForVoiceRecognition + " isInCall: " + isInCall()); if (state == HeadsetHalConstants.VR_STATE_STARTED) { // TODO(BT) handle virtualcall if (!mVoiceRecognitionStarted && !isInCall()) { try { mService.startActivity(sVoiceCommandIntent); } catch (ActivityNotFoundException e) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); return; } expectVoiceRecognition(); } } else if (state == HeadsetHalConstants.VR_STATE_STOPPED) { if (mVoiceRecognitionStarted || mWaitingForVoiceRecognition) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0); mVoiceRecognitionStarted = false; mWaitingForVoiceRecognition = false; if (!isInCall()) { disconnectAudioNative(getByteAddress(mCurrentDevice)); mAudioManager.setParameters("A2dpSuspended=false"); } } else { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } else { Log.e(TAG, "Bad Voice Recognition state: " + state); } } private void processLocalVrEvent(int state) { 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) { Log.d(TAG, "Voice recognition started successfully"); mWaitingForVoiceRecognition = false; atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0); removeMessages(START_VR_TIMEOUT); } else { Log.d(TAG, "Voice recognition started locally"); needAudio = startVoiceRecognitionNative(); } 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"); connectAudioNative(getByteAddress(mCurrentDevice)); } 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() && !isInCall()) { disconnectAudioNative(getByteAddress(mCurrentDevice)); mAudioManager.setParameters("A2dpSuspended=false"); } } } } private synchronized void expectVoiceRecognition() { mWaitingForVoiceRecognition = true; sendMessageDelayed(START_VR_TIMEOUT, 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; } // This method does not check for error conditon (newState == prevState) private void broadcastConnectionState(BluetoothDevice device, int newState, int prevState) { if (DBG) log("Connection state " + device + ": " + prevState + "->" + newState); /* 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.sendBroadcast(intent, HeadsetService.BLUETOOTH_PERM); } private void broadcastAudioState(BluetoothDevice device, int newState, int prevState) { 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.sendBroadcast(intent, HeadsetService.BLUETOOTH_PERM); if (DBG) log("Audio state " + device + ": " + prevState + "->" + newState); } private void configAudioParameters() { // Reset NREC on connect event. Headset will override later mAudioManager.setParameters(HEADSET_NAME + "=" + getCurrentDeviceName() + ";" + HEADSET_NREC + "=on"); } 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; } private void processAnswerCall() { 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() { 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) { String dialNumber; 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); 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); 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); return; } } else { // Remove trailing ';' if (number.charAt(number.length() - 1) == ';') { number = number.substring(0, number.length() - 1); } dialNumber = PhoneNumberUtils.convertPreDial(number); } // TODO(BT) do we need to terminate virtual call first // like call 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; sendMessageDelayed(DIALING_OUT_TIMEOUT, DIALING_OUT_TIMEOUT_VALUE); } private void processVolumeEvent(int volumeType, int volume) { 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) { 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) { mPhoneState.setNumActiveCall(callState.mNumActive); mPhoneState.setNumHeldCall(callState.mNumHeld); mPhoneState.setCallState(callState.mCallState); if (mDialingOut && callState.mCallState == HeadsetHalConstants.CALL_STATE_DIALING) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0); removeMessages(DIALING_OUT_TIMEOUT); mDialingOut = false; } log("mNumActive: " + callState.mNumActive + " mNumHeld: " + callState.mNumHeld + " mCallState: " + callState.mCallState); log("mNumber: " + callState.mNumber + " mType: " + callState.mType); if (getCurrentState() != mDisconnected) { phoneStateChangeNative(callState.mNumActive, callState.mNumHeld, callState.mCallState, callState.mNumber, callState.mType); } } // enable 1 enable noice reduction // 0 disable noice reduction private void processNoiceReductionEvent(int enable) { if (enable == 1) { mAudioManager.setParameters(HEADSET_NREC + "=on"); } else { mAudioManager.setParameters(HEADSET_NREC + "off"); } } private void processAtChld(int chld) { if (mPhoneProxy != null) { try { if (mPhoneProxy.processChld(chld)) { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0); } else { atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } else { Log.e(TAG, "Handsfree phone proxy null for At+Chld"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } private void processSubscriberNumberRequest() { if (mPhoneProxy != null) { try { String number = mPhoneProxy.getSubscriberNumber(); if (number != null) { atResponseStringNative("+CNUM: ,\"" + number + "\"," + PhoneNumberUtils.toaFromString(number) + ",,4"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_OK, 0); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } else { Log.e(TAG, "Handsfree phone proxy null for At+CNUM"); } } private void processAtCind() { cindResponseNative(mPhoneState.getService(), mPhoneState.getNumActiveCall(), mPhoneState.getNumHeldCall(), mPhoneState.getCallState(), mPhoneState.getSignal(), mPhoneState.getRoam(), mPhoneState.getBatteryCharge()); } private void processAtCops() { if (mPhoneProxy != null) { try { String operatorName = mPhoneProxy.getNetworkOperator(); if (operatorName == null) { operatorName = ""; } copsResponseNative(operatorName); } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); copsResponseNative(""); } } else { Log.e(TAG, "Handsfree phone proxy null for At+COPS"); copsResponseNative(""); } } private void processAtClcc() { if (mPhoneProxy != null) { try { if (!mPhoneProxy.listCurrentCalls()) { clccResponseNative(0, 0, 0, 0, false, "", 0); } } catch (RemoteException e) { Log.e(TAG, Log.getStackTraceString(new Throwable())); clccResponseNative(0, 0, 0, 0, false, "", 0); } } else { Log.e(TAG, "Handsfree phone proxy null for At+CLCC"); clccResponseNative(0, 0, 0, 0, false, "", 0); } } private void processAtCscs(String atString, int type) { log("processAtCscs - atString = "+ atString); if(mPhonebook != null) { mPhonebook.handleCscsCommand(atString, type); } else { Log.e(TAG, "Phonebook handle null for At+CSCS"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } private void processAtCpbs(String atString, int type) { log("processAtCpbs - atString = "+ atString); if(mPhonebook != null) { mPhonebook.handleCpbsCommand(atString, type); } else { Log.e(TAG, "Phonebook handle null for At+CPBS"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } private void processAtCpbr(String atString, int type, BluetoothDevice mCurrentDevice) { log("processAtCpbr - atString = "+ atString); if(mPhonebook != null) { mPhonebook.handleCpbrCommand(atString, type, mCurrentDevice); } else { Log.e(TAG, "Phonebook handle null for At+CPBR"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } private void processUnknownAt(String atString) { // TODO (BT) log("processUnknownAt - atString = "+ atString); String atCommand = parseUnknownAt(atString); int commandType = getAtCommandType(atCommand); if (atCommand.startsWith("+CSCS")) processAtCscs(atCommand.substring(5), commandType); else if (atCommand.startsWith("+CPBS")) processAtCpbs(atCommand.substring(5), commandType); else if (atCommand.startsWith("+CPBR")) processAtCpbr(atCommand.substring(5), commandType, mCurrentDevice); else atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } private void processKeyPressed() { 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) { StackEvent event = new StackEvent(EVENT_TYPE_VR_STATE_CHANGED); event.valueInt = state; sendMessage(STACK_EVENT, event); } private void onAnswerCall() { StackEvent event = new StackEvent(EVENT_TYPE_ANSWER_CALL); sendMessage(STACK_EVENT, event); } private void onHangupCall() { StackEvent event = new StackEvent(EVENT_TYPE_HANGUP_CALL); sendMessage(STACK_EVENT, event); } private void onVolumeChanged(int type, int volume) { StackEvent event = new StackEvent(EVENT_TYPE_VOLUME_CHANGED); event.valueInt = type; event.valueInt2 = volume; sendMessage(STACK_EVENT, event); } private void onDialCall(String number) { StackEvent event = new StackEvent(EVENT_TYPE_DIAL_CALL); event.valueString = number; sendMessage(STACK_EVENT, event); } private void onSendDtmf(int dtmf) { StackEvent event = new StackEvent(EVENT_TYPE_SEND_DTMF); event.valueInt = dtmf; sendMessage(STACK_EVENT, event); } private void onNoiceReductionEnable(boolean enable) { StackEvent event = new StackEvent(EVENT_TYPE_NOICE_REDUCTION); event.valueInt = enable ? 1 : 0; sendMessage(STACK_EVENT, event); } private void onAtChld(int chld) { StackEvent event = new StackEvent(EVENT_TYPE_AT_CHLD); event.valueInt = chld; sendMessage(STACK_EVENT, event); } private void onAtCnum() { StackEvent event = new StackEvent(EVENT_TYPE_SUBSCRIBER_NUMBER_REQUEST); sendMessage(STACK_EVENT, event); } private void onAtCind() { StackEvent event = new StackEvent(EVENT_TYPE_AT_CIND); sendMessage(STACK_EVENT, event); } private void onAtCops() { StackEvent event = new StackEvent(EVENT_TYPE_AT_COPS); sendMessage(STACK_EVENT, event); } private void onAtClcc() { StackEvent event = new StackEvent(EVENT_TYPE_AT_CLCC); sendMessage(STACK_EVENT, event); } private void onUnknownAt(String atString) { StackEvent event = new StackEvent(EVENT_TYPE_UNKNOWN_AT); event.valueString = atString; sendMessage(STACK_EVENT, event); } private void onKeyPressed() { StackEvent event = new StackEvent(EVENT_TYPE_KEY_PRESSED); 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 processRoamChanged(boolean roam) { mPhoneState.setRoam(roam ? HeadsetHalConstants.SERVICE_TYPE_ROAMING : HeadsetHalConstants.SERVICE_TYPE_HOME); } private void processDeviceStateChanged(HeadsetDeviceState deviceState) { notifyDeviceStatusNative(deviceState.mService, deviceState.mRoam, deviceState.mSignal, deviceState.mBatteryCharge); } private void processSendClccResponse(HeadsetClccResponse clcc) { clccResponseNative(clcc.mIndex, clcc.mDirection, clcc.mStatus, clcc.mMode, clcc.mMpty, clcc.mNumber, clcc.mType); } private String getCurrentDeviceName() { String defaultName = ""; if (mCurrentDevice == null) { return defaultName; } String deviceName = mCurrentDevice.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)); } boolean isConnected() { IState currentState = getCurrentState(); return (currentState == mConnected || currentState == mAudioOn); } private void log(String msg) { if (DBG) { Log.d(TAG, msg); } } public void handleAccessPermissionResult(Intent intent) { log("handleAccessPermissionResult"); 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.setTrust(true); } atCommandResult = mPhonebook.processCpbrCommand(); } } mPhonebook.setCpbrIndex(-1); mPhonebook.setCheckingAccessPermission(false); if (atCommandResult >= 0) { atResponseCodeNative(atCommandResult, atCommandErrorCode); } else log("handleAccessPermissionResult - RESULT_NONE"); } else { Log.e(TAG, "Phonebook handle null"); atResponseCodeNative(HeadsetHalConstants.AT_RESPONSE_ERROR, 0); } } 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; 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); /*package*/ native boolean atResponseStringNative(String responseString); private native static void classInitNative(); private native void initializeNative(); 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(); private native boolean stopVoiceRecognitionNative(); private native boolean setVolumeNative(int volumeType, int volume); private native boolean cindResponseNative(int service, int numActive, int numHeld, int callState, int signal, int roam, int batteryCharge); 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); private native boolean copsResponseNative(String operatorName); private native boolean phoneStateChangeNative(int numActive, int numHeld, int callState, String number, int type); }