2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.phone;
19 import android.bluetooth.AtCommandHandler;
20 import android.bluetooth.AtCommandResult;
21 import android.bluetooth.AtParser;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothHeadset;
24 import android.bluetooth.BluetoothIntent;
25 import android.bluetooth.HeadsetBase;
26 import android.bluetooth.ScoSocket;
27 import android.content.ActivityNotFoundException;
28 import android.content.BroadcastReceiver;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.media.AudioManager;
33 import android.net.Uri;
34 import android.os.AsyncResult;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Message;
38 import android.os.PowerManager;
39 import android.os.PowerManager.WakeLock;
40 import android.os.SystemProperties;
41 import android.telephony.PhoneNumberUtils;
42 import android.telephony.ServiceState;
43 import android.util.Log;
45 import com.android.internal.telephony.Call;
46 import com.android.internal.telephony.Connection;
47 import com.android.internal.telephony.Phone;
48 import com.android.internal.telephony.TelephonyIntents;
50 import java.util.LinkedList;
52 * Bluetooth headset manager for the Phone app.
55 public class BluetoothHandsfree {
56 private static final String TAG = "BT HS/HF";
57 private static final boolean DBG = false;
58 private static final boolean VDBG = false; // even more logging
60 public static final int TYPE_UNKNOWN = 0;
61 public static final int TYPE_HEADSET = 1;
62 public static final int TYPE_HANDSFREE = 2;
64 private final Context mContext;
65 private final Phone mPhone;
66 private ServiceState mServiceState;
67 private HeadsetBase mHeadset; // null when not connected
68 private int mHeadsetType;
69 private boolean mAudioPossible;
70 private ScoSocket mIncomingSco;
71 private ScoSocket mOutgoingSco;
72 private ScoSocket mConnectedSco;
74 private Call mForegroundCall;
75 private Call mBackgroundCall;
76 private Call mRingingCall;
78 private AudioManager mAudioManager;
79 private PowerManager mPowerManager;
81 private boolean mUserWantsAudio;
82 private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call
83 private WakeLock mStartVoiceRecognitionWakeLock; // held while waiting for voice recognition
86 private static final int MAX_CONNECTIONS = 6; // Max connections allowed by GSM
88 private long mBgndEarliestConnectionTime = 0;
89 private boolean mClip = false; // Calling Line Information Presentation
90 private boolean mIndicatorsEnabled = false;
91 private boolean mCmee = false; // Extended Error reporting
92 private long[] mClccTimestamps; // Timestamps associated with each clcc index
93 private boolean[] mClccUsed; // Is this clcc index in use
94 private boolean mWaitingForCallStart;
95 private boolean mWaitingForVoiceRecognition;
97 private final BluetoothPhoneState mPhoneState; // for CIND and CIEV updates
98 private final BluetoothAtPhonebook mPhonebook;
100 private DebugThread mDebugThread;
101 private int mScoGain = Integer.MIN_VALUE;
103 private static Intent sVoiceCommandIntent;
106 private static final String HEADSET_NREC = "bt_headset_nrec";
107 private static final String HEADSET_NAME = "bt_headset_name";
109 private int mRemoteBrsf = 0;
110 private int mLocalBrsf = 0;
112 /* Constants from Bluetooth Specification Hands-Free profile version 1.5 */
113 private static final int BRSF_AG_THREE_WAY_CALLING = 1 << 0;
114 private static final int BRSF_AG_EC_NR = 1 << 1;
115 private static final int BRSF_AG_VOICE_RECOG = 1 << 2;
116 private static final int BRSF_AG_IN_BAND_RING = 1 << 3;
117 private static final int BRSF_AG_VOICE_TAG_NUMBE = 1 << 4;
118 private static final int BRSF_AG_REJECT_CALL = 1 << 5;
119 private static final int BRSF_AG_ENHANCED_CALL_STATUS = 1 << 6;
120 private static final int BRSF_AG_ENHANCED_CALL_CONTROL = 1 << 7;
121 private static final int BRSF_AG_ENHANCED_ERR_RESULT_CODES = 1 << 8;
123 private static final int BRSF_HF_EC_NR = 1 << 0;
124 private static final int BRSF_HF_CW_THREE_WAY_CALLING = 1 << 1;
125 private static final int BRSF_HF_CLIP = 1 << 2;
126 private static final int BRSF_HF_VOICE_REG_ACT = 1 << 3;
127 private static final int BRSF_HF_REMOTE_VOL_CONTROL = 1 << 4;
128 private static final int BRSF_HF_ENHANCED_CALL_STATUS = 1 << 5;
129 private static final int BRSF_HF_ENHANCED_CALL_CONTROL = 1 << 6;
131 public static String typeToString(int type) {
143 public BluetoothHandsfree(Context context, Phone phone) {
146 BluetoothDevice bluetooth =
147 (BluetoothDevice)context.getSystemService(Context.BLUETOOTH_SERVICE);
148 boolean bluetoothCapable = (bluetooth != null);
149 mHeadset = null; // nothing connected yet
151 mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
152 mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
154 mStartCallWakeLock.setReferenceCounted(false);
155 mStartVoiceRecognitionWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
156 TAG + ":VoiceRecognition");
157 mStartVoiceRecognitionWakeLock.setReferenceCounted(false);
159 mLocalBrsf = BRSF_AG_THREE_WAY_CALLING |
161 BRSF_AG_REJECT_CALL |
162 BRSF_AG_ENHANCED_CALL_STATUS;
164 if (sVoiceCommandIntent == null) {
165 sVoiceCommandIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
166 sVoiceCommandIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
168 if (mContext.getPackageManager().resolveActivity(sVoiceCommandIntent, 0) != null &&
169 !BluetoothHeadset.DISABLE_BT_VOICE_DIALING) {
170 mLocalBrsf |= BRSF_AG_VOICE_RECOG;
173 if (bluetoothCapable) {
177 mRingingCall = mPhone.getRingingCall();
178 mForegroundCall = mPhone.getForegroundCall();
179 mBackgroundCall = mPhone.getBackgroundCall();
180 mPhoneState = new BluetoothPhoneState();
181 mUserWantsAudio = true;
182 mPhonebook = new BluetoothAtPhonebook(mContext, this);
183 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
186 /* package */ synchronized void onBluetoothEnabled() {
187 /* Bluez has a bug where it will always accept and then orphan
188 * incoming SCO connections, regardless of whether we have a listening
189 * SCO socket. So the best thing to do is always run a listening socket
190 * while bluetooth is on so that at least we can diconnect it
191 * immediately when we don't want it.
193 if (mIncomingSco == null) {
194 mIncomingSco = createScoSocket();
195 mIncomingSco.accept();
199 /* package */ synchronized void onBluetoothDisabled() {
200 if (mConnectedSco != null) {
201 mAudioManager.setBluetoothScoOn(false);
202 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED);
203 mConnectedSco.close();
204 mConnectedSco = null;
206 if (mOutgoingSco != null) {
207 mOutgoingSco.close();
210 if (mIncomingSco != null) {
211 mIncomingSco.close();
216 private boolean isHeadsetConnected() {
217 if (mHeadset == null) {
220 return mHeadset.isConnected();
223 /* package */ void connectHeadset(HeadsetBase headset, int headsetType) {
225 mHeadsetType = headsetType;
226 if (mHeadsetType == TYPE_HEADSET) {
227 initializeHeadsetAtParser();
229 initializeHandsfreeAtParser();
231 headset.startEventThread();
232 configAudioParameters();
238 if (isIncallAudio()) {
243 /* returns true if there is some kind of in-call audio we may wish to route
245 private boolean isIncallAudio() {
246 Call.State state = mForegroundCall.getState();
248 return (state == Call.State.ACTIVE || state == Call.State.ALERTING);
251 /* package */ void disconnectHeadset() {
257 private void resetAtState() {
259 mIndicatorsEnabled = false;
261 mClccTimestamps = new long[MAX_CONNECTIONS];
262 mClccUsed = new boolean[MAX_CONNECTIONS];
263 for (int i = 0; i < MAX_CONNECTIONS; i++) {
264 mClccUsed[i] = false;
269 private void configAudioParameters() {
270 String name = mHeadset.getName();
274 mAudioManager.setParameter(HEADSET_NAME, name);
275 mAudioManager.setParameter(HEADSET_NREC, "on");
279 /** Represents the data that we send in a +CIND or +CIEV command to the HF
281 private class BluetoothPhoneState {
284 private int mService;
287 // 1: active call (where active means audio is routed - not held call)
290 // 0: not in call setup
291 // 1: incoming call setup
292 // 2: outgoing call setup
293 // 3: remote party being alerted in an outgoing call setup
294 private int mCallsetup;
297 // 1: held call and active call
299 private int mCallheld;
301 // cellular signal strength of AG: 0-5
304 // cellular signal strength in CSQ rssi scale
305 private int mRssi; // for CSQ
307 // 0: roaming not active (home)
311 // battery charge of AG: 0-5
312 private int mBattchg;
315 // 1: registered, home network
316 // 5: registered, roaming
317 private int mStat; // for CREG
319 private String mRingingNumber; // Context for in-progress RING's
320 private int mRingingType;
321 private boolean mIgnoreRing = false;
323 private static final int SERVICE_STATE_CHANGED = 1;
324 private static final int PHONE_STATE_CHANGED = 2;
325 private static final int RING = 3;
327 private Handler mStateChangeHandler = new Handler() {
329 public void handleMessage(Message msg) {
332 AtCommandResult result = ring();
333 if (result != null) {
334 sendURC(result.toString());
337 case SERVICE_STATE_CHANGED:
338 ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
339 updateServiceState(sendUpdate(), state);
341 case PHONE_STATE_CHANGED:
342 Connection connection = null;
343 if (((AsyncResult) msg.obj).result instanceof Connection) {
344 connection = (Connection) ((AsyncResult) msg.obj).result;
346 updatePhoneState(sendUpdate(), connection);
352 private BluetoothPhoneState() {
354 updateServiceState(false, mPhone.getServiceState());
355 updatePhoneState(false, null);
356 mBattchg = 5; // There is currently no API to get battery level
357 // on demand, so set to 5 and wait for an update
358 mSignal = asuToSignal(mPhone.getSignalStrengthASU());
360 // register for updates
361 mPhone.registerForServiceStateChanged(mStateChangeHandler,
362 SERVICE_STATE_CHANGED, null);
363 mPhone.registerForPhoneStateChanged(mStateChangeHandler,
364 PHONE_STATE_CHANGED, null);
365 IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
366 filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED);
367 mContext.registerReceiver(mStateReceiver, filter);
370 private boolean sendUpdate() {
371 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mIndicatorsEnabled;
374 private boolean sendClipUpdate() {
375 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mClip;
378 /* convert [0,31] ASU signal strength to the [0,5] expected by
379 * bluetooth devices. Scale is similar to status bar policy
381 private int asuToSignal(int asu) {
382 if (asu >= 16) return 5;
383 else if (asu >= 8) return 4;
384 else if (asu >= 4) return 3;
385 else if (asu >= 2) return 2;
386 else if (asu >= 1) return 1;
390 /* convert [0,5] signal strength to a rssi signal strength for CSQ
391 * which is [0,31]. Despite the same scale, this is not the same value
394 private int signalToRssi(int signal) {
395 // using C4A suggested values
408 private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() {
410 public void onReceive(Context context, Intent intent) {
411 if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
412 updateBatteryState(intent);
413 } else if (intent.getAction().equals(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED)) {
414 updateSignalState(intent);
419 private synchronized void updateBatteryState(Intent intent) {
420 int batteryLevel = intent.getIntExtra("level", -1);
421 int scale = intent.getIntExtra("scale", -1);
422 if (batteryLevel == -1 || scale == -1) {
425 batteryLevel = batteryLevel * 5 / scale;
426 if (mBattchg != batteryLevel) {
427 mBattchg = batteryLevel;
429 sendURC("+CIEV: 7," + mBattchg);
434 private synchronized void updateSignalState(Intent intent) {
436 signal = asuToSignal(intent.getIntExtra("asu", -1));
437 mRssi = signalToRssi(signal); // no unsolicited CSQ
438 if (signal != mSignal) {
441 sendURC("+CIEV: 5," + mSignal);
446 private synchronized void updateServiceState(boolean sendUpdate, ServiceState state) {
447 int service = state.getState() == ServiceState.STATE_IN_SERVICE ? 1 : 0;
448 int roam = state.getRoaming() ? 1 : 0;
450 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
455 stat = (roam == 1) ? 5 : 1;
458 if (service != mService) {
461 result.addResponse("+CIEV: 1," + mService);
467 result.addResponse("+CIEV: 6," + mRoam);
473 result.addResponse(toCregString());
477 sendURC(result.toString());
480 private synchronized void updatePhoneState(boolean sendUpdate, Connection connection) {
484 int prevCallsetup = mCallsetup;
485 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
487 if (DBG) log("updatePhoneState()");
489 switch (mPhone.getState()) {
491 mUserWantsAudio = true; // out of call - reset state
498 switch(mForegroundCall.getState()) {
501 mAudioPossible = true;
505 mAudioPossible = false;
509 // Open the SCO channel for the outgoing call.
511 mAudioPossible = true;
514 mAudioPossible = false;
517 switch(mRingingCall.getState()) {
524 switch(mBackgroundCall.getState()) {
537 // This means that a call has transitioned from NOT ACTIVE to ACTIVE.
543 result.addResponse("+CIEV: 2," + mCall);
546 if (mCallsetup != callsetup) {
547 mCallsetup = callsetup;
549 // If mCall = 0, send CIEV
550 // mCall = 1, mCallsetup = 0, send CIEV
551 // mCall = 1, mCallsetup = 1, send CIEV after CCWA,
552 // if 3 way supported.
553 // mCall = 1, mCallsetup = 2 / 3 -> send CIEV,
554 // if 3 way is supported
555 if (mCall != 1 || mCallsetup == 0 ||
556 mCallsetup != 1 && (mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) {
557 result.addResponse("+CIEV: 3," + mCallsetup);
562 boolean callsSwitched =
563 (callheld == 1 && ! (mBackgroundCall.getEarliestConnectTime() ==
564 mBgndEarliestConnectionTime));
566 mBgndEarliestConnectionTime = mBackgroundCall.getEarliestConnectTime();
568 if (mCallheld != callheld || callsSwitched) {
569 mCallheld = callheld;
571 result.addResponse("+CIEV: 4," + mCallheld);
575 if (callsetup == 1 && callsetup != prevCallsetup) {
577 String number = null;
579 // find incoming phone number and type
580 if (connection == null) {
581 connection = mRingingCall.getEarliestConnection();
582 if (connection == null) {
583 Log.e(TAG, "Could not get a handle on Connection object for new " +
587 if (connection != null) {
588 number = connection.getAddress();
589 if (number != null) {
590 type = PhoneNumberUtils.toaFromString(number);
593 if (number == null) {
596 if ((call != 0 || callheld != 0) && sendUpdate) {
598 if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) {
599 result.addResponse("+CCWA: \"" + number + "\"," + type);
600 result.addResponse("+CIEV: 3," + callsetup);
603 // regular new incoming call
604 mRingingNumber = number;
608 if ((mLocalBrsf & BRSF_AG_IN_BAND_RING) == 0x1) {
611 result.addResult(ring());
614 sendURC(result.toString());
617 private AtCommandResult ring() {
618 if (!mIgnoreRing && mRingingCall.isRinging()) {
619 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
620 result.addResponse("RING");
621 if (sendClipUpdate()) {
622 result.addResponse("+CLIP: \"" + mRingingNumber + "\"," + mRingingType);
625 Message msg = mStateChangeHandler.obtainMessage(RING);
626 mStateChangeHandler.sendMessageDelayed(msg, 3000);
632 private synchronized String toCregString() {
633 return new String("+CREG: 1," + mStat);
636 private synchronized AtCommandResult toCindResult() {
637 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
638 String status = "+CIND: " + mService + "," + mCall + "," + mCallsetup + "," +
639 mCallheld + "," + mSignal + "," + mRoam + "," + mBattchg;
640 result.addResponse(status);
644 private synchronized AtCommandResult toCsqResult() {
645 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
646 String status = "+CSQ: " + mRssi + ",99";
647 result.addResponse(status);
652 private synchronized AtCommandResult getCindTestResult() {
653 return new AtCommandResult("+CIND: (\"service\",(0-1))," + "(\"call\",(0-1))," +
654 "(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5))," +
655 "(\"roam\",(0-1)),(\"battchg\",(0-5))");
658 private synchronized void ignoreRing() {
662 sendURC("+CIEV: 3," + mCallsetup);
668 private static final int SCO_ACCEPTED = 1;
669 private static final int SCO_CONNECTED = 2;
670 private static final int SCO_CLOSED = 3;
671 private static final int CHECK_CALL_STARTED = 4;
672 private static final int CHECK_VOICE_RECOGNITION_STARTED = 5;
674 private final Handler mHandler = new Handler() {
676 public synchronized void handleMessage(Message msg) {
679 if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
680 if (isHeadsetConnected() && (mAudioPossible || allowAudioAnytime()) &&
681 mConnectedSco == null) {
682 Log.i(TAG, "Routing audio for incoming SCO connection");
683 mConnectedSco = (ScoSocket)msg.obj;
684 mAudioManager.setBluetoothScoOn(true);
685 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED);
687 Log.i(TAG, "Rejecting incoming SCO connection");
688 ((ScoSocket)msg.obj).close();
690 } // else error trying to accept, try again
691 mIncomingSco = createScoSocket();
692 mIncomingSco.accept();
695 if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected() &&
696 mConnectedSco == null) {
697 if (DBG) log("Routing audio for outgoing SCO conection");
698 mConnectedSco = (ScoSocket)msg.obj;
699 mAudioManager.setBluetoothScoOn(true);
700 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED);
701 } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
702 if (DBG) log("Rejecting new connected outgoing SCO socket");
703 ((ScoSocket)msg.obj).close();
704 mOutgoingSco.close();
709 if (mConnectedSco == (ScoSocket)msg.obj) {
710 mConnectedSco = null;
711 mAudioManager.setBluetoothScoOn(false);
712 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED);
713 } else if (mOutgoingSco == (ScoSocket)msg.obj) {
715 } else if (mIncomingSco == (ScoSocket)msg.obj) {
719 case CHECK_CALL_STARTED:
720 if (mWaitingForCallStart) {
721 mWaitingForCallStart = false;
722 Log.e(TAG, "Timeout waiting for call to start");
724 if (mStartCallWakeLock.isHeld()) {
725 mStartCallWakeLock.release();
729 case CHECK_VOICE_RECOGNITION_STARTED:
730 if (mWaitingForVoiceRecognition) {
731 mWaitingForVoiceRecognition = false;
732 Log.e(TAG, "Timeout waiting for voice recognition to start");
740 private ScoSocket createScoSocket() {
741 return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED);
744 private void broadcastAudioStateIntent(int state) {
745 if (VDBG) log("broadcastAudioStateIntent(" + state + ")");
746 Intent intent = new Intent(BluetoothIntent.HEADSET_AUDIO_STATE_CHANGED_ACTION);
747 intent.putExtra(BluetoothIntent.HEADSET_AUDIO_STATE, state);
748 mContext.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH);
751 /** Request to establish SCO (audio) connection to bluetooth
752 * headset/handsfree, if one is connected. Does not block.
753 * Returns false if the user has requested audio off, or if there
754 * is some other immediate problem that will prevent BT audio.
756 /* package */ synchronized boolean audioOn() {
757 if (VDBG) log("audioOn()");
758 if (!isHeadsetConnected()) {
759 if (DBG) log("audioOn(): headset is not connected!");
763 if (mConnectedSco != null) {
764 if (DBG) log("audioOn(): audio is already connected");
768 if (!mUserWantsAudio) {
769 if (DBG) log("audioOn(): user requested no audio, ignoring");
773 if (mOutgoingSco != null) {
774 if (DBG) log("audioOn(): outgoing SCO already in progress");
777 mOutgoingSco = createScoSocket();
778 if (!mOutgoingSco.connect(mHeadset.getAddress())) {
785 /** Used to indicate the user requested BT audio on.
786 * This will establish SCO (BT audio), even if the user requested it off
787 * previously on this call.
789 /* package */ synchronized void userWantsAudioOn() {
790 mUserWantsAudio = true;
793 /** Used to indicate the user requested BT audio off.
794 * This will prevent us from establishing BT audio again during this call
795 * if audioOn() is called.
797 /* package */ synchronized void userWantsAudioOff() {
798 mUserWantsAudio = false;
802 /** Request to disconnect SCO (audio) connection to bluetooth
803 * headset/handsfree, if one is connected. Does not block.
805 /* package */ synchronized void audioOff() {
806 if (VDBG) log("audioOff()");
808 if (mConnectedSco != null) {
809 mAudioManager.setBluetoothScoOn(false);
810 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED);
811 mConnectedSco.close();
812 mConnectedSco = null;
814 if (mOutgoingSco != null) {
815 mOutgoingSco.close();
820 /* package */ boolean isAudioOn() {
821 return (mConnectedSco != null);
824 /* package */ void ignoreRing() {
825 mPhoneState.ignoreRing();
828 private void sendURC(String urc) {
829 if (isHeadsetConnected()) {
830 mHeadset.sendURC(urc);
834 /** helper to redial last dialled number */
835 private AtCommandResult redial() {
836 String number = mPhonebook.getLastDialledNumber();
837 if (number == null) {
838 // spec seems to suggest sending ERROR if we dont have a
840 if (DBG) log("Bluetooth redial requested (+BLDN), but no previous " +
841 "outgoing calls found. Ignoring");
842 return new AtCommandResult(AtCommandResult.ERROR);
844 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
845 Uri.fromParts("tel", number, null));
846 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
847 mContext.startActivity(intent);
849 // We do not immediately respond OK, wait until we get a phone state
850 // update. If we return OK now and the handsfree immeidately requests
851 // our phone state it will say we are not in call yet which confuses
854 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
857 /** Build the +CLCC result
858 * The complexity arises from the fact that we need to maintain the same
859 * CLCC index even as a call moves between states. */
860 private synchronized AtCommandResult getClccResult() {
861 // Collect all known connections
862 Connection[] clccConnections = new Connection[MAX_CONNECTIONS]; // indexed by CLCC index
863 LinkedList<Connection> newConnections = new LinkedList<Connection>();
864 LinkedList<Connection> connections = new LinkedList<Connection>();
865 if (mRingingCall.getState().isAlive()) {
866 connections.addAll(mRingingCall.getConnections());
868 if (mForegroundCall.getState().isAlive()) {
869 connections.addAll(mForegroundCall.getConnections());
871 if (mBackgroundCall.getState().isAlive()) {
872 connections.addAll(mBackgroundCall.getConnections());
875 // Mark connections that we already known about
876 boolean clccUsed[] = new boolean[MAX_CONNECTIONS];
877 for (int i = 0; i < MAX_CONNECTIONS; i++) {
878 clccUsed[i] = mClccUsed[i];
879 mClccUsed[i] = false;
881 for (Connection c : connections) {
882 boolean found = false;
883 long timestamp = c.getCreateTime();
884 for (int i = 0; i < MAX_CONNECTIONS; i++) {
885 if (clccUsed[i] && timestamp == mClccTimestamps[i]) {
888 clccConnections[i] = c;
893 newConnections.add(c);
897 // Find a CLCC index for new connections
898 while (!newConnections.isEmpty()) {
899 // Find lowest empty index
901 while (mClccUsed[i]) i++;
902 // Find earliest connection
903 long earliestTimestamp = newConnections.get(0).getCreateTime();
904 Connection earliestConnection = newConnections.get(0);
905 for (int j = 0; j < newConnections.size(); j++) {
906 long timestamp = newConnections.get(j).getCreateTime();
907 if (timestamp < earliestTimestamp) {
908 earliestTimestamp = timestamp;
909 earliestConnection = newConnections.get(j);
915 mClccTimestamps[i] = earliestTimestamp;
916 clccConnections[i] = earliestConnection;
917 newConnections.remove(earliestConnection);
921 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
922 for (int i = 0; i < clccConnections.length; i++) {
924 String clccEntry = connectionToClccEntry(i, clccConnections[i]);
925 if (clccEntry != null) {
926 result.addResponse(clccEntry);
934 /** Convert a Connection object into a single +CLCC result */
935 private String connectionToClccEntry(int index, Connection c) {
937 switch (c.getState()) {
957 return null; // bad state
961 Call call = c.getCall();
963 mpty = call.isMultiparty() ? 1 : 0;
966 int direction = c.isIncoming() ? 1 : 0;
968 String number = c.getAddress();
970 if (number != null) {
971 type = PhoneNumberUtils.toaFromString(number);
974 String result = "+CLCC: " + (index + 1) + "," + direction + "," + state + ",0," + mpty;
975 if (number != null) {
976 result += ",\"" + number + "\"," + type;
981 * Register AT Command handlers to implement the Headset profile
983 private void initializeHeadsetAtParser() {
984 if (DBG) log("Registering Headset AT commands");
985 AtParser parser = mHeadset.getAtParser();
986 // Headset's usually only have one button, which is meant to cause the
987 // HS to send us AT+CKPD=200 or AT+CKPD.
988 parser.register("+CKPD", new AtCommandHandler() {
989 private AtCommandResult headsetButtonPress() {
990 if (mRingingCall.isRinging()) {
992 PhoneUtils.answerCall(mPhone);
993 // If in-band ring tone is supported, SCO connection will already
994 // be up and the following call will just return.
996 } else if (mForegroundCall.getState().isAlive()) {
998 // Transfer audio from AG to HS
1001 if (mHeadset.getDirection() == HeadsetBase.DIRECTION_INCOMING &&
1002 (System.currentTimeMillis() - mHeadset.getConnectTimestamp()) < 5000) {
1003 // Headset made a recent ACL connection to us - and
1004 // made a mandatory AT+CKPD request to connect
1005 // audio which races with our automatic audio
1010 PhoneUtils.hangup(mPhone);
1014 // No current call - redial last number
1017 return new AtCommandResult(AtCommandResult.OK);
1020 public AtCommandResult handleActionCommand() {
1021 return headsetButtonPress();
1024 public AtCommandResult handleSetCommand(Object[] args) {
1025 return headsetButtonPress();
1031 * Register AT Command handlers to implement the Handsfree profile
1033 private void initializeHandsfreeAtParser() {
1034 if (DBG) log("Registering Handsfree AT commands");
1035 AtParser parser = mHeadset.getAtParser();
1038 parser.register('A', new AtCommandHandler() {
1040 public AtCommandResult handleBasicCommand(String args) {
1041 PhoneUtils.answerCall(mPhone);
1042 return new AtCommandResult(AtCommandResult.OK);
1045 parser.register('D', new AtCommandHandler() {
1047 public AtCommandResult handleBasicCommand(String args) {
1048 if (args.length() > 0) {
1049 if (args.charAt(0) == '>') {
1050 // Yuck - memory dialling requested.
1051 // Just dial last number for now
1052 if (args.startsWith(">9999")) { // for PTS test
1053 return new AtCommandResult(AtCommandResult.ERROR);
1057 // Remove trailing ';'
1058 if (args.charAt(args.length() - 1) == ';') {
1059 args = args.substring(0, args.length() - 1);
1061 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
1062 Uri.fromParts("tel", args, null));
1063 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1064 mContext.startActivity(intent);
1067 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
1070 return new AtCommandResult(AtCommandResult.ERROR);
1075 parser.register("+CHUP", new AtCommandHandler() {
1077 public AtCommandResult handleActionCommand() {
1078 if (!mForegroundCall.isIdle()) {
1079 PhoneUtils.hangup(mForegroundCall);
1080 } else if (!mRingingCall.isIdle()) {
1081 PhoneUtils.hangup(mRingingCall);
1082 } else if (!mBackgroundCall.isIdle()) {
1083 PhoneUtils.hangup(mBackgroundCall);
1085 return new AtCommandResult(AtCommandResult.OK);
1089 // Bluetooth Retrieve Supported Features command
1090 parser.register("+BRSF", new AtCommandHandler() {
1091 private AtCommandResult sendBRSF() {
1092 return new AtCommandResult("+BRSF: " + mLocalBrsf);
1095 public AtCommandResult handleSetCommand(Object[] args) {
1096 // AT+BRSF=<handsfree supported features bitmap>
1097 // Handsfree is telling us which features it supports. We
1098 // send the features we support
1099 if (args.length == 1 && (args[0] instanceof Integer)) {
1100 mRemoteBrsf = (Integer) args[0];
1102 Log.w(TAG, "HF didn't sent BRSF assuming 0");
1107 public AtCommandResult handleActionCommand() {
1108 // This seems to be out of spec, but lets do the nice thing
1112 public AtCommandResult handleReadCommand() {
1113 // This seems to be out of spec, but lets do the nice thing
1118 // Call waiting notification on/off
1119 parser.register("+CCWA", new AtCommandHandler() {
1121 public AtCommandResult handleActionCommand() {
1122 // Seems to be out of spec, but lets return nicely
1123 return new AtCommandResult(AtCommandResult.OK);
1126 public AtCommandResult handleReadCommand() {
1127 // Call waiting is always on
1128 return new AtCommandResult("+CCWA: 1");
1131 public AtCommandResult handleSetCommand(Object[] args) {
1133 // Handsfree is trying to enable/disable call waiting. We
1134 // cannot disable in the current implementation.
1135 return new AtCommandResult(AtCommandResult.OK);
1138 public AtCommandResult handleTestCommand() {
1139 // Request for range of supported CCWA paramters
1140 return new AtCommandResult("+CCWA: (\"n\",(1))");
1144 // Mobile Equipment Event Reporting enable/disable command
1145 // Of the full 3GPP syntax paramters (mode, keyp, disp, ind, bfr) we
1146 // only support paramter ind (disable/enable evert reporting using
1148 parser.register("+CMER", new AtCommandHandler() {
1150 public AtCommandResult handleReadCommand() {
1151 return new AtCommandResult(
1152 "+CMER: 3,0,0," + (mIndicatorsEnabled ? "1" : "0"));
1155 public AtCommandResult handleSetCommand(Object[] args) {
1156 if (args.length < 4) {
1157 // This is a syntax error
1158 return new AtCommandResult(AtCommandResult.ERROR);
1159 } else if (args[0].equals(3) && args[1].equals(0) &&
1160 args[2].equals(0)) {
1161 if (args[3].equals(0)) {
1162 mIndicatorsEnabled = false;
1163 return new AtCommandResult(AtCommandResult.OK);
1164 } else if (args[3].equals(1)) {
1165 mIndicatorsEnabled = true;
1166 return new AtCommandResult(AtCommandResult.OK);
1168 return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
1170 return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
1174 public AtCommandResult handleTestCommand() {
1175 return new AtCommandResult("+CMER: (3),(0),(0),(0-1)");
1179 // Mobile Equipment Error Reporting enable/disable
1180 parser.register("+CMEE", new AtCommandHandler() {
1182 public AtCommandResult handleActionCommand() {
1183 // out of spec, assume they want to enable
1185 return new AtCommandResult(AtCommandResult.OK);
1188 public AtCommandResult handleReadCommand() {
1189 return new AtCommandResult("+CMEE: " + (mCmee ? "1" : "0"));
1192 public AtCommandResult handleSetCommand(Object[] args) {
1194 if (args.length == 0) {
1195 // <n> ommitted - default to 0
1197 return new AtCommandResult(AtCommandResult.OK);
1198 } else if (!(args[0] instanceof Integer)) {
1200 return new AtCommandResult(AtCommandResult.ERROR);
1202 mCmee = ((Integer)args[0] == 1);
1203 return new AtCommandResult(AtCommandResult.OK);
1207 public AtCommandResult handleTestCommand() {
1208 // Probably not required but spec, but no harm done
1209 return new AtCommandResult("+CMEE: (0-1)");
1213 // Bluetooth Last Dialled Number
1214 parser.register("+BLDN", new AtCommandHandler() {
1216 public AtCommandResult handleActionCommand() {
1221 // Indicator Update command
1222 parser.register("+CIND", new AtCommandHandler() {
1224 public AtCommandResult handleReadCommand() {
1225 return mPhoneState.toCindResult();
1228 public AtCommandResult handleTestCommand() {
1229 return mPhoneState.getCindTestResult();
1233 // Query Signal Quality (legacy)
1234 parser.register("+CSQ", new AtCommandHandler() {
1236 public AtCommandResult handleActionCommand() {
1237 return mPhoneState.toCsqResult();
1241 // Query network registration state
1242 parser.register("+CREG", new AtCommandHandler() {
1244 public AtCommandResult handleReadCommand() {
1245 return new AtCommandResult(mPhoneState.toCregString());
1249 // Send DTMF. I don't know if we are also expected to play the DTMF tone
1250 // locally, right now we don't
1251 parser.register("+VTS", new AtCommandHandler() {
1253 public AtCommandResult handleSetCommand(Object[] args) {
1254 if (args.length >= 1) {
1256 if (args[0] instanceof Integer) {
1257 c = ((Integer) args[0]).toString().charAt(0);
1259 c = ((String) args[0]).charAt(0);
1261 if (isValidDtmf(c)) {
1263 return new AtCommandResult(AtCommandResult.OK);
1266 return new AtCommandResult(AtCommandResult.ERROR);
1268 private boolean isValidDtmf(char c) {
1274 if (Character.digit(c, 14) != -1) {
1275 return true; // 0-9 and A-D
1283 parser.register("+CLCC", new AtCommandHandler() {
1285 public AtCommandResult handleActionCommand() {
1286 return getClccResult();
1290 // Call Hold and Multiparty Handling command
1291 parser.register("+CHLD", new AtCommandHandler() {
1293 public AtCommandResult handleSetCommand(Object[] args) {
1294 if (args.length >= 1) {
1295 if (args[0].equals(0)) {
1297 if (mRingingCall.isRinging()) {
1298 result = PhoneUtils.hangupRingingCall(mPhone);
1300 result = PhoneUtils.hangupHoldingCall(mPhone);
1303 return new AtCommandResult(AtCommandResult.OK);
1305 return new AtCommandResult(AtCommandResult.ERROR);
1307 } else if (args[0].equals(1)) {
1308 // Hangup active call, answer held call
1309 if (PhoneUtils.answerAndEndActive(mPhone)) {
1310 return new AtCommandResult(AtCommandResult.OK);
1312 return new AtCommandResult(AtCommandResult.ERROR);
1314 } else if (args[0].equals(2)) {
1315 PhoneUtils.switchHoldingAndActive(mPhone);
1316 return new AtCommandResult(AtCommandResult.OK);
1317 } else if (args[0].equals(3)) {
1318 if (mForegroundCall.getState().isAlive() &&
1319 mBackgroundCall.getState().isAlive()) {
1320 PhoneUtils.mergeCalls(mPhone);
1322 return new AtCommandResult(AtCommandResult.OK);
1325 return new AtCommandResult(AtCommandResult.ERROR);
1328 public AtCommandResult handleTestCommand() {
1329 return new AtCommandResult("+CHLD: (0,1,2,3)");
1333 // Get Network operator name
1334 parser.register("+COPS", new AtCommandHandler() {
1336 public AtCommandResult handleReadCommand() {
1337 String operatorName = mPhone.getServiceState().getOperatorAlphaLong();
1338 if (operatorName != null) {
1339 if (operatorName.length() > 16) {
1340 operatorName = operatorName.substring(0, 16);
1342 return new AtCommandResult(
1343 "+COPS: 0,0,\"" + operatorName + "\"");
1345 return new AtCommandResult(
1346 "+COPS: 0,0,\"UNKNOWN\",0");
1350 public AtCommandResult handleSetCommand(Object[] args) {
1351 // Handsfree only supports AT+COPS=3,0
1352 if (args.length != 2 || !(args[0] instanceof Integer)
1353 || !(args[1] instanceof Integer)) {
1355 return new AtCommandResult(AtCommandResult.ERROR);
1356 } else if ((Integer)args[0] != 3 || (Integer)args[1] != 0) {
1357 return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED);
1359 return new AtCommandResult(AtCommandResult.OK);
1363 public AtCommandResult handleTestCommand() {
1364 // Out of spec, but lets be friendly
1365 return new AtCommandResult("+COPS: (3),(0)");
1370 // AT+CPIN is not in the handsfree spec (although it is in 3GPP)
1371 parser.register("+CPIN", new AtCommandHandler() {
1373 public AtCommandResult handleReadCommand() {
1374 return new AtCommandResult("+CPIN: READY");
1378 // Bluetooth Response and Hold
1379 // Only supported on PDC (Japan) and CDMA networks.
1380 parser.register("+BTRH", new AtCommandHandler() {
1382 public AtCommandResult handleReadCommand() {
1383 // Replying with just OK indicates no response and hold
1384 // features in use now
1385 return new AtCommandResult(AtCommandResult.OK);
1388 public AtCommandResult handleSetCommand(Object[] args) {
1389 // Neeed PDC or CDMA
1390 return new AtCommandResult(AtCommandResult.ERROR);
1394 // Request International Mobile Subscriber Identity (IMSI)
1395 // Not in bluetooth handset spec
1396 parser.register("+CIMI", new AtCommandHandler() {
1398 public AtCommandResult handleActionCommand() {
1400 String imsi = mPhone.getSubscriberId();
1401 if (imsi == null || imsi.length() == 0) {
1402 return reportCmeError(BluetoothCmeError.SIM_FAILURE);
1404 return new AtCommandResult(imsi);
1409 // Calling Line Identification Presentation
1410 parser.register("+CLIP", new AtCommandHandler() {
1412 public AtCommandResult handleReadCommand() {
1413 // Currently assumes the network is provisioned for CLIP
1414 return new AtCommandResult("+CLIP: " + (mClip ? "1" : "0") + ",1");
1417 public AtCommandResult handleSetCommand(Object[] args) {
1419 if (args.length >= 1 && (args[0].equals(0) || args[0].equals(1))) {
1420 mClip = args[0].equals(1);
1421 return new AtCommandResult(AtCommandResult.OK);
1423 return new AtCommandResult(AtCommandResult.ERROR);
1427 public AtCommandResult handleTestCommand() {
1428 return new AtCommandResult("+CLIP: (0-1)");
1432 // AT+CGSN - Returns the device IMEI number.
1433 parser.register("+CGSN", new AtCommandHandler() {
1435 public AtCommandResult handleActionCommand() {
1436 // Get the IMEI of the device.
1437 // mPhone will not be NULL at this point.
1438 return new AtCommandResult("+CGSN: " + mPhone.getDeviceId());
1442 // AT+CGMM - Query Model Information
1443 parser.register("+CGMM", new AtCommandHandler() {
1445 public AtCommandResult handleActionCommand() {
1446 // Return the Model Information.
1447 String model = SystemProperties.get("ro.product.model");
1448 if (model != null) {
1449 return new AtCommandResult("+CGMM: " + model);
1451 return new AtCommandResult(AtCommandResult.ERROR);
1456 // AT+CGMI - Query Manufacturer Information
1457 parser.register("+CGMI", new AtCommandHandler() {
1459 public AtCommandResult handleActionCommand() {
1460 // Return the Model Information.
1461 String manuf = SystemProperties.get("ro.product.manufacturer");
1462 if (manuf != null) {
1463 return new AtCommandResult("+CGMI: " + manuf);
1465 return new AtCommandResult(AtCommandResult.ERROR);
1470 // Noise Reduction and Echo Cancellation control
1471 parser.register("+NREC", new AtCommandHandler() {
1473 public AtCommandResult handleSetCommand(Object[] args) {
1474 if (args[0].equals(0)) {
1475 mAudioManager.setParameter(HEADSET_NREC, "off");
1476 return new AtCommandResult(AtCommandResult.OK);
1477 } else if (args[0].equals(1)) {
1478 mAudioManager.setParameter(HEADSET_NREC, "on");
1479 return new AtCommandResult(AtCommandResult.OK);
1481 return new AtCommandResult(AtCommandResult.ERROR);
1485 // Voice recognition (dialing)
1486 parser.register("+BVRA", new AtCommandHandler() {
1488 public AtCommandResult handleSetCommand(Object[] args) {
1489 if (BluetoothHeadset.DISABLE_BT_VOICE_DIALING) {
1490 return new AtCommandResult(AtCommandResult.ERROR);
1492 if (args.length >= 1 && args[0].equals(1)) {
1493 synchronized (BluetoothHandsfree.this) {
1494 if (!mWaitingForVoiceRecognition) {
1496 mContext.startActivity(sVoiceCommandIntent);
1497 } catch (ActivityNotFoundException e) {
1498 return new AtCommandResult(AtCommandResult.ERROR);
1500 expectVoiceRecognition();
1503 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing yet
1504 } else if (args.length >= 1 && args[0].equals(0)) {
1506 return new AtCommandResult(AtCommandResult.OK);
1508 return new AtCommandResult(AtCommandResult.ERROR);
1511 public AtCommandResult handleTestCommand() {
1512 return new AtCommandResult("+BVRA: (0-1)");
1516 // Retrieve Subscriber Number
1517 parser.register("+CNUM", new AtCommandHandler() {
1519 public AtCommandResult handleActionCommand() {
1520 String number = mPhone.getLine1Number();
1521 if (number == null) {
1522 return new AtCommandResult(AtCommandResult.OK);
1524 return new AtCommandResult("+CNUM: ,\"" + number + "\"," +
1525 PhoneNumberUtils.toaFromString(number) + ",,4");
1530 parser.register("+VGM", new AtCommandHandler() {
1532 public AtCommandResult handleSetCommand(Object[] args) {
1533 // AT+VGM=<gain> in range [0,15]
1534 // Headset/Handsfree is reporting its current gain setting
1535 return new AtCommandResult(AtCommandResult.OK);
1540 parser.register("+VGS", new AtCommandHandler() {
1542 public AtCommandResult handleSetCommand(Object[] args) {
1543 // AT+VGS=<gain> in range [0,15]
1544 if (args.length != 1 || !(args[0] instanceof Integer)) {
1545 return new AtCommandResult(AtCommandResult.ERROR);
1547 mScoGain = (Integer) args[0];
1548 int flag = mAudioManager.isBluetoothScoOn() ? AudioManager.FLAG_SHOW_UI:0;
1550 mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, mScoGain, flag);
1551 return new AtCommandResult(AtCommandResult.OK);
1555 // Phone activity status
1556 parser.register("+CPAS", new AtCommandHandler() {
1558 public AtCommandResult handleActionCommand() {
1560 switch (mPhone.getState()) {
1571 return new AtCommandResult("+CPAS: " + status);
1574 mPhonebook.register(parser);
1577 public void sendScoGainUpdate(int gain) {
1578 if (mScoGain != gain && (mRemoteBrsf & BRSF_HF_REMOTE_VOL_CONTROL) != 0x0) {
1579 sendURC("+VGS:" + gain);
1584 public AtCommandResult reportCmeError(int error) {
1586 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
1587 result.addResponse("+CME ERROR: " + error);
1590 return new AtCommandResult(AtCommandResult.ERROR);
1594 private static final int START_CALL_TIMEOUT = 10000; // ms
1596 private synchronized void expectCallStart() {
1597 mWaitingForCallStart = true;
1598 Message msg = Message.obtain(mHandler, CHECK_CALL_STARTED);
1599 mHandler.sendMessageDelayed(msg, START_CALL_TIMEOUT);
1600 if (!mStartCallWakeLock.isHeld()) {
1601 mStartCallWakeLock.acquire(START_CALL_TIMEOUT);
1605 private synchronized void callStarted() {
1606 if (mWaitingForCallStart) {
1607 mWaitingForCallStart = false;
1609 if (mStartCallWakeLock.isHeld()) {
1610 mStartCallWakeLock.release();
1615 private static final int START_VOICE_RECOGNITION_TIMEOUT = 5000; // ms
1617 private synchronized void expectVoiceRecognition() {
1618 mWaitingForVoiceRecognition = true;
1619 Message msg = Message.obtain(mHandler, CHECK_VOICE_RECOGNITION_STARTED);
1620 mHandler.sendMessageDelayed(msg, START_VOICE_RECOGNITION_TIMEOUT);
1621 if (!mStartVoiceRecognitionWakeLock.isHeld()) {
1622 mStartVoiceRecognitionWakeLock.acquire(START_VOICE_RECOGNITION_TIMEOUT);
1626 /* package */ synchronized boolean startVoiceRecognition() {
1627 if (mWaitingForVoiceRecognition) {
1629 mWaitingForVoiceRecognition = false;
1633 sendURC("+BVRA: 1");
1635 boolean ret = audioOn();
1636 if (mStartVoiceRecognitionWakeLock.isHeld()) {
1637 mStartVoiceRecognitionWakeLock.release();
1642 /* package */ synchronized boolean stopVoiceRecognition() {
1643 sendURC("+BVRA: 0");
1648 private boolean inDebug() {
1649 return DBG && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE, false);
1652 private boolean allowAudioAnytime() {
1653 return inDebug() && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE_AUDIO_ANYTIME,
1657 private void startDebug() {
1658 if (DBG && mDebugThread == null) {
1659 mDebugThread = new DebugThread();
1660 mDebugThread.start();
1664 private void stopDebug() {
1665 if (mDebugThread != null) {
1666 mDebugThread.interrupt();
1667 mDebugThread = null;
1671 /** Debug thread to read debug properties - runs when debug.bt.hfp is true
1672 * at the time a bluetooth handsfree device is connected. Debug properties
1673 * are polled and mock updates sent every 1 second */
1674 private class DebugThread extends Thread {
1675 /** Turns on/off handsfree profile debugging mode */
1676 private static final String DEBUG_HANDSFREE = "debug.bt.hfp";
1678 /** Mock battery level change - use 0 to 5 */
1679 private static final String DEBUG_HANDSFREE_BATTERY = "debug.bt.hfp.battery";
1681 /** Mock no cellular service when false */
1682 private static final String DEBUG_HANDSFREE_SERVICE = "debug.bt.hfp.service";
1684 /** Mock cellular roaming when true */
1685 private static final String DEBUG_HANDSFREE_ROAM = "debug.bt.hfp.roam";
1687 /** false to true transition will force an audio (SCO) connection to
1688 * be established. true to false will force audio to be disconnected
1690 private static final String DEBUG_HANDSFREE_AUDIO = "debug.bt.hfp.audio";
1692 /** true allows incoming SCO connection out of call.
1694 private static final String DEBUG_HANDSFREE_AUDIO_ANYTIME = "debug.bt.hfp.audio_anytime";
1696 /** Mock signal strength change in ASU - use 0 to 31 */
1697 private static final String DEBUG_HANDSFREE_SIGNAL = "debug.bt.hfp.signal";
1699 /** Debug AT+CLCC: print +CLCC result */
1700 private static final String DEBUG_HANDSFREE_CLCC = "debug.bt.hfp.clcc";
1702 /** Debug AT+BSIR - Send In Band Ringtones Unsolicited AT command.
1703 * debug.bt.unsol.inband = 0 => AT+BSIR = 0 sent by the AG
1704 * debug.bt.unsol.inband = 1 => AT+BSIR = 0 sent by the AG
1705 * Other values are ignored.
1708 private static final String DEBUG_UNSOL_INBAND_RINGTONE =
1709 "debug.bt.unsol.inband";
1713 boolean oldService = true;
1714 boolean oldRoam = false;
1715 boolean oldAudio = false;
1717 while (!isInterrupted() && inDebug()) {
1718 int batteryLevel = SystemProperties.getInt(DEBUG_HANDSFREE_BATTERY, -1);
1719 if (batteryLevel >= 0 && batteryLevel <= 5) {
1720 Intent intent = new Intent();
1721 intent.putExtra("level", batteryLevel);
1722 intent.putExtra("scale", 5);
1723 mPhoneState.updateBatteryState(intent);
1726 boolean serviceStateChanged = false;
1727 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_SERVICE, true) != oldService) {
1728 oldService = !oldService;
1729 serviceStateChanged = true;
1731 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_ROAM, false) != oldRoam) {
1733 serviceStateChanged = true;
1735 if (serviceStateChanged) {
1736 Bundle b = new Bundle();
1737 b.putInt("state", oldService ? 0 : 1);
1738 b.putBoolean("roaming", oldRoam);
1739 mPhoneState.updateServiceState(true, ServiceState.newFromBundle(b));
1742 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_AUDIO, false) != oldAudio) {
1743 oldAudio = !oldAudio;
1751 int signalLevel = SystemProperties.getInt(DEBUG_HANDSFREE_SIGNAL, -1);
1752 if (signalLevel >= 0 && signalLevel <= 31) {
1753 Intent intent = new Intent();
1754 intent.putExtra("asu", signalLevel);
1755 mPhoneState.updateSignalState(intent);
1758 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_CLCC, false)) {
1759 log(getClccResult().toString());
1762 sleep(1000); // 1 second
1763 } catch (InterruptedException e) {
1768 SystemProperties.getInt(DEBUG_UNSOL_INBAND_RINGTONE, -1);
1769 if (inBandRing == 0 || inBandRing == 1) {
1770 AtCommandResult result =
1771 new AtCommandResult(AtCommandResult.UNSOLICITED);
1772 result.addResponse("+BSIR: " + inBandRing);
1773 sendURC(result.toString());
1779 private static void log(String msg) {