/* * 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.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; 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.PhoneFactory; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.CallManager; import java.util.LinkedList; /** * Bluetooth headset manager for the Phone app. * @hide */ public class BluetoothHandsfree { private static final String TAG = "BluetoothHandsfree";//"BT HS/HF"; // private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 1) // && (SystemProperties.getInt("ro.debuggable", 0) == 1); private static final boolean DBG = true; private static final boolean VDBG = true;//(PhoneApp.DBG_LEVEL >= 2); // 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 CallManager mCM; private final BluetoothA2dp mA2dp; private BluetoothDevice mA2dpDevice; private int mA2dpState; 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 mPendingSco; // waiting for a2dp sink to suspend before establishing SCO private boolean mA2dpSuspended; 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 GSM_MAX_CONNECTIONS = 6; // Max connections allowed by GSM private static final int CDMA_MAX_CONNECTIONS = 2; // Max connections allowed by CDMA 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; CdmaPhoneCallState.PhoneCallState mCdmaThreeWayCallState = CdmaPhoneCallState.PhoneCallState.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; // CDMA specific flag used in context with BT devices having display capabilities // to show which Caller is active. This state might not be always true as in CDMA // networks if a caller drops off no update is provided to the Phone. // This flag is just used as a toggle to provide a update to the BT device to specify // which caller is active. private boolean mCdmaIsSecondCallActive = false; private boolean mCdmaCallsSwapped = false; private final Phone mPhone = PhoneFactory.getDefaultPhone(); private boolean mAudioConfirm = false; /* 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, CallManager cm) { mCM = cm; mContext = context; BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); boolean bluetoothCapable = (adapter != null); mHeadset = null; // nothing connected yet mA2dp = new BluetoothA2dp(mContext); mA2dpState = BluetoothA2dp.STATE_DISCONNECTED; mA2dpDevice = null; mA2dpSuspended = false; 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.isBluetoothVoiceDialingEnabled(mContext)) { mLocalBrsf |= BRSF_AG_VOICE_RECOG; } mBluetoothPhoneState = new BluetoothPhoneState(); mUserWantsAudio = true; mPhonebook = new BluetoothAtPhonebook(mContext, this); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); cdmaSetSecondCallState(false); if (bluetoothCapable) { resetAtState(); } } /* 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() { audioOff(); if (mIncomingSco != null) { mIncomingSco.close(); mIncomingSco = null; } } //true if ^conf unsolicited public boolean isAudioConfirm() { return mAudioConfirm; } private boolean isHeadsetConnected() { if (mHeadset == null) { return false; } return mHeadset.isConnected(); } /* package */ synchronized 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 = mCM.getActiveFgCallState(); return (state == Call.State.ACTIVE || state == Call.State.ALERTING); } /* package */ synchronized void disconnectHeadset() { // Close off the SCO sockets audioOff(); mHeadset = null; stopDebug(); resetAtState(); } /* package */ synchronized void resetAtState() { mClip = false; mIndicatorsEnabled = false; mServiceConnectionEstablished = false; mCmee = false; mClccTimestamps = new long[GSM_MAX_CONNECTIONS]; mClccUsed = new boolean[GSM_MAX_CONNECTIONS]; for (int i = 0; i < GSM_MAX_CONNECTIONS; i++) { mClccUsed[i] = false; } mRemoteBrsf = 0; mPhonebook.resetAtState(); } private void configAudioParameters() { String name = mHeadset.getRemoteDevice().getName(); if (name == null) { name = "<unknown>"; } mAudioManager.setParameters(HEADSET_NAME+"="+name+";"+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 boolean mStopRing = false; private static final int SERVICE_STATE_CHANGED = 1; private static final int PRECISE_CALL_STATE_CHANGED = 2; private static final int RING = 3; private static final int PHONE_CDMA_CALL_WAITING = 4; private static final int PRECISE_VIDEOCALL_STATE_CHANGED = 5; private static final int AUDIO_CONFIRM = 6; 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 PRECISE_CALL_STATE_CHANGED: case PHONE_CDMA_CALL_WAITING: case PRECISE_VIDEOCALL_STATE_CHANGED: Connection connection = null; if (((AsyncResult) msg.obj).result instanceof Connection) { connection = (Connection) ((AsyncResult) msg.obj).result; } handlePreciseCallStateChange(sendUpdate(), connection); break; case AUDIO_CONFIRM: mAudioConfirm = true; //for bug 16950:don't audioOn if speaker is on if (!PhoneUtils.isSpeakerOn(mContext)) { audioOn(); } } } }; private BluetoothPhoneState() { // init members // TODO May consider to repalce the default phone's state and signal // by CallManagter's state and signal updateServiceState(false, mCM.getDefaultPhone().getServiceState()); handlePreciseCallStateChange(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(mCM.getDefaultPhone().getSignalStrength()); // register for updates // Use the service state of default phone as BT service state to // avoid situation such as no cell or wifi connection but still // reporting in service (since SipPhone always reports in service). mCM.getDefaultPhone().registerForServiceStateChanged(mStateChangeHandler, SERVICE_STATE_CHANGED, null); mCM.registerForPreciseCallStateChanged(mStateChangeHandler, PRECISE_CALL_STATE_CHANGED, null); mCM.registerForSycnInd(mStateChangeHandler, AUDIO_CONFIRM, null); mPhone.registerForPreciseVideoCallStateChanged(mStateChangeHandler, PRECISE_VIDEOCALL_STATE_CHANGED, null); if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) { mPhone.registerForCallWaiting(mStateChangeHandler, PHONE_CDMA_CALL_WAITING, null); } mCM.registerForCallWaiting(mStateChangeHandler, PHONE_CDMA_CALL_WAITING, null); IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED); filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); mContext.registerReceiver(mStateReceiver, filter); } private void updateBtPhoneStateAfterRadioTechnologyChange() { if(VDBG) Log.d(TAG, "updateBtPhoneStateAfterRadioTechnologyChange..."); //Unregister all events from the old obsolete phone mCM.getDefaultPhone().unregisterForServiceStateChanged(mStateChangeHandler); mCM.unregisterForPreciseCallStateChanged(mStateChangeHandler); mCM.unregisterForCallWaiting(mStateChangeHandler); mCM.unregisterForSycnInd(mStateChangeHandler); //Register all events new to the new active phone mCM.getDefaultPhone().registerForServiceStateChanged(mStateChangeHandler, SERVICE_STATE_CHANGED, null); mCM.registerForPreciseCallStateChanged(mStateChangeHandler, PRECISE_CALL_STATE_CHANGED, null); mCM.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; } private void stopRing() { mStopRing = true; } /* convert [0,31] ASU signal strength to the [0,5] expected by * bluetooth devices. Scale is similar to status bar policy */ private int gsmAsuToSignal(SignalStrength signalStrength) { int asu = signalStrength.getGsmSignalStrength(); 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 the cdma / evdo db levels to appropriate icon level. * The scale is similar to the one used in status bar policy. * * @param signalStrength * @return the icon level */ private int cdmaDbmEcioToSignal(SignalStrength signalStrength) { int levelDbm = 0; int levelEcio = 0; int cdmaIconLevel = 0; int evdoIconLevel = 0; int cdmaDbm = signalStrength.getCdmaDbm(); int cdmaEcio = signalStrength.getCdmaEcio(); if (cdmaDbm >= -75) levelDbm = 4; else if (cdmaDbm >= -85) levelDbm = 3; else if (cdmaDbm >= -95) levelDbm = 2; else if (cdmaDbm >= -100) levelDbm = 1; else levelDbm = 0; // Ec/Io are in dB*10 if (cdmaEcio >= -90) levelEcio = 4; else if (cdmaEcio >= -110) levelEcio = 3; else if (cdmaEcio >= -130) levelEcio = 2; else if (cdmaEcio >= -150) levelEcio = 1; else levelEcio = 0; cdmaIconLevel = (levelDbm < levelEcio) ? levelDbm : levelEcio; if (mServiceState != null && (mServiceState.getRadioTechnology() == ServiceState.RADIO_TECHNOLOGY_EVDO_0 || mServiceState.getRadioTechnology() == ServiceState.RADIO_TECHNOLOGY_EVDO_A)) { int evdoEcio = signalStrength.getEvdoEcio(); int evdoSnr = signalStrength.getEvdoSnr(); int levelEvdoEcio = 0; int levelEvdoSnr = 0; // Ec/Io are in dB*10 if (evdoEcio >= -650) levelEvdoEcio = 4; else if (evdoEcio >= -750) levelEvdoEcio = 3; else if (evdoEcio >= -900) levelEvdoEcio = 2; else if (evdoEcio >= -1050) levelEvdoEcio = 1; else levelEvdoEcio = 0; if (evdoSnr > 7) levelEvdoSnr = 4; else if (evdoSnr > 5) levelEvdoSnr = 3; else if (evdoSnr > 3) levelEvdoSnr = 2; else if (evdoSnr > 1) levelEvdoSnr = 1; else levelEvdoSnr = 0; evdoIconLevel = (levelEvdoEcio < levelEvdoSnr) ? levelEvdoEcio : levelEvdoSnr; } // TODO(): There is a bug open regarding what should be sent. return (cdmaIconLevel > evdoIconLevel) ? cdmaIconLevel : evdoIconLevel; } private int asuToSignal(SignalStrength signalStrength) { if (signalStrength.isGsm()) { return gsmAsuToSignal(signalStrength); } else { return cdmaDbmEcioToSignal(signalStrength); } } /* 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); } else if (intent.getAction().equals(BluetoothA2dp.ACTION_SINK_STATE_CHANGED)) { int state = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, BluetoothA2dp.STATE_DISCONNECTED); int oldState = intent.getIntExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, BluetoothA2dp.STATE_DISCONNECTED); BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); // We are only concerned about Connected sinks to suspend and resume // them. We can safely ignore SINK_STATE_CHANGE for other devices. if (mA2dpDevice != null && !device.equals(mA2dpDevice)) return; synchronized (BluetoothHandsfree.this) { mA2dpState = state; if (state == BluetoothA2dp.STATE_DISCONNECTED) { mA2dpDevice = null; } else { mA2dpDevice = device; } if (oldState == BluetoothA2dp.STATE_PLAYING && mA2dpState == BluetoothA2dp.STATE_CONNECTED) { if (mA2dpSuspended) { if (mPendingSco) { mHandler.removeMessages(MESSAGE_CHECK_PENDING_SCO); if (DBG) log("A2DP suspended, completing SCO"); mOutgoingSco = createScoSocket(); if (!mOutgoingSco.connect( mHeadset.getRemoteDevice().getAddress(), mHeadset.getRemoteDevice().getName())) { mOutgoingSco = null; } mPendingSco = false; } } } } } } }; 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 if (mHeadset == null) { return; } 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); mServiceState = state; 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 handlePreciseCallStateChange(boolean sendUpdate, Connection connection) { int call = 0; int callsetup = 0; int callheld = 0; int prevCallsetup = mCallsetup; AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); Call foregroundCall = mCM.getActiveFgCall(); Call backgroundCall = mCM.getFirstActiveBgCall(); Call ringingCall = mCM.getFirstActiveRingingCall(); log("updatePhoneState(" + sendUpdate + ", " + connection + ")"); // This function will get called when the Precise Call State // {@link Call.State} changes. Hence, we might get this update // even if the {@link Phone.state} is same as before. // Check for the same. //Get the Call references from the new active phone again mRingingCall = mPhone.getRingingCall(); mForegroundCall = mPhone.getForegroundCall(); mBackgroundCall = mPhone.getBackgroundCall(); Phone.State newState = mCM.getState(); if (newState != mPhoneState) { mPhoneState = newState; switch (mPhoneState) { case IDLE: mUserWantsAudio = true; // out of call - reset state audioOff(); mAudioConfirm = false; break; default: callStarted(); } } switch(foregroundCall.getState()) { case ACTIVE: call = 1; mAudioPossible = true; break; case DIALING: callsetup = 2; mAudioPossible = true; // We also need to send a Call started indication // for cases where the 2nd MO was initiated was // from a *BT hands free* and is waiting for a // +BLND: OK response // There is a special case handling of the same case // for CDMA below if (mCM.getFgPhone().getPhoneType() == Phone.PHONE_TYPE_GSM) { callStarted(); } break; case ALERTING: callsetup = 3; // Open the SCO channel for the outgoing call. //for bug 16950:don't audioOn if speaker is on if (!PhoneUtils.isSpeakerOn(mContext)) { audioOn(); } mAudioPossible = true; break; case DISCONNECTING: // This is a transient state, we don't want to send // any AT commands during this state. call = mCall; callsetup = mCallsetup; callheld = mCallheld; break; default: mAudioPossible = false; } switch(ringingCall.getState()) { case INCOMING: case WAITING: callsetup = 1; break; case DISCONNECTING: // This is a transient state, we don't want to send // any AT commands during this state. call = mCall; callsetup = mCallsetup; callheld = mCallheld; break; } switch(backgroundCall.getState()) { case HOLDING: if (call == 1) { callheld = 1; } else { call = 1; callheld = 2; } break; case DISCONNECTING: // This is a transient state, we don't want to send // any AT commands during this state. call = mCall; callsetup = mCallsetup; callheld = mCallheld; break; } log("mCall: " + mCall + ", mCallsetup: " + mCallsetup + ", mCallheld: " + mCallheld); log("call: " + call + ", callsetup: " + callsetup + ", callheld: " + callheld); if (mCall != call) { if (call == 1) { // This means that a call has transitioned from NOT ACTIVE to ACTIVE. // Switch on audio. //for bug 16950:don't audioOn if speaker is on when dial if (!((prevCallsetup == 2 || prevCallsetup == 3) && PhoneUtils.isSpeakerOn(mContext))) { 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); } } } if (mCM.getDefaultPhone().getPhoneType() == Phone.PHONE_TYPE_CDMA) { PhoneApp app = PhoneApp.getInstance(); if (app.cdmaPhoneCallState != null) { CdmaPhoneCallState.PhoneCallState currCdmaThreeWayCallState = app.cdmaPhoneCallState.getCurrentCallState(); CdmaPhoneCallState.PhoneCallState prevCdmaThreeWayCallState = app.cdmaPhoneCallState.getPreviousCallState(); log("CDMA call state: " + currCdmaThreeWayCallState + " prev state:" + prevCdmaThreeWayCallState); callheld = getCdmaCallHeldStatus(currCdmaThreeWayCallState, prevCdmaThreeWayCallState); if (mCdmaThreeWayCallState != currCdmaThreeWayCallState) { // In CDMA, the network does not provide any feedback // to the phone when the 2nd MO call goes through the // stages of DIALING > ALERTING -> ACTIVE we fake the // sequence if ((currCdmaThreeWayCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) && app.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) { mAudioPossible = true; if (sendUpdate) { if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { result.addResponse("+CIEV: 3,2"); // Mimic putting the call on hold result.addResponse("+CIEV: 4,1"); mCallheld = callheld; result.addResponse("+CIEV: 3,3"); result.addResponse("+CIEV: 3,0"); } } // We also need to send a Call started indication // for cases where the 2nd MO was initiated was // from a *BT hands free* and is waiting for a // +BLND: OK response callStarted(); } // In CDMA, the network does not provide any feedback to // the phone when a user merges a 3way call or swaps // between two calls we need to send a CIEV response // indicating that a call state got changed which should // trigger a CLCC update request from the BT client. if (currCdmaThreeWayCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL && prevCdmaThreeWayCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { mAudioPossible = true; if (sendUpdate) { if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { result.addResponse("+CIEV: 2,1"); result.addResponse("+CIEV: 3,0"); } } } } mCdmaThreeWayCallState = currCdmaThreeWayCallState; } } boolean callsSwitched; if (mCM.getDefaultPhone().getPhoneType() == Phone.PHONE_TYPE_CDMA && mCdmaThreeWayCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { callsSwitched = mCdmaCallsSwapped; } else { callsSwitched = (callheld == 1 && ! (backgroundCall.getEarliestConnectTime() == mBgndEarliestConnectionTime)); mBgndEarliestConnectionTime = backgroundCall.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 = ringingCall.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; mStopRing = false; if ((mLocalBrsf & BRSF_AG_IN_BAND_RING) != 0x0) { audioOn(); } result.addResult(ring()); } } sendURC(result.toString()); } private int getCdmaCallHeldStatus(CdmaPhoneCallState.PhoneCallState currState, CdmaPhoneCallState.PhoneCallState prevState) { int callheld; // Update the Call held information if (currState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { if (prevState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { callheld = 0; //0: no calls held, as now *both* the caller are active } else { callheld = 1; //1: held call and active call, as on answering a // Call Waiting, one of the caller *is* put on hold } } else if (currState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { callheld = 1; //1: held call and active call, as on make a 3 Way Call // the first caller *is* put on hold } else { callheld = 0; //0: no calls held as this is a SINGLE_ACTIVE call } return callheld; } private AtCommandResult ring() { if (!mIgnoreRing && !mStopRing && mCM.getFirstActiveRingingCall().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 void updateCallHeld() { if (mCallheld != 0) { mCallheld = 0; sendURC("+CIEV: 4,0"); } } private synchronized AtCommandResult toCindResult() { AtCommandResult result = new AtCommandResult(AtCommandResult.OK); mSignal = asuToSignal(mCM.getDefaultPhone().getSignalStrength()); 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 static final int MESSAGE_CHECK_PENDING_SCO = 6; private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { synchronized (BluetoothHandsfree.this) { 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, mHeadset.getRemoteDevice()); } 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 (VDBG) log("Routing audio for outgoing SCO conection"); mConnectedSco = (ScoSocket)msg.obj; mAudioManager.setBluetoothScoOn(true); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED, mHeadset.getRemoteDevice()); } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) { if (VDBG) 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.close(); mConnectedSco = null; mAudioManager.setBluetoothScoOn(false); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED, mHeadset.getRemoteDevice()); } else if (mOutgoingSco == (ScoSocket)msg.obj) { mOutgoingSco.close(); mOutgoingSco = 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; case MESSAGE_CHECK_PENDING_SCO: if (mPendingSco && isA2dpMultiProfile()) { Log.w(TAG, "Timeout suspending A2DP for SCO (mA2dpState = " + mA2dpState + "). Starting SCO anyway"); mOutgoingSco = createScoSocket(); if (!(isHeadsetConnected() && mOutgoingSco.connect(mHeadset.getRemoteDevice().getAddress(), mHeadset.getRemoteDevice().getName()))) { mOutgoingSco = null; } mPendingSco = false; } break; } } } }; private ScoSocket createScoSocket() { return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED); } private void broadcastAudioStateIntent(int state, BluetoothDevice device) { if (VDBG) log("broadcastAudioStateIntent(" + state + ")"); Intent intent = new Intent(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); intent.putExtra(BluetoothHeadset.EXTRA_AUDIO_STATE, state); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); mContext.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); } void updateBtHandsfreeAfterRadioTechnologyChange() { if(VDBG) Log.d(TAG, "updateBtHandsfreeAfterRadioTechnologyChange..."); 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; } if (mPendingSco) { if (DBG) log("audioOn(): SCO already pending"); return true; } mA2dpSuspended = false; mPendingSco = false; if (isA2dpMultiProfile() && mA2dpState == BluetoothA2dp.STATE_PLAYING) { if (DBG) log("suspending A2DP stream for SCO"); mA2dpSuspended = mA2dp.suspendSink(mA2dpDevice); if (mA2dpSuspended) { mPendingSco = true; Message msg = mHandler.obtainMessage(MESSAGE_CHECK_PENDING_SCO); mHandler.sendMessageDelayed(msg, 2000); } else { Log.w(TAG, "Could not suspend A2DP stream for SCO, going ahead with SCO"); } } if (!mPendingSco) { mOutgoingSco = createScoSocket(); if (!mOutgoingSco.connect(mHeadset.getRemoteDevice().getAddress(), mHeadset.getRemoteDevice().getName())) { 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(): mPendingSco: " + mPendingSco + ", mConnectedSco: " + mConnectedSco + ", mOutgoingSco: " + mOutgoingSco + ", mA2dpState: " + mA2dpState + ", mA2dpSuspended: " + mA2dpSuspended + ", mIncomingSco:" + mIncomingSco); if (mA2dpSuspended) { if (isA2dpMultiProfile()) { if (DBG) log("resuming A2DP stream after disconnecting SCO"); mA2dp.resumeSink(mA2dpDevice); } mA2dpSuspended = false; } mPendingSco = false; if (mConnectedSco != null) { BluetoothDevice device = null; if (mHeadset != null) { device = mHeadset.getRemoteDevice(); } mConnectedSco.close(); mConnectedSco = null; mAudioManager.setBluetoothScoOn(false); broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED, device); } if (mOutgoingSco != null) { mOutgoingSco.close(); mOutgoingSco = null; } } /* package */ boolean isAudioOn() { return (mConnectedSco != null); } private boolean isA2dpMultiProfile() { return mA2dp != null && mHeadset != null && mA2dpDevice != null && mA2dpDevice.equals(mHeadset.getRemoteDevice()); } /* package */ void ignoreRing() { mBluetoothPhoneState.ignoreRing(); } private void sendURC(String urc) { log("sendURC: " + 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 (VDBG) 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 gsmGetClccResult() { // Collect all known connections Connection[] clccConnections = new Connection[GSM_MAX_CONNECTIONS]; // indexed by CLCC index LinkedList<Connection> newConnections = new LinkedList<Connection>(); LinkedList<Connection> connections = new LinkedList<Connection>(); Call foregroundCall = mCM.getActiveFgCall(); Call backgroundCall = mCM.getFirstActiveBgCall(); Call ringingCall = mCM.getFirstActiveRingingCall(); if (ringingCall.getState().isAlive()) { connections.addAll(ringingCall.getConnections()); } if (foregroundCall.getState().isAlive()) { connections.addAll(foregroundCall.getConnections()); } if (backgroundCall.getState().isAlive()) { connections.addAll(backgroundCall.getConnections()); } // Mark connections that we already known about boolean clccUsed[] = new boolean[GSM_MAX_CONNECTIONS]; for (int i = 0; i < GSM_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 < GSM_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; } /** Build the +CLCC result for CDMA * 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 cdmaGetClccResult() { // In CDMA at one time a user can have only two live/active connections Connection[] clccConnections = new Connection[CDMA_MAX_CONNECTIONS];// indexed by CLCC index Call foregroundCall = mCM.getActiveFgCall(); Call ringingCall = mCM.getFirstActiveRingingCall(); Call.State ringingCallState = ringingCall.getState(); // If the Ringing Call state is INCOMING, that means this is the very first call // hence there should not be any Foreground Call if (ringingCallState == Call.State.INCOMING) { if (VDBG) log("Filling clccConnections[0] for INCOMING state"); clccConnections[0] = ringingCall.getLatestConnection(); } else if (foregroundCall.getState().isAlive()) { // Getting Foreground Call connection based on Call state if (ringingCall.isRinging()) { if (VDBG) log("Filling clccConnections[0] & [1] for CALL WAITING state"); clccConnections[0] = foregroundCall.getEarliestConnection(); clccConnections[1] = ringingCall.getLatestConnection(); } else { if (foregroundCall.getConnections().size() <= 1) { // Single call scenario if (VDBG) log("Filling clccConnections[0] with ForgroundCall latest connection"); clccConnections[0] = foregroundCall.getLatestConnection(); } else { // Multiple Call scenario. This would be true for both // CONF_CALL and THRWAY_ACTIVE state if (VDBG) log("Filling clccConnections[0] & [1] with ForgroundCall connections"); clccConnections[0] = foregroundCall.getEarliestConnection(); clccConnections[1] = foregroundCall.getLatestConnection(); } } } // Update the mCdmaIsSecondCallActive flag based on the Phone call state if (PhoneApp.getInstance().cdmaPhoneCallState.getCurrentCallState() == CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE) { cdmaSetSecondCallState(false); } else if (PhoneApp.getInstance().cdmaPhoneCallState.getCurrentCallState() == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { cdmaSetSecondCallState(true); } // Build CLCC AtCommandResult result = new AtCommandResult(AtCommandResult.OK); for (int i = 0; (i < clccConnections.length) && (clccConnections[i] != null); i++) { String clccEntry = cdmaConnectionToClccEntry(i, clccConnections[i]); if (clccEntry != null) { result.addResponse(clccEntry); } } return result; } /** Convert a Connection object into a single +CLCC result for CDMA phones */ private String cdmaConnectionToClccEntry(int index, Connection c) { int state; PhoneApp app = PhoneApp.getInstance(); CdmaPhoneCallState.PhoneCallState currCdmaCallState = app.cdmaPhoneCallState.getCurrentCallState(); CdmaPhoneCallState.PhoneCallState prevCdmaCallState = app.cdmaPhoneCallState.getPreviousCallState(); if ((prevCdmaCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) && (currCdmaCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL)) { // If the current state is reached after merging two calls // we set the state of all the connections as ACTIVE state = 0; } else { switch (c.getState()) { case ACTIVE: // For CDMA since both the connections are set as active by FW after accepting // a Call waiting or making a 3 way call, we need to set the state specifically // to ACTIVE/HOLDING based on the mCdmaIsSecondCallActive flag. This way the // CLCC result will allow BT devices to enable the swap or merge options if (index == 0) { // For the 1st active connection state = mCdmaIsSecondCallActive ? 1 : 0; } else { // for the 2nd active connection state = mCdmaIsSecondCallActive ? 0 : 1; } 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; if (currCdmaCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { if (prevCdmaCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { // If the current state is reached after merging two calls // we set the multiparty call true. mpty = 1; } else { // CALL_CONF state is not from merging two calls, but from // accepting the second call. In this case first will be on // hold. mpty = 0; } } else { mpty = 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 (VDBG) 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 (mCM.getFirstActiveRingingCall().isRinging()) { // Answer the call mBluetoothPhoneState.stopRing(); sendURC("OK"); PhoneUtils.answerCall(mCM.getFirstActiveRingingCall()); // If in-band ring tone is supported, SCO connection will already // be up and the following call will just return. audioOn(); return new AtCommandResult(AtCommandResult.UNSOLICITED); } else if (mCM.hasActiveFgCall()) { 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(PhoneApp.getInstance().mCM); } } return new AtCommandResult(AtCommandResult.OK); } else { // No current call - redial last number return redial(); } } @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 (VDBG) log("Registering Handsfree AT commands"); AtParser parser = mHeadset.getAtParser(); final Phone phone = mCM.getDefaultPhone(); // Answer parser.register('A', new AtCommandHandler() { @Override public AtCommandResult handleBasicCommand(String args) { sendURC("OK"); mBluetoothPhoneState.stopRing(); PhoneUtils.answerCall(mCM.getFirstActiveRingingCall()); return new AtCommandResult(AtCommandResult.UNSOLICITED); } }); 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() { sendURC("OK"); if (mCM.hasActiveFgCall()) { PhoneUtils.hangupActiveCall(mCM.getActiveFgCall()); } else if (mCM.hasActiveRingingCall()) { PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall()); } else if (mCM.hasActiveBgCall()) { PhoneUtils.hangupHoldingCall(mCM.getFirstActiveBgCall()); } return new AtCommandResult(AtCommandResult.UNSOLICITED); } }); // 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 supported features bitmap> // 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=<n> // 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=<n> if (args.length == 0) { // <n> 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)) { phone.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() { int phoneType = phone.getPhoneType(); if (phoneType == Phone.PHONE_TYPE_CDMA) { return cdmaGetClccResult(); } else if (phoneType == Phone.PHONE_TYPE_GSM) { return gsmGetClccResult(); } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } } }); // Call Hold and Multiparty Handling command parser.register("+CHLD", new AtCommandHandler() { @Override public AtCommandResult handleSetCommand(Object[] args) { int phoneType = phone.getPhoneType(); Call ringingCall = mCM.getFirstActiveRingingCall(); Call backgroundCall = mCM.getFirstActiveBgCall(); if (args.length >= 1) { if (args[0].equals(0)) { boolean result; if (ringingCall.isRinging()) { result = PhoneUtils.hangupRingingCall(ringingCall); } else { result = PhoneUtils.hangupHoldingCall(backgroundCall); } if (result) { return new AtCommandResult(AtCommandResult.OK); } else { return new AtCommandResult(AtCommandResult.ERROR); } } else if (args[0].equals(1)) { if (phoneType == Phone.PHONE_TYPE_CDMA) { if (ringingCall.isRinging()) { // Hangup the active call and then answer call waiting call. if (VDBG) log("CHLD:1 Callwaiting Answer call"); PhoneUtils.hangup(PhoneApp.getInstance().mCM); PhoneUtils.answerCall(ringingCall); PhoneUtils.setMute(false); } else { // If there is no Call waiting then just hangup // the active call. In CDMA this mean that the complete // call session would be ended if (VDBG) log("CHLD:1 Hangup Call"); PhoneUtils.hangup(PhoneApp.getInstance().mCM); } return new AtCommandResult(AtCommandResult.OK); } else if (phoneType == Phone.PHONE_TYPE_GSM) { // Hangup active call, answer held call //TS for compile //restore ls for bug 11040 if (PhoneUtils.answerAndEndActive(PhoneApp.getInstance().mCM, ringingCall)) { return new AtCommandResult(AtCommandResult.OK); } else { return new AtCommandResult(AtCommandResult.ERROR); } } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } } else if (args[0].equals(2)) { sendURC("OK"); if (phoneType == Phone.PHONE_TYPE_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. // If the Phone state is already in CONF_CALL then we simply send // a flash cmd by calling switchHoldingAndActive() if (ringingCall.isRinging()) { if (VDBG) log("CHLD:2 Callwaiting Answer call"); PhoneUtils.answerCall(ringingCall); PhoneUtils.setMute(false); // Setting the second callers state flag to TRUE (i.e. active) cdmaSetSecondCallState(true); } else if (PhoneApp.getInstance().cdmaPhoneCallState .getCurrentCallState() == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { if (VDBG) log("CHLD:2 Swap Calls"); PhoneUtils.switchHoldingAndActive(backgroundCall); // Toggle the second callers active state flag cdmaSwapSecondCallState(); } } else if (phoneType == Phone.PHONE_TYPE_GSM) { PhoneUtils.switchHoldingAndActive(backgroundCall); } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } return new AtCommandResult(AtCommandResult.UNSOLICITED); } else if (args[0].equals(3)) { sendURC("OK"); if (phoneType == Phone.PHONE_TYPE_CDMA) { CdmaPhoneCallState.PhoneCallState state = PhoneApp.getInstance().cdmaPhoneCallState.getCurrentCallState(); // For CDMA, we need to check if the call is in THRWAY_ACTIVE state if (state == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) { if (VDBG) log("CHLD:3 Merge Calls"); PhoneUtils.mergeCalls(); } else if (state == CdmaPhoneCallState.PhoneCallState.CONF_CALL) { // State is CONF_CALL already and we are getting a merge call // This can happen when CONF_CALL was entered from a Call Waiting mBluetoothPhoneState.updateCallHeld(); } } else if (phoneType == Phone.PHONE_TYPE_GSM) { if (mCM.hasActiveFgCall() && mCM.hasActiveBgCall()) { PhoneUtils.mergeCalls(); } } else { throw new IllegalStateException("Unexpected phone type: " + phoneType); } return new AtCommandResult(AtCommandResult.UNSOLICITED); } } 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 = phone.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 = phone.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=<n> 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. // phone will not be NULL at this point. return new AtCommandResult("+CGSN: " + phone.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.setParameters(HEADSET_NREC+"=off"); return new AtCommandResult(AtCommandResult.OK); } else if (args[0].equals(1)) { mAudioManager.setParameters(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.isBluetoothVoiceDialingEnabled(mContext)) { 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 = phone.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=<gain> 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=<gain> 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 (mCM.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(gsmGetClccResult().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()); } } } } public void cdmaSwapSecondCallState() { if (VDBG) log("cdmaSetSecondCallState: Toggling mCdmaIsSecondCallActive"); mCdmaIsSecondCallActive = !mCdmaIsSecondCallActive; mCdmaCallsSwapped = true; } public void cdmaSetSecondCallState(boolean state) { if (VDBG) log("cdmaSetSecondCallState: Setting mCdmaIsSecondCallActive to " + state); mCdmaIsSecondCallActive = state; if (!mCdmaIsSecondCallActive) { mCdmaCallsSwapped = false; } } private static void log(String msg) { Log.d(TAG, msg); } }