| /* |
| * 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<BluetoothDevice> getConnectedDevices() { |
| List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); |
| synchronized(this) { |
| if (isConnected()) { |
| devices.add(mCurrentDevice); |
| } |
| } |
| return devices; |
| } |
| |
| boolean isAudioOn() { |
| return (getCurrentState() == mAudioOn); |
| } |
| |
| boolean isAudioConnected(BluetoothDevice device) { |
| synchronized(this) { |
| if (getCurrentState() == mAudioOn && mCurrentDevice.equals(device)) { |
| 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)); |
| } |
| 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"); |
| 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)); |
| } |
| } |
| } |
| |
| private synchronized void expectVoiceRecognition() { |
| mWaitingForVoiceRecognition = true; |
| sendMessageDelayed(START_VR_TIMEOUT, START_VR_TIMEOUT_VALUE); |
| if (!mStartVoiceRecognitionWakeLock.isHeld()) { |
| mStartVoiceRecognitionWakeLock.acquire(START_VR_TIMEOUT_VALUE); |
| } |
| } |
| |
| List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { |
| List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); |
| Set<BluetoothDevice> 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) { |
| 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); |
| if (DBG) log("Connection state " + device + ": " + prevState + "->" + newState); |
| mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.HEADSET, newState, prevState); |
| } |
| |
| 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 = "<unknown>"; |
| 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); |
| } |