/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.phone; import android.bluetooth.AtCommandHandler; import android.bluetooth.AtCommandResult; import android.bluetooth.AtParser; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothIntent; import android.bluetooth.HeadsetBase; import android.bluetooth.ScoSocket; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.net.Uri; import android.os.AsyncResult; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemProperties; import android.telephony.PhoneNumberUtils; import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.util.Log; import com.android.internal.telephony.Call; import com.android.internal.telephony.Connection; import com.android.internal.telephony.Phone; import com.android.internal.telephony.TelephonyIntents; import java.util.LinkedList; /** * Bluetooth headset manager for the Phone app. * @hide */ public class BluetoothHandsfree { private static final String TAG = "BT HS/HF"; private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2); private static final boolean VDBG = false; // even more logging public static final int TYPE_UNKNOWN = 0; public static final int TYPE_HEADSET = 1; public static final int TYPE_HANDSFREE = 2; private final Context mContext; private final Phone mPhone; private ServiceState mServiceState; private HeadsetBase mHeadset; // null when not connected private int mHeadsetType; private boolean mAudioPossible; private ScoSocket mIncomingSco; private ScoSocket mOutgoingSco; private ScoSocket mConnectedSco; private Call mForegroundCall; private Call mBackgroundCall; private Call mRingingCall; private AudioManager mAudioManager; private PowerManager mPowerManager; private boolean mUserWantsAudio; private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call private WakeLock mStartVoiceRecognitionWakeLock; // held while waiting for voice recognition // AT command state private static final int MAX_CONNECTIONS = 6; // Max connections allowed by GSM private long mBgndEarliestConnectionTime = 0; private boolean mClip = false; // Calling Line Information Presentation private boolean mIndicatorsEnabled = false; private boolean mCmee = false; // Extended Error reporting private long[] mClccTimestamps; // Timestamps associated with each clcc index private boolean[] mClccUsed; // Is this clcc index in use private boolean mWaitingForCallStart; private boolean mWaitingForVoiceRecognition; // do not connect audio until service connection is established // for 3-way supported devices, this is after AT+CHLD // for non-3-way supported devices, this is after AT+CMER (see spec) private boolean mServiceConnectionEstablished; private final BluetoothPhoneState mBluetoothPhoneState; // for CIND and CIEV updates private final BluetoothAtPhonebook mPhonebook; private Phone.State mPhoneState = Phone.State.IDLE; private DebugThread mDebugThread; private int mScoGain = Integer.MIN_VALUE; private static Intent sVoiceCommandIntent; // Audio parameters private static final String HEADSET_NREC = "bt_headset_nrec"; private static final String HEADSET_NAME = "bt_headset_name"; private int mRemoteBrsf = 0; private int mLocalBrsf = 0; /* Constants from Bluetooth Specification Hands-Free profile version 1.5 */ private static final int BRSF_AG_THREE_WAY_CALLING = 1 << 0; private static final int BRSF_AG_EC_NR = 1 << 1; private static final int BRSF_AG_VOICE_RECOG = 1 << 2; private static final int BRSF_AG_IN_BAND_RING = 1 << 3; private static final int BRSF_AG_VOICE_TAG_NUMBE = 1 << 4; private static final int BRSF_AG_REJECT_CALL = 1 << 5; private static final int BRSF_AG_ENHANCED_CALL_STATUS = 1 << 6; private static final int BRSF_AG_ENHANCED_CALL_CONTROL = 1 << 7; private static final int BRSF_AG_ENHANCED_ERR_RESULT_CODES = 1 << 8; private static final int BRSF_HF_EC_NR = 1 << 0; private static final int BRSF_HF_CW_THREE_WAY_CALLING = 1 << 1; private static final int BRSF_HF_CLIP = 1 << 2; private static final int BRSF_HF_VOICE_REG_ACT = 1 << 3; private static final int BRSF_HF_REMOTE_VOL_CONTROL = 1 << 4; private static final int BRSF_HF_ENHANCED_CALL_STATUS = 1 << 5; private static final int BRSF_HF_ENHANCED_CALL_CONTROL = 1 << 6; public static String typeToString(int type) { switch (type) { case TYPE_UNKNOWN: return "unknown"; case TYPE_HEADSET: return "headset"; case TYPE_HANDSFREE: return "handsfree"; } return null; } public BluetoothHandsfree(Context context, Phone phone) { mPhone = phone; mContext = context; BluetoothDevice bluetooth = (BluetoothDevice)context.getSystemService(Context.BLUETOOTH_SERVICE); boolean bluetoothCapable = (bluetooth != null); mHeadset = null; // nothing connected yet mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":StartCall"); mStartCallWakeLock.setReferenceCounted(false); mStartVoiceRecognitionWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG + ":VoiceRecognition"); mStartVoiceRecognitionWakeLock.setReferenceCounted(false); mLocalBrsf = BRSF_AG_THREE_WAY_CALLING | BRSF_AG_EC_NR | BRSF_AG_REJECT_CALL | BRSF_AG_ENHANCED_CALL_STATUS; if (sVoiceCommandIntent == null) { sVoiceCommandIntent = new Intent(Intent.ACTION_VOICE_COMMAND); sVoiceCommandIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } if (mContext.getPackageManager().resolveActivity(sVoiceCommandIntent, 0) != null && !BluetoothHeadset.DISABLE_BT_VOICE_DIALING) { mLocalBrsf |= BRSF_AG_VOICE_RECOG; } if (bluetoothCapable) { resetAtState(); } mRingingCall = mPhone.getRingingCall(); mForegroundCall = mPhone.getForegroundCall(); mBackgroundCall = mPhone.getBackgroundCall(); mBluetoothPhoneState = new BluetoothPhoneState(); mUserWantsAudio = true; mPhonebook = new BluetoothAtPhonebook(mContext, this); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); } /* package */ synchronized void onBluetoothEnabled() { /* Bluez has a bug where it will always accept and then orphan * incoming SCO connections, regardless of whether we have a listening * SCO socket. So the best thing to do is always run a listening socket * while bluetooth is on so that at least we can diconnect it * immediately when we don't want it. */ if (mIncomingSco == null) { mIncomingSco = createScoSocket(); mIncomingSco.accept(); } } /* package */ synchronized void onBluetoothDisabled() { if (mConnectedSco != null) { mAudioManager.setBluetoothScoOn(false); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); mConnectedSco.close(); mConnectedSco = null; } if (mOutgoingSco != null) { mOutgoingSco.close(); mOutgoingSco = null; } if (mIncomingSco != null) { mIncomingSco.close(); mIncomingSco = null; } } private boolean isHeadsetConnected() { if (mHeadset == null) { return false; } return mHeadset.isConnected(); } /* package */ void connectHeadset(HeadsetBase headset, int headsetType) { mHeadset = headset; mHeadsetType = headsetType; if (mHeadsetType == TYPE_HEADSET) { initializeHeadsetAtParser(); } else { initializeHandsfreeAtParser(); } headset.startEventThread(); configAudioParameters(); if (inDebug()) { startDebug(); } if (isIncallAudio()) { audioOn(); } } /* returns true if there is some kind of in-call audio we may wish to route * bluetooth to */ private boolean isIncallAudio() { Call.State state = mForegroundCall.getState(); return (state == Call.State.ACTIVE || state == Call.State.ALERTING); } /* package */ void disconnectHeadset() { mHeadset = null; stopDebug(); resetAtState(); } private void resetAtState() { mClip = false; mIndicatorsEnabled = false; mServiceConnectionEstablished = false; mCmee = false; mClccTimestamps = new long[MAX_CONNECTIONS]; mClccUsed = new boolean[MAX_CONNECTIONS]; for (int i = 0; i < MAX_CONNECTIONS; i++) { mClccUsed[i] = false; } mRemoteBrsf = 0; } private void configAudioParameters() { String name = mHeadset.getName(); if (name == null) { name = ""; } mAudioManager.setParameter(HEADSET_NAME, name); mAudioManager.setParameter(HEADSET_NREC, "on"); } /** Represents the data that we send in a +CIND or +CIEV command to the HF */ private class BluetoothPhoneState { // 0: no service // 1: service private int mService; // 0: no active call // 1: active call (where active means audio is routed - not held call) private int mCall; // 0: not in call setup // 1: incoming call setup // 2: outgoing call setup // 3: remote party being alerted in an outgoing call setup private int mCallsetup; // 0: no calls held // 1: held call and active call // 2: held call only private int mCallheld; // cellular signal strength of AG: 0-5 private int mSignal; // cellular signal strength in CSQ rssi scale private int mRssi; // for CSQ // 0: roaming not active (home) // 1: roaming active private int mRoam; // battery charge of AG: 0-5 private int mBattchg; // 0: not registered // 1: registered, home network // 5: registered, roaming private int mStat; // for CREG private String mRingingNumber; // Context for in-progress RING's private int mRingingType; private boolean mIgnoreRing = false; private static final int SERVICE_STATE_CHANGED = 1; private static final int PHONE_STATE_CHANGED = 2; private static final int RING = 3; private static final int PHONE_CDMA_CALL_WAITING = 4; private Handler mStateChangeHandler = new Handler() { @Override public void handleMessage(Message msg) { switch(msg.what) { case RING: AtCommandResult result = ring(); if (result != null) { sendURC(result.toString()); } break; case SERVICE_STATE_CHANGED: ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result; updateServiceState(sendUpdate(), state); break; case PHONE_STATE_CHANGED: case PHONE_CDMA_CALL_WAITING: Connection connection = null; if (((AsyncResult) msg.obj).result instanceof Connection) { connection = (Connection) ((AsyncResult) msg.obj).result; } updatePhoneState(sendUpdate(), connection); break; } } }; private BluetoothPhoneState() { // init members updateServiceState(false, mPhone.getServiceState()); updatePhoneState(false, null); mBattchg = 5; // There is currently no API to get battery level // on demand, so set to 5 and wait for an update mSignal = asuToSignal(mPhone.getSignalStrength()); // register for updates mPhone.registerForServiceStateChanged(mStateChangeHandler, SERVICE_STATE_CHANGED, null); mPhone.registerForPhoneStateChanged(mStateChangeHandler, PHONE_STATE_CHANGED, null); if (mPhone.getPhoneName().equals("CDMA")) { mPhone.registerForCallWaiting(mStateChangeHandler, PHONE_CDMA_CALL_WAITING, null); } IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED); mContext.registerReceiver(mStateReceiver, filter); } private void updateBtPhoneStateAfterRadioTechnologyChange() { if(DBG) Log.d(TAG, "updateBtPhoneStateAfterRadioTechnologyChange..."); //Unregister all events from the old obsolete phone mPhone.unregisterForServiceStateChanged(mStateChangeHandler); mPhone.unregisterForPhoneStateChanged(mStateChangeHandler); mPhone.unregisterForCallWaiting(mStateChangeHandler); //Register all events new to the new active phone mPhone.registerForServiceStateChanged(mStateChangeHandler, SERVICE_STATE_CHANGED, null); mPhone.registerForPhoneStateChanged(mStateChangeHandler, PHONE_STATE_CHANGED, null); if (mPhone.getPhoneName().equals("CDMA")) { mPhone.registerForCallWaiting(mStateChangeHandler, PHONE_CDMA_CALL_WAITING, null); } } private boolean sendUpdate() { return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mIndicatorsEnabled; } private boolean sendClipUpdate() { return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mClip; } /* convert [0,31] ASU signal strength to the [0,5] expected by * bluetooth devices. Scale is similar to status bar policy */ private int gsmAsuToSignal(int asu) { if (asu >= 16) return 5; else if (asu >= 8) return 4; else if (asu >= 4) return 3; else if (asu >= 2) return 2; else if (asu >= 1) return 1; else return 0; } /* convert cdma dBm signal strength to the [0,5] expected by * bluetooth devices. Scale is similar to status bar policy */ private int cdmaDbmToSignal(int cdmaDbm) { if (cdmaDbm >= -75) return 5; else if (cdmaDbm >= -85) return 4; else if (cdmaDbm >= -95) return 3; else if (cdmaDbm >= -100) return 2; else if (cdmaDbm >= -105) return 2; else return 0; } private int asuToSignal(SignalStrength signalStrength) { if (!signalStrength.isGsm()) { return gsmAsuToSignal(signalStrength.getCdmaDbm()); } else { return cdmaDbmToSignal(signalStrength.getGsmSignalStrength()); } } /* convert [0,5] signal strength to a rssi signal strength for CSQ * which is [0,31]. Despite the same scale, this is not the same value * as ASU. */ private int signalToRssi(int signal) { // using C4A suggested values switch (signal) { case 0: return 0; case 1: return 4; case 2: return 8; case 3: return 13; case 4: return 19; case 5: return 31; } return 0; } private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) { updateBatteryState(intent); } else if (intent.getAction().equals(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED)) { updateSignalState(intent); } } }; private synchronized void updateBatteryState(Intent intent) { int batteryLevel = intent.getIntExtra("level", -1); int scale = intent.getIntExtra("scale", -1); if (batteryLevel == -1 || scale == -1) { return; // ignore } batteryLevel = batteryLevel * 5 / scale; if (mBattchg != batteryLevel) { mBattchg = batteryLevel; if (sendUpdate()) { sendURC("+CIEV: 7," + mBattchg); } } } private synchronized void updateSignalState(Intent intent) { // NOTE this function is called by the BroadcastReceiver mStateReceiver after intent // ACTION_SIGNAL_STRENGTH_CHANGED and by the DebugThread mDebugThread SignalStrength signalStrength = SignalStrength.newFromBundle(intent.getExtras()); int signal; if (signalStrength != null) { signal = asuToSignal(signalStrength); mRssi = signalToRssi(signal); // no unsolicited CSQ if (signal != mSignal) { mSignal = signal; if (sendUpdate()) { sendURC("+CIEV: 5," + mSignal); } } } else { Log.e(TAG, "Signal Strength null"); } } private synchronized void updateServiceState(boolean sendUpdate, ServiceState state) { int service = state.getState() == ServiceState.STATE_IN_SERVICE ? 1 : 0; int roam = state.getRoaming() ? 1 : 0; int stat; AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); if (service == 0) { stat = 0; } else { stat = (roam == 1) ? 5 : 1; } if (service != mService) { mService = service; if (sendUpdate) { result.addResponse("+CIEV: 1," + mService); } } if (roam != mRoam) { mRoam = roam; if (sendUpdate) { result.addResponse("+CIEV: 6," + mRoam); } } if (stat != mStat) { mStat = stat; if (sendUpdate) { result.addResponse(toCregString()); } } sendURC(result.toString()); } private synchronized void updatePhoneState(boolean sendUpdate, Connection connection) { int call = 0; int callsetup = 0; int callheld = 0; int prevCallsetup = mCallsetup; AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); if (DBG) log("updatePhoneState()"); Phone.State newState = mPhone.getState(); if (newState != mPhoneState) { mPhoneState = newState; switch (mPhoneState) { case IDLE: mUserWantsAudio = true; // out of call - reset state audioOff(); break; default: callStarted(); } } switch(mForegroundCall.getState()) { case ACTIVE: call = 1; mAudioPossible = true; break; case DIALING: callsetup = 2; mAudioPossible = false; break; case ALERTING: callsetup = 3; // Open the SCO channel for the outgoing call. audioOn(); mAudioPossible = true; break; default: mAudioPossible = false; } switch(mRingingCall.getState()) { case INCOMING: case WAITING: callsetup = 1; break; } switch(mBackgroundCall.getState()) { case HOLDING: if (call == 1) { callheld = 1; } else { call = 1; callheld = 2; } break; } if (mCall != call) { if (call == 1) { // This means that a call has transitioned from NOT ACTIVE to ACTIVE. // Switch on audio. audioOn(); } mCall = call; if (sendUpdate) { result.addResponse("+CIEV: 2," + mCall); } } if (mCallsetup != callsetup) { mCallsetup = callsetup; if (sendUpdate) { // If mCall = 0, send CIEV // mCall = 1, mCallsetup = 0, send CIEV // mCall = 1, mCallsetup = 1, send CIEV after CCWA, // if 3 way supported. // mCall = 1, mCallsetup = 2 / 3 -> send CIEV, // if 3 way is supported if (mCall != 1 || mCallsetup == 0 || mCallsetup != 1 && (mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { result.addResponse("+CIEV: 3," + mCallsetup); } } } boolean callsSwitched = (callheld == 1 && ! (mBackgroundCall.getEarliestConnectTime() == mBgndEarliestConnectionTime)); mBgndEarliestConnectionTime = mBackgroundCall.getEarliestConnectTime(); if (mCallheld != callheld || callsSwitched) { mCallheld = callheld; if (sendUpdate) { result.addResponse("+CIEV: 4," + mCallheld); } } if (callsetup == 1 && callsetup != prevCallsetup) { // new incoming call String number = null; int type = 128; // find incoming phone number and type if (connection == null) { connection = mRingingCall.getEarliestConnection(); if (connection == null) { Log.e(TAG, "Could not get a handle on Connection object for new " + "incoming call"); } } if (connection != null) { number = connection.getAddress(); if (number != null) { type = PhoneNumberUtils.toaFromString(number); } } if (number == null) { number = ""; } if ((call != 0 || callheld != 0) && sendUpdate) { // call waiting if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { result.addResponse("+CCWA: \"" + number + "\"," + type); result.addResponse("+CIEV: 3," + callsetup); } } else { // regular new incoming call mRingingNumber = number; mRingingType = type; mIgnoreRing = false; if ((mLocalBrsf & BRSF_AG_IN_BAND_RING) == 0x1) { audioOn(); } result.addResult(ring()); } } sendURC(result.toString()); } private AtCommandResult ring() { if (!mIgnoreRing && mRingingCall.isRinging()) { AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); result.addResponse("RING"); if (sendClipUpdate()) { result.addResponse("+CLIP: \"" + mRingingNumber + "\"," + mRingingType); } Message msg = mStateChangeHandler.obtainMessage(RING); mStateChangeHandler.sendMessageDelayed(msg, 3000); return result; } return null; } private synchronized String toCregString() { return new String("+CREG: 1," + mStat); } private synchronized AtCommandResult toCindResult() { AtCommandResult result = new AtCommandResult(AtCommandResult.OK); String status = "+CIND: " + mService + "," + mCall + "," + mCallsetup + "," + mCallheld + "," + mSignal + "," + mRoam + "," + mBattchg; result.addResponse(status); return result; } private synchronized AtCommandResult toCsqResult() { AtCommandResult result = new AtCommandResult(AtCommandResult.OK); String status = "+CSQ: " + mRssi + ",99"; result.addResponse(status); return result; } private synchronized AtCommandResult getCindTestResult() { return new AtCommandResult("+CIND: (\"service\",(0-1))," + "(\"call\",(0-1))," + "(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5))," + "(\"roam\",(0-1)),(\"battchg\",(0-5))"); } private synchronized void ignoreRing() { mCallsetup = 0; mIgnoreRing = true; if (sendUpdate()) { sendURC("+CIEV: 3," + mCallsetup); } } }; private static final int SCO_ACCEPTED = 1; private static final int SCO_CONNECTED = 2; private static final int SCO_CLOSED = 3; private static final int CHECK_CALL_STARTED = 4; private static final int CHECK_VOICE_RECOGNITION_STARTED = 5; private final Handler mHandler = new Handler() { @Override public synchronized void handleMessage(Message msg) { switch (msg.what) { case SCO_ACCEPTED: if (msg.arg1 == ScoSocket.STATE_CONNECTED) { if (isHeadsetConnected() && (mAudioPossible || allowAudioAnytime()) && mConnectedSco == null) { Log.i(TAG, "Routing audio for incoming SCO connection"); mConnectedSco = (ScoSocket)msg.obj; mAudioManager.setBluetoothScoOn(true); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED); } else { Log.i(TAG, "Rejecting incoming SCO connection"); ((ScoSocket)msg.obj).close(); } } // else error trying to accept, try again mIncomingSco = createScoSocket(); mIncomingSco.accept(); break; case SCO_CONNECTED: if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected() && mConnectedSco == null) { if (DBG) log("Routing audio for outgoing SCO conection"); mConnectedSco = (ScoSocket)msg.obj; mAudioManager.setBluetoothScoOn(true); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED); } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) { if (DBG) log("Rejecting new connected outgoing SCO socket"); ((ScoSocket)msg.obj).close(); mOutgoingSco.close(); } mOutgoingSco = null; break; case SCO_CLOSED: if (mConnectedSco == (ScoSocket)msg.obj) { mConnectedSco = null; mAudioManager.setBluetoothScoOn(false); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); } else if (mOutgoingSco == (ScoSocket)msg.obj) { mOutgoingSco = null; } else if (mIncomingSco == (ScoSocket)msg.obj) { mIncomingSco = null; } break; case CHECK_CALL_STARTED: if (mWaitingForCallStart) { mWaitingForCallStart = false; Log.e(TAG, "Timeout waiting for call to start"); sendURC("ERROR"); if (mStartCallWakeLock.isHeld()) { mStartCallWakeLock.release(); } } break; case CHECK_VOICE_RECOGNITION_STARTED: if (mWaitingForVoiceRecognition) { mWaitingForVoiceRecognition = false; Log.e(TAG, "Timeout waiting for voice recognition to start"); sendURC("ERROR"); } break; } } }; private ScoSocket createScoSocket() { return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED); } private void broadcastAudioStateIntent(int state) { if (VDBG) log("broadcastAudioStateIntent(" + state + ")"); Intent intent = new Intent(BluetoothIntent.HEADSET_AUDIO_STATE_CHANGED_ACTION); intent.putExtra(BluetoothIntent.HEADSET_AUDIO_STATE, state); mContext.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); } void updateBtHandsfreeAfterRadioTechnologyChange() { if(DBG) Log.d(TAG, "updateBtHandsfreeAfterRadioTechnologyChange..."); //Get the Call references from the new active phone again mRingingCall = mPhone.getRingingCall(); mForegroundCall = mPhone.getForegroundCall(); mBackgroundCall = mPhone.getBackgroundCall(); mBluetoothPhoneState.updateBtPhoneStateAfterRadioTechnologyChange(); } /** Request to establish SCO (audio) connection to bluetooth * headset/handsfree, if one is connected. Does not block. * Returns false if the user has requested audio off, or if there * is some other immediate problem that will prevent BT audio. */ /* package */ synchronized boolean audioOn() { if (VDBG) log("audioOn()"); if (!isHeadsetConnected()) { if (DBG) log("audioOn(): headset is not connected!"); return false; } if (mHeadsetType == TYPE_HANDSFREE && !mServiceConnectionEstablished) { if (DBG) log("audioOn(): service connection not yet established!"); return false; } if (mConnectedSco != null) { if (DBG) log("audioOn(): audio is already connected"); return true; } if (!mUserWantsAudio) { if (DBG) log("audioOn(): user requested no audio, ignoring"); return false; } if (mOutgoingSco != null) { if (DBG) log("audioOn(): outgoing SCO already in progress"); return true; } mOutgoingSco = createScoSocket(); if (!mOutgoingSco.connect(mHeadset.getAddress())) { mOutgoingSco = null; } return true; } /** Used to indicate the user requested BT audio on. * This will establish SCO (BT audio), even if the user requested it off * previously on this call. */ /* package */ synchronized void userWantsAudioOn() { mUserWantsAudio = true; audioOn(); } /** Used to indicate the user requested BT audio off. * This will prevent us from establishing BT audio again during this call * if audioOn() is called. */ /* package */ synchronized void userWantsAudioOff() { mUserWantsAudio = false; audioOff(); } /** Request to disconnect SCO (audio) connection to bluetooth * headset/handsfree, if one is connected. Does not block. */ /* package */ synchronized void audioOff() { if (VDBG) log("audioOff()"); if (mConnectedSco != null) { mAudioManager.setBluetoothScoOn(false); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); mConnectedSco.close(); mConnectedSco = null; } if (mOutgoingSco != null) { mOutgoingSco.close(); mOutgoingSco = null; } } /* package */ boolean isAudioOn() { return (mConnectedSco != null); } /* package */ void ignoreRing() { mBluetoothPhoneState.ignoreRing(); } private void sendURC(String urc) { if (isHeadsetConnected()) { mHeadset.sendURC(urc); } } /** helper to redial last dialled number */ private AtCommandResult redial() { String number = mPhonebook.getLastDialledNumber(); if (number == null) { // spec seems to suggest sending ERROR if we dont have a // number to redial if (DBG) log("Bluetooth redial requested (+BLDN), but no previous " + "outgoing calls found. Ignoring"); return new AtCommandResult(AtCommandResult.ERROR); } Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.fromParts("tel", number, null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); // We do not immediately respond OK, wait until we get a phone state // update. If we return OK now and the handsfree immeidately requests // our phone state it will say we are not in call yet which confuses // some devices expectCallStart(); return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing } /** Build the +CLCC result * The complexity arises from the fact that we need to maintain the same * CLCC index even as a call moves between states. */ private synchronized AtCommandResult getClccResult() { // Collect all known connections Connection[] clccConnections = new Connection[MAX_CONNECTIONS]; // indexed by CLCC index LinkedList newConnections = new LinkedList(); LinkedList connections = new LinkedList(); if (mRingingCall.getState().isAlive()) { connections.addAll(mRingingCall.getConnections()); } if (mForegroundCall.getState().isAlive()) { connections.addAll(mForegroundCall.getConnections()); } if (mBackgroundCall.getState().isAlive()) { connections.addAll(mBackgroundCall.getConnections()); } // Mark connections that we already known about boolean clccUsed[] = new boolean[MAX_CONNECTIONS]; for (int i = 0; i < MAX_CONNECTIONS; i++) { clccUsed[i] = mClccUsed[i]; mClccUsed[i] = false; } for (Connection c : connections) { boolean found = false; long timestamp = c.getCreateTime(); for (int i = 0; i < MAX_CONNECTIONS; i++) { if (clccUsed[i] && timestamp == mClccTimestamps[i]) { mClccUsed[i] = true; found = true; clccConnections[i] = c; break; } } if (!found) { newConnections.add(c); } } // Find a CLCC index for new connections while (!newConnections.isEmpty()) { // Find lowest empty index int i = 0; while (mClccUsed[i]) i++; // Find earliest connection long earliestTimestamp = newConnections.get(0).getCreateTime(); Connection earliestConnection = newConnections.get(0); for (int j = 0; j < newConnections.size(); j++) { long timestamp = newConnections.get(j).getCreateTime(); if (timestamp < earliestTimestamp) { earliestTimestamp = timestamp; earliestConnection = newConnections.get(j); } } // update mClccUsed[i] = true; mClccTimestamps[i] = earliestTimestamp; clccConnections[i] = earliestConnection; newConnections.remove(earliestConnection); } // Build CLCC AtCommandResult result = new AtCommandResult(AtCommandResult.OK); for (int i = 0; i < clccConnections.length; i++) { if (mClccUsed[i]) { String clccEntry = connectionToClccEntry(i, clccConnections[i]); if (clccEntry != null) { result.addResponse(clccEntry); } } } return result; } /** Convert a Connection object into a single +CLCC result */ private String connectionToClccEntry(int index, Connection c) { int state; switch (c.getState()) { case ACTIVE: state = 0; break; case HOLDING: state = 1; break; case DIALING: state = 2; break; case ALERTING: state = 3; break; case INCOMING: state = 4; break; case WAITING: state = 5; break; default: return null; // bad state } int mpty = 0; Call call = c.getCall(); if (call != null) { mpty = call.isMultiparty() ? 1 : 0; } int direction = c.isIncoming() ? 1 : 0; String number = c.getAddress(); int type = -1; if (number != null) { type = PhoneNumberUtils.toaFromString(number); } String result = "+CLCC: " + (index + 1) + "," + direction + "," + state + ",0," + mpty; if (number != null) { result += ",\"" + number + "\"," + type; } return result; } /** * Register AT Command handlers to implement the Headset profile */ private void initializeHeadsetAtParser() { if (DBG) log("Registering Headset AT commands"); AtParser parser = mHeadset.getAtParser(); // Headset's usually only have one button, which is meant to cause the // HS to send us AT+CKPD=200 or AT+CKPD. parser.register("+CKPD", new AtCommandHandler() { private AtCommandResult headsetButtonPress() { if (mRingingCall.isRinging()) { // Answer the call PhoneUtils.answerCall(mPhone); // If in-band ring tone is supported, SCO connection will already // be up and the following call will just return. audioOn(); } else if (mForegroundCall.getState().isAlive()) { if (!isAudioOn()) { // Transfer audio from AG to HS audioOn(); } else { if (mHeadset.getDirection() == HeadsetBase.DIRECTION_INCOMING && (System.currentTimeMillis() - mHeadset.getConnectTimestamp()) < 5000) { // Headset made a recent ACL connection to us - and // made a mandatory AT+CKPD request to connect // audio which races with our automatic audio // setup. ignore } else { // Hang up the call audioOff(); PhoneUtils.hangup(mPhone); } } } else { // No current call - redial last number return redial(); } return new AtCommandResult(AtCommandResult.OK); } @Override public AtCommandResult handleActionCommand() { return headsetButtonPress(); } @Override public AtCommandResult handleSetCommand(Object[] args) { return headsetButtonPress(); } }); } /** * Register AT Command handlers to implement the Handsfree profile */ private void initializeHandsfreeAtParser() { if (DBG) log("Registering Handsfree AT commands"); AtParser parser = mHeadset.getAtParser(); // Answer parser.register('A', new AtCommandHandler() { @Override public AtCommandResult handleBasicCommand(String args) { PhoneUtils.answerCall(mPhone); return new AtCommandResult(AtCommandResult.OK); } }); parser.register('D', new AtCommandHandler() { @Override public AtCommandResult handleBasicCommand(String args) { if (args.length() > 0) { if (args.charAt(0) == '>') { // Yuck - memory dialling requested. // Just dial last number for now if (args.startsWith(">9999")) { // for PTS test return new AtCommandResult(AtCommandResult.ERROR); } return redial(); } else { // Remove trailing ';' if (args.charAt(args.length() - 1) == ';') { args = args.substring(0, args.length() - 1); } Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.fromParts("tel", args, null)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); expectCallStart(); return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing } } return new AtCommandResult(AtCommandResult.ERROR); } }); // Hang-up command parser.register("+CHUP", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { if (!mForegroundCall.isIdle()) { PhoneUtils.hangup(mForegroundCall); } else if (!mRingingCall.isIdle()) { PhoneUtils.hangup(mRingingCall); } else if (!mBackgroundCall.isIdle()) { PhoneUtils.hangup(mBackgroundCall); } return new AtCommandResult(AtCommandResult.OK); } }); // Bluetooth Retrieve Supported Features command parser.register("+BRSF", new AtCommandHandler() { private AtCommandResult sendBRSF() { return new AtCommandResult("+BRSF: " + mLocalBrsf); } @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+BRSF= // Handsfree is telling us which features it supports. We // send the features we support if (args.length == 1 && (args[0] instanceof Integer)) { mRemoteBrsf = (Integer) args[0]; } else { Log.w(TAG, "HF didn't sent BRSF assuming 0"); } return sendBRSF(); } @Override public AtCommandResult handleActionCommand() { // This seems to be out of spec, but lets do the nice thing return sendBRSF(); } @Override public AtCommandResult handleReadCommand() { // This seems to be out of spec, but lets do the nice thing return sendBRSF(); } }); // Call waiting notification on/off parser.register("+CCWA", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // Seems to be out of spec, but lets return nicely return new AtCommandResult(AtCommandResult.OK); } @Override public AtCommandResult handleReadCommand() { // Call waiting is always on return new AtCommandResult("+CCWA: 1"); } @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+CCWA= // Handsfree is trying to enable/disable call waiting. We // cannot disable in the current implementation. return new AtCommandResult(AtCommandResult.OK); } @Override public AtCommandResult handleTestCommand() { // Request for range of supported CCWA paramters return new AtCommandResult("+CCWA: (\"n\",(1))"); } }); // Mobile Equipment Event Reporting enable/disable command // Of the full 3GPP syntax paramters (mode, keyp, disp, ind, bfr) we // only support paramter ind (disable/enable evert reporting using // +CDEV) parser.register("+CMER", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { return new AtCommandResult( "+CMER: 3,0,0," + (mIndicatorsEnabled ? "1" : "0")); } @Override public AtCommandResult handleSetCommand(Object[] args) { if (args.length < 4) { // This is a syntax error return new AtCommandResult(AtCommandResult.ERROR); } else if (args[0].equals(3) && args[1].equals(0) && args[2].equals(0)) { boolean valid = false; if (args[3].equals(0)) { mIndicatorsEnabled = false; valid = true; } else if (args[3].equals(1)) { mIndicatorsEnabled = true; valid = true; } if (valid) { if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) == 0x0) { mServiceConnectionEstablished = true; sendURC("OK"); // send immediately, then initiate audio if (isIncallAudio()) { audioOn(); } // only send OK once return new AtCommandResult(AtCommandResult.UNSOLICITED); } else { return new AtCommandResult(AtCommandResult.OK); } } } return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED); } @Override public AtCommandResult handleTestCommand() { return new AtCommandResult("+CMER: (3),(0),(0),(0-1)"); } }); // Mobile Equipment Error Reporting enable/disable parser.register("+CMEE", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // out of spec, assume they want to enable mCmee = true; return new AtCommandResult(AtCommandResult.OK); } @Override public AtCommandResult handleReadCommand() { return new AtCommandResult("+CMEE: " + (mCmee ? "1" : "0")); } @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+CMEE= if (args.length == 0) { // ommitted - default to 0 mCmee = false; return new AtCommandResult(AtCommandResult.OK); } else if (!(args[0] instanceof Integer)) { // Syntax error return new AtCommandResult(AtCommandResult.ERROR); } else { mCmee = ((Integer)args[0] == 1); return new AtCommandResult(AtCommandResult.OK); } } @Override public AtCommandResult handleTestCommand() { // Probably not required but spec, but no harm done return new AtCommandResult("+CMEE: (0-1)"); } }); // Bluetooth Last Dialled Number parser.register("+BLDN", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { return redial(); } }); // Indicator Update command parser.register("+CIND", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { return mBluetoothPhoneState.toCindResult(); } @Override public AtCommandResult handleTestCommand() { return mBluetoothPhoneState.getCindTestResult(); } }); // Query Signal Quality (legacy) parser.register("+CSQ", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { return mBluetoothPhoneState.toCsqResult(); } }); // Query network registration state parser.register("+CREG", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { return new AtCommandResult(mBluetoothPhoneState.toCregString()); } }); // Send DTMF. I don't know if we are also expected to play the DTMF tone // locally, right now we don't parser.register("+VTS", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { if (args.length >= 1) { char c; if (args[0] instanceof Integer) { c = ((Integer) args[0]).toString().charAt(0); } else { c = ((String) args[0]).charAt(0); } if (isValidDtmf(c)) { mPhone.sendDtmf(c); return new AtCommandResult(AtCommandResult.OK); } } return new AtCommandResult(AtCommandResult.ERROR); } private boolean isValidDtmf(char c) { switch (c) { case '#': case '*': return true; default: if (Character.digit(c, 14) != -1) { return true; // 0-9 and A-D } return false; } } }); // List calls parser.register("+CLCC", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { return getClccResult(); } }); // Call Hold and Multiparty Handling command parser.register("+CHLD", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { if (args.length >= 1) { if (args[0].equals(0)) { boolean result; if (mRingingCall.isRinging()) { result = PhoneUtils.hangupRingingCall(mPhone); } else { result = PhoneUtils.hangupHoldingCall(mPhone); } if (result) { return new AtCommandResult(AtCommandResult.OK); } else { return new AtCommandResult(AtCommandResult.ERROR); } } else if (args[0].equals(1)) { if (mPhone.getPhoneName().equals("CDMA")) { // For CDMA, there is no answerAndEndActive, so we can behave // the same way as CHLD=2 here PhoneUtils.answerCall(mPhone); PhoneUtils.setMute(mPhone, false); return new AtCommandResult(AtCommandResult.OK); } else { // Hangup active call, answer held call if (PhoneUtils.answerAndEndActive(mPhone)) { return new AtCommandResult(AtCommandResult.OK); } else { return new AtCommandResult(AtCommandResult.ERROR); } } } else if (args[0].equals(2)) { if (mPhone.getPhoneName().equals("CDMA")) { // For CDMA, the way we switch to a new incoming call is by // calling PhoneUtils.answerCall(). switchAndHoldActive() won't // properly update the call state within telephony. PhoneUtils.answerCall(mPhone); PhoneUtils.setMute(mPhone, false); } else { PhoneUtils.switchHoldingAndActive(mPhone); } return new AtCommandResult(AtCommandResult.OK); } else if (args[0].equals(3)) { if (mForegroundCall.getState().isAlive() && mBackgroundCall.getState().isAlive()) { PhoneUtils.mergeCalls(mPhone); } return new AtCommandResult(AtCommandResult.OK); } } return new AtCommandResult(AtCommandResult.ERROR); } @Override public AtCommandResult handleTestCommand() { mServiceConnectionEstablished = true; sendURC("+CHLD: (0,1,2,3)"); sendURC("OK"); // send reply first, then connect audio if (isIncallAudio()) { audioOn(); } // already replied return new AtCommandResult(AtCommandResult.UNSOLICITED); } }); // Get Network operator name parser.register("+COPS", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { String operatorName = mPhone.getServiceState().getOperatorAlphaLong(); if (operatorName != null) { if (operatorName.length() > 16) { operatorName = operatorName.substring(0, 16); } return new AtCommandResult( "+COPS: 0,0,\"" + operatorName + "\""); } else { return new AtCommandResult( "+COPS: 0,0,\"UNKNOWN\",0"); } } @Override public AtCommandResult handleSetCommand(Object[] args) { // Handsfree only supports AT+COPS=3,0 if (args.length != 2 || !(args[0] instanceof Integer) || !(args[1] instanceof Integer)) { // syntax error return new AtCommandResult(AtCommandResult.ERROR); } else if ((Integer)args[0] != 3 || (Integer)args[1] != 0) { return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED); } else { return new AtCommandResult(AtCommandResult.OK); } } @Override public AtCommandResult handleTestCommand() { // Out of spec, but lets be friendly return new AtCommandResult("+COPS: (3),(0)"); } }); // Mobile PIN // AT+CPIN is not in the handsfree spec (although it is in 3GPP) parser.register("+CPIN", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { return new AtCommandResult("+CPIN: READY"); } }); // Bluetooth Response and Hold // Only supported on PDC (Japan) and CDMA networks. parser.register("+BTRH", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { // Replying with just OK indicates no response and hold // features in use now return new AtCommandResult(AtCommandResult.OK); } @Override public AtCommandResult handleSetCommand(Object[] args) { // Neeed PDC or CDMA return new AtCommandResult(AtCommandResult.ERROR); } }); // Request International Mobile Subscriber Identity (IMSI) // Not in bluetooth handset spec parser.register("+CIMI", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // AT+CIMI String imsi = mPhone.getSubscriberId(); if (imsi == null || imsi.length() == 0) { return reportCmeError(BluetoothCmeError.SIM_FAILURE); } else { return new AtCommandResult(imsi); } } }); // Calling Line Identification Presentation parser.register("+CLIP", new AtCommandHandler() { @Override public AtCommandResult handleReadCommand() { // Currently assumes the network is provisioned for CLIP return new AtCommandResult("+CLIP: " + (mClip ? "1" : "0") + ",1"); } @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+CLIP= if (args.length >= 1 && (args[0].equals(0) || args[0].equals(1))) { mClip = args[0].equals(1); return new AtCommandResult(AtCommandResult.OK); } else { return new AtCommandResult(AtCommandResult.ERROR); } } @Override public AtCommandResult handleTestCommand() { return new AtCommandResult("+CLIP: (0-1)"); } }); // AT+CGSN - Returns the device IMEI number. parser.register("+CGSN", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // Get the IMEI of the device. // mPhone will not be NULL at this point. return new AtCommandResult("+CGSN: " + mPhone.getDeviceId()); } }); // AT+CGMM - Query Model Information parser.register("+CGMM", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // Return the Model Information. String model = SystemProperties.get("ro.product.model"); if (model != null) { return new AtCommandResult("+CGMM: " + model); } else { return new AtCommandResult(AtCommandResult.ERROR); } } }); // AT+CGMI - Query Manufacturer Information parser.register("+CGMI", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { // Return the Model Information. String manuf = SystemProperties.get("ro.product.manufacturer"); if (manuf != null) { return new AtCommandResult("+CGMI: " + manuf); } else { return new AtCommandResult(AtCommandResult.ERROR); } } }); // Noise Reduction and Echo Cancellation control parser.register("+NREC", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { if (args[0].equals(0)) { mAudioManager.setParameter(HEADSET_NREC, "off"); return new AtCommandResult(AtCommandResult.OK); } else if (args[0].equals(1)) { mAudioManager.setParameter(HEADSET_NREC, "on"); return new AtCommandResult(AtCommandResult.OK); } return new AtCommandResult(AtCommandResult.ERROR); } }); // Voice recognition (dialing) parser.register("+BVRA", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { if (BluetoothHeadset.DISABLE_BT_VOICE_DIALING) { return new AtCommandResult(AtCommandResult.ERROR); } if (args.length >= 1 && args[0].equals(1)) { synchronized (BluetoothHandsfree.this) { if (!mWaitingForVoiceRecognition) { try { mContext.startActivity(sVoiceCommandIntent); } catch (ActivityNotFoundException e) { return new AtCommandResult(AtCommandResult.ERROR); } expectVoiceRecognition(); } } return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing yet } else if (args.length >= 1 && args[0].equals(0)) { audioOff(); return new AtCommandResult(AtCommandResult.OK); } return new AtCommandResult(AtCommandResult.ERROR); } @Override public AtCommandResult handleTestCommand() { return new AtCommandResult("+BVRA: (0-1)"); } }); // Retrieve Subscriber Number parser.register("+CNUM", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { String number = mPhone.getLine1Number(); if (number == null) { return new AtCommandResult(AtCommandResult.OK); } return new AtCommandResult("+CNUM: ,\"" + number + "\"," + PhoneNumberUtils.toaFromString(number) + ",,4"); } }); // Microphone Gain parser.register("+VGM", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+VGM= in range [0,15] // Headset/Handsfree is reporting its current gain setting return new AtCommandResult(AtCommandResult.OK); } }); // Speaker Gain parser.register("+VGS", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { // AT+VGS= in range [0,15] if (args.length != 1 || !(args[0] instanceof Integer)) { return new AtCommandResult(AtCommandResult.ERROR); } mScoGain = (Integer) args[0]; int flag = mAudioManager.isBluetoothScoOn() ? AudioManager.FLAG_SHOW_UI:0; mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, mScoGain, flag); return new AtCommandResult(AtCommandResult.OK); } }); // Phone activity status parser.register("+CPAS", new AtCommandHandler() { @Override public AtCommandResult handleActionCommand() { int status = 0; switch (mPhone.getState()) { case IDLE: status = 0; break; case RINGING: status = 3; break; case OFFHOOK: status = 4; break; } return new AtCommandResult("+CPAS: " + status); } }); mPhonebook.register(parser); } public void sendScoGainUpdate(int gain) { if (mScoGain != gain && (mRemoteBrsf & BRSF_HF_REMOTE_VOL_CONTROL) != 0x0) { sendURC("+VGS:" + gain); mScoGain = gain; } } public AtCommandResult reportCmeError(int error) { if (mCmee) { AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); result.addResponse("+CME ERROR: " + error); return result; } else { return new AtCommandResult(AtCommandResult.ERROR); } } private static final int START_CALL_TIMEOUT = 10000; // ms private synchronized void expectCallStart() { mWaitingForCallStart = true; Message msg = Message.obtain(mHandler, CHECK_CALL_STARTED); mHandler.sendMessageDelayed(msg, START_CALL_TIMEOUT); if (!mStartCallWakeLock.isHeld()) { mStartCallWakeLock.acquire(START_CALL_TIMEOUT); } } private synchronized void callStarted() { if (mWaitingForCallStart) { mWaitingForCallStart = false; sendURC("OK"); if (mStartCallWakeLock.isHeld()) { mStartCallWakeLock.release(); } } } private static final int START_VOICE_RECOGNITION_TIMEOUT = 5000; // ms private synchronized void expectVoiceRecognition() { mWaitingForVoiceRecognition = true; Message msg = Message.obtain(mHandler, CHECK_VOICE_RECOGNITION_STARTED); mHandler.sendMessageDelayed(msg, START_VOICE_RECOGNITION_TIMEOUT); if (!mStartVoiceRecognitionWakeLock.isHeld()) { mStartVoiceRecognitionWakeLock.acquire(START_VOICE_RECOGNITION_TIMEOUT); } } /* package */ synchronized boolean startVoiceRecognition() { if (mWaitingForVoiceRecognition) { // HF initiated mWaitingForVoiceRecognition = false; sendURC("OK"); } else { // AG initiated sendURC("+BVRA: 1"); } boolean ret = audioOn(); if (mStartVoiceRecognitionWakeLock.isHeld()) { mStartVoiceRecognitionWakeLock.release(); } return ret; } /* package */ synchronized boolean stopVoiceRecognition() { sendURC("+BVRA: 0"); audioOff(); return true; } private boolean inDebug() { return DBG && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE, false); } private boolean allowAudioAnytime() { return inDebug() && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE_AUDIO_ANYTIME, false); } private void startDebug() { if (DBG && mDebugThread == null) { mDebugThread = new DebugThread(); mDebugThread.start(); } } private void stopDebug() { if (mDebugThread != null) { mDebugThread.interrupt(); mDebugThread = null; } } /** Debug thread to read debug properties - runs when debug.bt.hfp is true * at the time a bluetooth handsfree device is connected. Debug properties * are polled and mock updates sent every 1 second */ private class DebugThread extends Thread { /** Turns on/off handsfree profile debugging mode */ private static final String DEBUG_HANDSFREE = "debug.bt.hfp"; /** Mock battery level change - use 0 to 5 */ private static final String DEBUG_HANDSFREE_BATTERY = "debug.bt.hfp.battery"; /** Mock no cellular service when false */ private static final String DEBUG_HANDSFREE_SERVICE = "debug.bt.hfp.service"; /** Mock cellular roaming when true */ private static final String DEBUG_HANDSFREE_ROAM = "debug.bt.hfp.roam"; /** false to true transition will force an audio (SCO) connection to * be established. true to false will force audio to be disconnected */ private static final String DEBUG_HANDSFREE_AUDIO = "debug.bt.hfp.audio"; /** true allows incoming SCO connection out of call. */ private static final String DEBUG_HANDSFREE_AUDIO_ANYTIME = "debug.bt.hfp.audio_anytime"; /** Mock signal strength change in ASU - use 0 to 31 */ private static final String DEBUG_HANDSFREE_SIGNAL = "debug.bt.hfp.signal"; /** Debug AT+CLCC: print +CLCC result */ private static final String DEBUG_HANDSFREE_CLCC = "debug.bt.hfp.clcc"; /** Debug AT+BSIR - Send In Band Ringtones Unsolicited AT command. * debug.bt.unsol.inband = 0 => AT+BSIR = 0 sent by the AG * debug.bt.unsol.inband = 1 => AT+BSIR = 0 sent by the AG * Other values are ignored. */ private static final String DEBUG_UNSOL_INBAND_RINGTONE = "debug.bt.unsol.inband"; @Override public void run() { boolean oldService = true; boolean oldRoam = false; boolean oldAudio = false; while (!isInterrupted() && inDebug()) { int batteryLevel = SystemProperties.getInt(DEBUG_HANDSFREE_BATTERY, -1); if (batteryLevel >= 0 && batteryLevel <= 5) { Intent intent = new Intent(); intent.putExtra("level", batteryLevel); intent.putExtra("scale", 5); mBluetoothPhoneState.updateBatteryState(intent); } boolean serviceStateChanged = false; if (SystemProperties.getBoolean(DEBUG_HANDSFREE_SERVICE, true) != oldService) { oldService = !oldService; serviceStateChanged = true; } if (SystemProperties.getBoolean(DEBUG_HANDSFREE_ROAM, false) != oldRoam) { oldRoam = !oldRoam; serviceStateChanged = true; } if (serviceStateChanged) { Bundle b = new Bundle(); b.putInt("state", oldService ? 0 : 1); b.putBoolean("roaming", oldRoam); mBluetoothPhoneState.updateServiceState(true, ServiceState.newFromBundle(b)); } if (SystemProperties.getBoolean(DEBUG_HANDSFREE_AUDIO, false) != oldAudio) { oldAudio = !oldAudio; if (oldAudio) { audioOn(); } else { audioOff(); } } int signalLevel = SystemProperties.getInt(DEBUG_HANDSFREE_SIGNAL, -1); if (signalLevel >= 0 && signalLevel <= 31) { SignalStrength signalStrength = new SignalStrength(signalLevel, -1, -1, -1, -1, -1, -1, true); Intent intent = new Intent(); Bundle data = new Bundle(); signalStrength.fillInNotifierBundle(data); intent.putExtras(data); mBluetoothPhoneState.updateSignalState(intent); } if (SystemProperties.getBoolean(DEBUG_HANDSFREE_CLCC, false)) { log(getClccResult().toString()); } try { sleep(1000); // 1 second } catch (InterruptedException e) { break; } int inBandRing = SystemProperties.getInt(DEBUG_UNSOL_INBAND_RINGTONE, -1); if (inBandRing == 0 || inBandRing == 1) { AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); result.addResponse("+BSIR: " + inBandRing); sendURC(result.toString()); } } } } private static void log(String msg) { Log.d(TAG, msg); } }