blob: fbb416d5cbefdcd96818398b0a3ebf2ce113ce85 [file] [log] [blame]
The Android Open Source Projectabc47112008-10-21 07:00:00 -07001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.phone;
18
19import android.bluetooth.AtCommandHandler;
20import android.bluetooth.AtCommandResult;
21import android.bluetooth.AtParser;
22import android.bluetooth.BluetoothDevice;
23import android.bluetooth.HeadsetBase;
24import android.bluetooth.ScoSocket;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.database.Cursor;
30import android.media.AudioManager;
31import android.net.Uri;
32import android.os.AsyncResult;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.Message;
36import android.os.PowerManager;
37import android.os.PowerManager.WakeLock;
38import android.os.SystemProperties;
39import android.provider.CallLog.Calls;
40import android.provider.Contacts.Phones;
41import android.provider.Contacts.PhonesColumns;
42import com.android.internal.telephony.Call;
43import com.android.internal.telephony.Connection;
44import com.android.internal.telephony.Phone;
45import com.android.internal.telephony.TelephonyIntents;
46import android.telephony.PhoneNumberUtils;
47import android.telephony.ServiceState;
48import android.text.TextUtils;
49import android.util.Log;
50
51import java.util.LinkedList;
52import java.util.HashMap;
53
54/**
55 * Bluetooth headset manager for the Phone app.
56 * @hide
57 */
58public class BluetoothHandsfree {
59 private static final String TAG = "BT HS/HF";
60 private static final boolean DBG = false;
61 private static final boolean VDBG = false; // even more logging
62
63 public static final int TYPE_UNKNOWN = 0;
64 public static final int TYPE_HEADSET = 1;
65 public static final int TYPE_HANDSFREE = 2;
66
67 private Context mContext;
68 private Phone mPhone;
69 private ServiceState mServiceState;
70 private HeadsetBase mHeadset; // null when not connected
71 private int mHeadsetType;
72 private boolean mAudioPossible;
73 private ScoSocket mIncomingSco;
74 private ScoSocket mOutgoingSco;
75 private ScoSocket mConnectedSco;
76
77 private Call mForegroundCall;
78 private Call mBackgroundCall;
79 private Call mRingingCall;
80
81 private AudioManager mAudioManager;
82 private PowerManager mPowerManager;
83
84 private boolean mUserWantsAudio;
85 private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call
86
87 // AT command state
88 private static final int MAX_CONNECTIONS = 3; // Max connections for clcc indexing
89 // TODO: Verify
90
91 private boolean mClip = false; // Calling Line Information Presentation
92 private boolean mIndicatorsEnabled = false;
93 private boolean mCmee = false; // Extended Error reporting
94 private String mSelectedPB; // currently-selected phone book (includes quotes)
95 private BluetoothPhoneState mPhoneState; // for CIND and CIEV updates
96 private long[] mClccTimestamps; // Timestamps associated with each clcc index
97 private boolean[] mClccUsed; // Is this clcc index in use
98 private boolean mWaitingForCallStart;
99
100 // Audio parameters
101 private static final String HEADSET_NREC = "bt_headset_nrec";
102 private static final String HEADSET_NAME = "bt_headset_name";
103
104 private HashMap<String, PhoneBookEntry> mPhoneBooks = new HashMap<String, PhoneBookEntry>(5);
105
106 private class PhoneBookEntry {
107 public Cursor mPhonebookCursor; // result set of last phone-book query
108 public int mCursorNumberColumn;
109 public int mCursorNumberTypeColumn;
110 public int mCursorNameColumn;
111 };
112
113 public static String typeToString(int type) {
114 switch (type) {
115 case TYPE_UNKNOWN:
116 return "unknown";
117 case TYPE_HEADSET:
118 return "headset";
119 case TYPE_HANDSFREE:
120 return "handsfree";
121 }
122 return null;
123 }
124
125 /** The projection to use when querying the call log database in response
126 to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
127 dialed calls respectively)
128 */
129 private static final String[] CALLS_PROJECTION = new String[] {
130 Calls._ID, Calls.NUMBER
131 };
132
133 /** The projection to use when querying the contacts database in response
134 to AT+CPBR for the ME phonebook (saved phone numbers).
135 */
136 private static final String[] PHONES_PROJECTION = new String[] {
137 Phones._ID, Phones.NAME,
138 Phones.NUMBER, Phones.TYPE
139 };
140
141 /** The projection to use when querying the contacts database in response
142 to AT+CNUM for the ME phonebook (saved phone numbers). We need only
143 the phone numbers here and the phone type.
144 */
145 private static final String[] PHONES_LITE_PROJECTION = new String[] {
146 Phones._ID, Phones.NUMBER, Phones.TYPE
147 };
148
149 /** Android supports as many phonebook entries as the flash can hold, but
150 * BT periphals don't. Limit the number we'll report. */
151 private static final int MAX_PHONEBOOK_SIZE = 16384;
152
153 private static final String OUTGOING_CALL_WHERE =
154 Calls.TYPE + "=" + Calls.OUTGOING_TYPE;
155 private static final String INCOMING_CALL_WHERE =
156 Calls.TYPE + "=" + Calls.INCOMING_TYPE;
157 private static final String MISSED_CALL_WHERE =
158 Calls.TYPE + "=" + Calls.MISSED_TYPE;
159
160 public BluetoothHandsfree(Context context, Phone phone) {
161 mPhone = phone;
162 mContext = context;
163 BluetoothDevice bluetooth =
164 (BluetoothDevice)context.getSystemService(Context.BLUETOOTH_SERVICE);
165 boolean bluetoothCapable = (bluetooth != null);
166 mHeadset = null; // nothing connected yet
167
168 mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
169 mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
170 this.toString());
171 mStartCallWakeLock.setReferenceCounted(false);
172
173 if (bluetoothCapable) {
174 resetAtState();
175 }
176
177 mRingingCall = mPhone.getRingingCall();
178 mForegroundCall = mPhone.getForegroundCall();
179 mBackgroundCall = mPhone.getBackgroundCall();
180 mPhoneState = new BluetoothPhoneState();
181 mUserWantsAudio = true;
182
183 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
184 }
185
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.
192 */
193 if (mIncomingSco == null) {
194 mIncomingSco = createScoSocket();
195 mIncomingSco.accept();
196 }
197 }
198
199 /* package */ synchronized void onBluetoothDisabled() {
200 if (mConnectedSco != null) {
201 mAudioManager.setBluetoothScoOn(false);
202 mConnectedSco.close();
203 mConnectedSco = null;
204 }
205 if (mOutgoingSco != null) {
206 mOutgoingSco.close();
207 mOutgoingSco = null;
208 }
209 if (mIncomingSco != null) {
210 mIncomingSco.close();
211 mIncomingSco = null;
212 }
213 }
214
215 private boolean isHeadsetConnected() {
216 if (mHeadset == null) {
217 return false;
218 }
219 return mHeadset.isConnected();
220 }
221
222 /* package */ void connectHeadset(HeadsetBase headset, int headsetType) {
223 mHeadset = headset;
224 mHeadsetType = headsetType;
225 if (mHeadsetType == TYPE_HEADSET) {
226 initializeHeadsetAtParser();
227 } else {
228 initializeHandsfreeAtParser();
229 }
230 headset.startEventThread();
231 configAudioParameters();
232
233 if (inDebug()) {
234 startDebug();
235 }
236
237 if (isIncallAudio()) {
238 audioOn();
239 }
240 }
241
242 /* returns true if there is some kind of in-call audio we may wish to route
243 * bluetooth to */
244 private boolean isIncallAudio() {
245 Call.State state = mForegroundCall.getState();
246
247 return (state == Call.State.ACTIVE || state == Call.State.ALERTING);
248 }
249
250 /* package */ void disconnectHeadset() {
251 mHeadset = null;
252 stopDebug();
253 resetAtState();
254 }
255
256 private void resetAtState() {
257 mClip = false;
258 mIndicatorsEnabled = false;
259 mCmee = false;
260 mSelectedPB = null;
261 mPhoneBooks.clear();
262 // DC -- dialled calls
263 // RC -- received calls
264 // MC -- missed (or unanswered) calls
265 // ME -- MT phonebook
266 mPhoneBooks.put("\"DC\"", new PhoneBookEntry());
267 mPhoneBooks.put("\"RC\"", new PhoneBookEntry());
268 mPhoneBooks.put("\"MC\"", new PhoneBookEntry());
269 mPhoneBooks.put("\"ME\"", new PhoneBookEntry());
270 mClccTimestamps = new long[MAX_CONNECTIONS];
271 mClccUsed = new boolean[MAX_CONNECTIONS];
272 for (int i = 0; i < MAX_CONNECTIONS; i++) {
273 mClccUsed[i] = false;
274 }
275
276 }
277
278 private void configAudioParameters() {
279 String name = mHeadset.getName();
280 if (name == null) {
281 name = "<unknown>";
282 }
283 mAudioManager.setParameter(HEADSET_NAME, name);
284 mAudioManager.setParameter(HEADSET_NREC, "on");
285 }
286
287
288 /** Represents the data that we send in a +CIND or +CIEV command to the HF
289 */
290 private class BluetoothPhoneState {
291 // 0: no service
292 // 1: service
293 private int mService;
294
295 // 0: no active call
296 // 1: active call (where active means audio is routed - not held call)
297 private int mCall;
298
299 // 0: not in call setup
300 // 1: incoming call setup
301 // 2: outgoing call setup
302 // 3: remote party being alerted in an outgoing call setup
303 private int mCallsetup;
304
305 // 0: no calls held
306 // 1: held call and active call
307 // 2: held call only
308 private int mCallheld;
309
310 // cellular signal strength of AG: 0-5
311 private int mSignal;
312
313 // cellular signal strength in CSQ rssi scale
314 private int mRssi; // for CSQ
315
316 // 0: roaming not active (home)
317 // 1: roaming active
318 private int mRoam;
319
320 // battery charge of AG: 0-5
321 private int mBattchg;
322
323 // 0: not registered
324 // 1: registered, home network
325 // 5: registered, roaming
326 private int mStat; // for CREG
327
328 private String mRingingNumber; // Context for in-progress RING's
329 private int mRingingType;
330
331 private boolean mIgnoreRing = false;
332
333 private static final int SERVICE_STATE_CHANGED = 1;
334 private static final int PHONE_STATE_CHANGED = 2;
335 private static final int RING = 3;
336
337 private Handler mStateChangeHandler = new Handler() {
338 @Override
339 public void handleMessage(Message msg) {
340 switch(msg.what) {
341 case RING:
342 AtCommandResult result = ring();
343 if (result != null) {
344 sendURC(result.toString());
345 }
346 break;
347 case SERVICE_STATE_CHANGED:
348 ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
349 updateServiceState(sendUpdate(), state);
350 break;
351 case PHONE_STATE_CHANGED:
352 Connection connection = null;
353 if (((AsyncResult) msg.obj).result instanceof Connection) {
354 connection = (Connection) ((AsyncResult) msg.obj).result;
355 }
356 updatePhoneState(sendUpdate(), connection);
357 break;
358 }
359 }
360 };
361
362 private BluetoothPhoneState() {
363 // init members
364 updateServiceState(false, mPhone.getServiceState());
365 updatePhoneState(false, null);
366 mBattchg = 5; // There is currently no API to get battery level
367 // on demand, so set to 5 and wait for an update
368 mSignal = asuToSignal(mPhone.getSignalStrengthASU());
369
370 // register for updates
371 mPhone.registerForServiceStateChanged(mStateChangeHandler,
372 SERVICE_STATE_CHANGED, null);
373 mPhone.registerForPhoneStateChanged(mStateChangeHandler,
374 PHONE_STATE_CHANGED, null);
375 IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
376 filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED);
377 mContext.registerReceiver(mStateReceiver, filter);
378 }
379
380 private boolean sendUpdate() {
381 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mIndicatorsEnabled;
382 }
383
384 private boolean sendClipUpdate() {
385 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mClip;
386 }
387
388 /* convert [0,31] ASU signal strength to the [0,5] expected by
389 * bluetooth devices. Scale is similar to status bar policy
390 */
391 private int asuToSignal(int asu) {
392 if (asu >= 16) return 5;
393 else if (asu >= 8) return 4;
394 else if (asu >= 4) return 3;
395 else if (asu >= 2) return 2;
396 else if (asu >= 1) return 1;
397 else return 0;
398 }
399
400 /* convert [0,5] signal strength to a rssi signal strength for CSQ
401 * which is [0,31]. Despite the same scale, this is not the same value
402 * as ASU.
403 */
404 private int signalToRssi(int signal) {
405 // using C4A suggested values
406 switch (signal) {
407 case 0: return 0;
408 case 1: return 4;
409 case 2: return 8;
410 case 3: return 13;
411 case 4: return 19;
412 case 5: return 31;
413 }
414 return 0;
415 }
416
417
418 private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() {
419 @Override
420 public void onReceive(Context context, Intent intent) {
421 if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
422 updateBatteryState(intent);
423 } else if (intent.getAction().equals(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED)) {
424 updateSignalState(intent);
425 }
426 }
427 };
428
429 private synchronized void updateBatteryState(Intent intent) {
430 int batteryLevel = intent.getIntExtra("level", -1);
431 int scale = intent.getIntExtra("scale", -1);
432 if (batteryLevel == -1 || scale == -1) {
433 return; // ignore
434 }
435 batteryLevel = batteryLevel * 5 / scale;
436 if (mBattchg != batteryLevel) {
437 mBattchg = batteryLevel;
438 if (sendUpdate()) {
439 sendURC("+CIEV: 7," + mBattchg);
440 }
441 }
442 }
443
444 private synchronized void updateSignalState(Intent intent) {
445 int signal;
446 signal = asuToSignal(intent.getIntExtra("asu", -1));
447 mRssi = signalToRssi(signal); // no unsolicited CSQ
448 if (signal != mSignal) {
449 mSignal = signal;
450 if (sendUpdate()) {
451 sendURC("+CIEV: 5," + mSignal);
452 }
453 }
454 }
455
456 private synchronized void updateServiceState(boolean sendUpdate, ServiceState state) {
457 int service = state.getState() == ServiceState.STATE_IN_SERVICE ? 1 : 0;
458 int roam = state.getRoaming() ? 1 : 0;
459 int stat;
460 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
461
462 if (service == 0) {
463 stat = 0;
464 } else {
465 stat = (roam == 1) ? 5 : 1;
466 }
467
468 if (service != mService) {
469 mService = service;
470 if (sendUpdate) {
471 result.addResponse("+CIEV: 1," + mService);
472 }
473 }
474 if (roam != mRoam) {
475 mRoam = roam;
476 if (sendUpdate) {
477 result.addResponse("+CIEV: 6," + mRoam);
478 }
479 }
480 if (stat != mStat) {
481 mStat = stat;
482 if (sendUpdate) {
483 result.addResponse(toCregString());
484 }
485 }
486
487 sendURC(result.toString());
488 }
489
490 private synchronized void updatePhoneState(boolean sendUpdate, Connection connection) {
491 int call = 0;
492 int callsetup = 0;
493 int callheld = 0;
494 int prevCallsetup = mCallsetup;
495 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
496
497 if (DBG) log("updatePhoneState()");
498
499 switch (mPhone.getState()) {
500 case IDLE:
501 mUserWantsAudio = true; // out of call - reset state
502 audioOff();
503 break;
504 default:
505 callStarted();
506 }
507
508 switch(mForegroundCall.getState()) {
509 case ACTIVE:
510 call = 1;
511 mAudioPossible = true;
512 break;
513 case DIALING:
514 callsetup = 2;
515 mAudioPossible = false;
516 break;
517 case ALERTING:
518 callsetup = 3;
519 mAudioPossible = true;
520 break;
521 default:
522 mAudioPossible = false;
523 }
524
525 switch(mRingingCall.getState()) {
526 case INCOMING:
527 case WAITING:
528 callsetup = 1;
529 break;
530 }
531
532 switch(mBackgroundCall.getState()) {
533 case HOLDING:
534 if (call == 1) {
535 callheld = 1;
536 } else {
537 call = 1;
538 callheld = 2;
539 }
540 break;
541 }
542
543 if (mCall != call) {
544 mCall = call;
545 if (sendUpdate) {
546 result.addResponse("+CIEV: 2," + mCall);
547 }
548 }
549 if (mCallsetup != callsetup) {
550 mCallsetup = callsetup;
551 if (sendUpdate) {
552 result.addResponse("+CIEV: 3," + mCallsetup);
553 }
554 }
555 if (mCallheld != callheld) {
556 mCallheld = callheld;
557 if (sendUpdate) {
558 result.addResponse("+CIEV: 4," + mCallheld);
559 }
560 }
561
562 if (callsetup == 1 && callsetup != prevCallsetup) {
563 // new incoming call
564 String number = null;
565 int type = 128;
566 if (sendUpdate) {
567 // find incoming phone number and type
568 if (connection == null) {
569 connection = mRingingCall.getEarliestConnection();
570 if (connection == null) {
571 Log.e(TAG, "Could not get a handle on Connection object for new " +
572 "incoming call");
573 }
574 }
575 if (connection != null) {
576 number = connection.getAddress();
577 if (number != null) {
578 type = PhoneNumberUtils.toaFromString(number);
579 }
580 }
581 if (number == null) {
582 number = "";
583 }
584 }
585 if ((call != 0 || callheld != 0) && sendUpdate) {
586 // call waiting
587 result.addResponse("+CCWA: \"" + number + "\"," + type);
588 } else {
589 // regular new incoming call
590 mRingingNumber = number;
591 mRingingType = type;
592 mIgnoreRing = false;
593 result.addResult(ring());
594 }
595 }
596 sendURC(result.toString());
597 }
598
599 private AtCommandResult ring() {
600 if (!mIgnoreRing && mRingingCall.isRinging()) {
601 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
602 result.addResponse("RING");
603 if (sendClipUpdate()) {
604 result.addResponse("+CLIP: \"" + mRingingNumber + "\"," + mRingingType);
605 }
606
607 Message msg = mStateChangeHandler.obtainMessage(RING);
608 mStateChangeHandler.sendMessageDelayed(msg, 3000);
609 return result;
610 }
611 return null;
612 }
613
614 private synchronized String toCregString() {
615 return new String("+CREG: 1," + mStat);
616 }
617
618 private synchronized AtCommandResult toCindResult() {
619 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
620 String status = "+CIND: " + mService + "," + mCall + "," + mCallsetup + "," +
621 mCallheld + "," + mSignal + "," + mRoam + "," + mBattchg;
622 result.addResponse(status);
623 return result;
624 }
625
626 private synchronized AtCommandResult toCsqResult() {
627 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
628 String status = "+CSQ: " + mRssi + ",99";
629 result.addResponse(status);
630 return result;
631 }
632
633
634 private synchronized AtCommandResult getCindTestResult() {
635 return new AtCommandResult("+CIND: (\"service\",(0-1))," + "(\"call\",(0-1))," +
636 "(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5))," +
637 "(\"roam\",(0-1)),(\"battchg\",(0-5))");
638 }
639
640 private synchronized void ignoreRing() {
641 mCallsetup = 0;
642 mIgnoreRing = true;
643 if (sendUpdate()) {
644 sendURC("+CIEV: 3," + mCallsetup);
645 }
646 }
647
648 };
649
650 private static final int SCO_ACCEPTED = 1;
651 private static final int SCO_CONNECTED = 2;
652 private static final int SCO_CLOSED = 3;
653 private static final int CHECK_CALL_STARTED = 4;
654 private final Handler mHandler = new Handler() {
655 @Override
656 public synchronized void handleMessage(Message msg) {
657 switch (msg.what) {
658 case SCO_ACCEPTED:
659 if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
660 if (isHeadsetConnected() && mAudioPossible && mConnectedSco == null) {
661 Log.i(TAG, "Routing audio for incoming SCO connection");
662 mConnectedSco = (ScoSocket)msg.obj;
663 mAudioManager.setBluetoothScoOn(true);
664 } else {
665 Log.i(TAG, "Rejecting incoming SCO connection");
666 ((ScoSocket)msg.obj).close();
667 }
668 } // else error trying to accept, try again
669 mIncomingSco = createScoSocket();
670 mIncomingSco.accept();
671 break;
672 case SCO_CONNECTED:
673 if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected() &&
674 mConnectedSco == null) {
675 if (DBG) log("Routing audio for outgoing SCO conection");
676 mConnectedSco = (ScoSocket)msg.obj;
677 mAudioManager.setBluetoothScoOn(true);
678 } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
679 if (DBG) log("Rejecting new connected outgoing SCO socket");
680 ((ScoSocket)msg.obj).close();
681 mOutgoingSco.close();
682 }
683 mOutgoingSco = null;
684 break;
685 case SCO_CLOSED:
686 if (mConnectedSco == (ScoSocket)msg.obj) {
687 mConnectedSco = null;
688 mAudioManager.setBluetoothScoOn(false);
689 } else if (mOutgoingSco == (ScoSocket)msg.obj) {
690 mOutgoingSco = null;
691 } else if (mIncomingSco == (ScoSocket)msg.obj) {
692 mIncomingSco = null;
693 }
694 break;
695 case CHECK_CALL_STARTED:
696 if (mWaitingForCallStart) {
697 mWaitingForCallStart = false;
698 Log.e(TAG, "Timeout waiting for call to start");
699 sendURC("ERROR");
700 if (mStartCallWakeLock.isHeld()) {
701 mStartCallWakeLock.release();
702 }
703 }
704 break;
705 }
706 }
707 };
708
709 private ScoSocket createScoSocket() {
710 return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED);
711 }
712
713 /** Request to establish SCO (audio) connection to bluetooth
714 * headset/handsfree, if one is connected. Does not block.
715 * Returns false if the user has requested audio off, or if there
716 * is some other immediate problem that will prevent BT audio.
717 */
718 /* package */ synchronized boolean audioOn() {
719 if (VDBG) log("audioOn()");
720 if (!isHeadsetConnected()) {
721 if (DBG) log("audioOn(): headset is not connected!");
722 return false;
723 }
724
725 if (mConnectedSco != null) {
726 if (DBG) log("audioOn(): audio is already connected");
727 return true;
728 }
729
730 if (!mUserWantsAudio) {
731 if (DBG) log("audioOn(): user requested no audio, ignoring");
732 return false;
733 }
734
735 if (mOutgoingSco != null) {
736 if (DBG) log("audioOn(): outgoing SCO already in progress");
737 return true;
738 }
739 mOutgoingSco = createScoSocket();
740 if (!mOutgoingSco.connect(mHeadset.getAddress())) {
741 mOutgoingSco = null;
742 }
743
744 return true;
745 }
746
747 /** Used to indicate the user requested BT audio on.
748 * This will establish SCO (BT audio), even if the user requested it off
749 * previously on this call.
750 */
751 /* package */ synchronized void userWantsAudioOn() {
752 mUserWantsAudio = true;
753 audioOn();
754 }
755 /** Used to indicate the user requested BT audio off.
756 * This will prevent us from establishing BT audio again during this call
757 * if audioOn() is called.
758 */
759 /* package */ synchronized void userWantsAudioOff() {
760 mUserWantsAudio = false;
761 audioOff();
762 }
763
764 /** Request to disconnect SCO (audio) connection to bluetooth
765 * headset/handsfree, if one is connected. Does not block.
766 */
767 /* package */ synchronized void audioOff() {
768 if (VDBG) log("audioOff()");
769
770 if (mConnectedSco != null) {
771 mAudioManager.setBluetoothScoOn(false);
772 mConnectedSco.close();
773 mConnectedSco = null;
774 }
775 if (mOutgoingSco != null) {
776 mOutgoingSco.close();
777 mOutgoingSco = null;
778 }
779 }
780
781 /* package */ boolean isAudioOn() {
782 return (mConnectedSco != null);
783 }
784
785 /* package */ void ignoreRing() {
786 mPhoneState.ignoreRing();
787 }
788
789 /* List of AT error codes specified by the Handsfree profile. */
790
791 private static final int CME_ERROR_AG_FAILURE = 0;
792 private static final int CME_ERROR_NO_CONNECTION_TO_PHONE = 1;
793 // private static final int CME_ERROR_ = 2;
794 private static final int CME_ERROR_OPERATION_NOT_ALLOWED = 3;
795 private static final int CME_ERROR_OPERATION_NOT_SUPPORTED = 4;
796 private static final int CME_ERROR_PIN_REQUIRED = 5;
797 // private static final int CME_ERROR_ = 6;
798 // private static final int CME_ERROR_ = 7;
799 // private static final int CME_ERROR_ = 8;
800 // private static final int CME_ERROR_ = 9;
801 private static final int CME_ERROR_SIM_MISSING = 10;
802 private static final int CME_ERROR_SIM_PIN_REQUIRED = 11;
803 private static final int CME_ERROR_SIM_PUK_REQUIRED = 12;
804 private static final int CME_ERROR_SIM_FAILURE = 13;
805 private static final int CME_ERROR_SIM_BUSY = 14;
806 // private static final int CME_ERROR_ = 15;
807 private static final int CME_ERROR_WRONG_PASSWORD = 16;
808 private static final int CME_ERROR_SIM_PIN2_REQUIRED = 17;
809 private static final int CME_ERROR_SIM_PUK2_REQUIRED = 18;
810 // private static final int CME_ERROR_ = 19;
811 private static final int CME_ERROR_MEMORY_FULL = 20;
812 private static final int CME_ERROR_INVALID_INDEX = 21;
813 // private static final int CME_ERROR_ = 22;
814 private static final int CME_ERROR_MEMORY_FAILURE = 23;
815 private static final int CME_ERROR_TEXT_TOO_LONG = 24;
816 private static final int CME_ERROR_TEXT_HAS_INVALID_CHARS = 25;
817 private static final int CME_ERROR_DIAL_STRING_TOO_LONG = 26;
818 private static final int CME_ERROR_DIAL_STRING_HAS_INVALID_CHARS = 27;
819 // private static final int CME_ERROR_ = 28;
820 // private static final int CME_ERROR_ = 29;
821 private static final int CME_ERROR_NO_SERVICE = 30;
822 // private static final int CME_ERROR_ = 31;
823 private static final int CME_ERROR_911_ONLY_ALLOWED = 32;
824
825 public AtCommandResult reportAtError(int error) {
826 if (mCmee) {
827 AtCommandResult result =
828 new AtCommandResult(AtCommandResult.UNSOLICITED);
829 result.addResponse("+CME ERROR: " + error);
830 return result;
831 } else {
832 return new AtCommandResult(AtCommandResult.ERROR);
833 }
834 }
835
836 private static String getPhoneType(int type) {
837 switch (type) {
838 case PhonesColumns.TYPE_HOME:
839 return "H";
840 case PhonesColumns.TYPE_MOBILE:
841 return "M";
842 case PhonesColumns.TYPE_WORK:
843 return "W";
844 case PhonesColumns.TYPE_FAX_HOME:
845 case PhonesColumns.TYPE_FAX_WORK:
846 return "F";
847 case PhonesColumns.TYPE_OTHER:
848 case PhonesColumns.TYPE_CUSTOM:
849 default:
850 return "O";
851 }
852 }
853
854 private boolean initPhoneBookEntry(String pb, PhoneBookEntry pbe) {
855 String where;
856 boolean ancillaryPhonebook = true;
857
858 if (pb.equals("\"ME\"")) {
859 ancillaryPhonebook = false;
860 where = null;
861 } else if (pb.equals("\"DC\"")) {
862 where = OUTGOING_CALL_WHERE;
863 } else if (pb.equals("\"RC\"")) {
864 where = INCOMING_CALL_WHERE;
865 } else if (pb.equals("\"MC\"")) {
866 where = MISSED_CALL_WHERE;
867 } else {
868 return false;
869 }
870
871 if (pbe.mPhonebookCursor == null) {
872 if (ancillaryPhonebook) {
873// ContentValues values = new ContentValues();
874// values.put(Calls.NEW, "0");
875// mContext.getContentResolver().update(Calls.CONTENT_URI, values, where, null);
876 }
877
878 if (ancillaryPhonebook) {
879 pbe.mPhonebookCursor = mContext.getContentResolver().query(
880 Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
881 Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
882 pbe.mCursorNumberColumn = pbe.mPhonebookCursor.getColumnIndexOrThrow(Calls.NUMBER);
883 pbe.mCursorNumberTypeColumn = -1;
884 pbe.mCursorNameColumn = -1;
885 } else {
886 pbe.mPhonebookCursor = mContext.getContentResolver().query(
887 Phones.CONTENT_URI, PHONES_PROJECTION, where, null,
888 Phones.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
889 pbe.mCursorNumberColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.NUMBER);
890 pbe.mCursorNumberTypeColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.TYPE);
891 pbe.mCursorNameColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.NAME);
892 }
893 }
894
895 return true;
896 }
897
898 private void sendURC(String urc) {
899 if (isHeadsetConnected()) {
900 mHeadset.sendURC(urc);
901 }
902 }
903
904 /** helper to redial last dialled number */
905 private AtCommandResult redial() {
906 // Get the last dialled number from the phone book
907 String[] projection = {Calls.NUMBER};
908 Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection,
909 Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER +
910 " LIMIT 1");
911 if (cursor.getCount() < 1) {
912 // spec seems to suggest sending ERROR if we dont have a
913 // number to redial
914 if (DBG) log("Bluetooth redial requested (+BLDN), but no previous " +
915 "outgoing calls found. Ignoring");
916 cursor.close();
917 return new AtCommandResult(AtCommandResult.ERROR);
918 }
919
920 cursor.moveToNext();
921 int column = cursor.getColumnIndexOrThrow(Calls.NUMBER);
922 String number = cursor.getString(column);
923 cursor.close();
924
925 // Call it
926 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
927 Uri.fromParts("tel", number, null));
928 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
929 mContext.startActivity(intent);
930
931 // We do not immediately respond OK, wait until we get a phone state
932 // update. If we return OK now and the handsfree immeidately requests
933 // our phone state it will say we are not in call yet which confuses
934 // some devices
935 waitForCallStart();
936 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
937 }
938
939 /** Build the +CLCC result
940 * The complexity arises from the fact that we need to maintain the same
941 * CLCC index even as a call moves between states. */
942 private synchronized AtCommandResult getClccResult() {
943 // Collect all known connections
944 Connection[] clccConnections = new Connection[MAX_CONNECTIONS]; // indexed by CLCC index
945 LinkedList<Connection> newConnections = new LinkedList<Connection>();
946 LinkedList<Connection> connections = new LinkedList<Connection>();
947 if (mRingingCall.getState().isAlive()) {
948 connections.addAll(mRingingCall.getConnections());
949 }
950 if (mForegroundCall.getState().isAlive()) {
951 connections.addAll(mForegroundCall.getConnections());
952 }
953 if (mBackgroundCall.getState().isAlive()) {
954 connections.addAll(mBackgroundCall.getConnections());
955 }
956
957 // Mark connections that we already known about
958 boolean clccUsed[] = new boolean[MAX_CONNECTIONS];
959 for (int i = 0; i < MAX_CONNECTIONS; i++) {
960 clccUsed[i] = mClccUsed[i];
961 mClccUsed[i] = false;
962 }
963 for (Connection c : connections) {
964 boolean found = false;
965 long timestamp = c.getCreateTime();
966 for (int i = 0; i < MAX_CONNECTIONS; i++) {
967 if (clccUsed[i] && timestamp == mClccTimestamps[i]) {
968 mClccUsed[i] = true;
969 found = true;
970 clccConnections[i] = c;
971 break;
972 }
973 }
974 if (!found) {
975 newConnections.add(c);
976 }
977 }
978
979 // Find a CLCC index for new connections
980 while (!newConnections.isEmpty()) {
981 // Find lowest empty index
982 int i = 0;
983 while (mClccUsed[i]) i++;
984 // Find earliest connection
985 long earliestTimestamp = newConnections.get(0).getCreateTime();
986 Connection earliestConnection = newConnections.get(0);
987 for (int j = 0; j < newConnections.size(); j++) {
988 long timestamp = newConnections.get(j).getCreateTime();
989 if (timestamp < earliestTimestamp) {
990 earliestTimestamp = timestamp;
991 earliestConnection = newConnections.get(j);
992 }
993 }
994
995 // update
996 mClccUsed[i] = true;
997 mClccTimestamps[i] = earliestTimestamp;
998 clccConnections[i] = earliestConnection;
999 newConnections.remove(earliestConnection);
1000 }
1001
1002 // Build CLCC
1003 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
1004 for (int i = 0; i < clccConnections.length; i++) {
1005 if (mClccUsed[i]) {
1006 String clccEntry = connectionToClccEntry(i, clccConnections[i]);
1007 if (clccEntry != null) {
1008 result.addResponse(clccEntry);
1009 }
1010 }
1011 }
1012
1013 return result;
1014 }
1015
1016 /** Convert a Connection object into a single +CLCC result */
1017 private String connectionToClccEntry(int index, Connection c) {
1018 int state;
1019 switch (c.getState()) {
1020 case ACTIVE:
1021 state = 0;
1022 break;
1023 case HOLDING:
1024 state = 1;
1025 break;
1026 case DIALING:
1027 state = 2;
1028 break;
1029 case ALERTING:
1030 state = 3;
1031 break;
1032 case INCOMING:
1033 state = 4;
1034 break;
1035 case WAITING:
1036 state = 5;
1037 break;
1038 default:
1039 return null; // bad state
1040 }
1041
1042 int mpty = 0;
1043 Call call = c.getCall();
1044 if (call != null) {
1045 mpty = call.isMultiparty() ? 1 : 0;
1046 }
1047
1048 int direction = c.isIncoming() ? 1 : 0;
1049
1050 String number = c.getAddress();
1051 int type = -1;
1052 if (number != null) {
1053 type = PhoneNumberUtils.toaFromString(number);
1054 }
1055
1056 String result = "+CLCC: " + (index + 1) + "," + direction + "," + state + ",0," + mpty;
1057 if (number != null) {
1058 result += "," + number + "," + type;
1059 }
1060 return result;
1061 }
1062 /**
1063 * Register AT Command handlers to implement the Headset profile
1064 */
1065 private void initializeHeadsetAtParser() {
1066 if (DBG) log("Registering Headset AT commands");
1067 AtParser parser = mHeadset.getAtParser();
1068 // Headset's usually only have one button, which is meant to cause the
1069 // HS to send us AT+CKPD=200 or AT+CKPD.
1070 parser.register("+CKPD", new AtCommandHandler() {
1071 private AtCommandResult headsetButtonPress() {
1072 if (mRingingCall.isRinging()) {
1073 // Answer the call
1074 PhoneUtils.answerCall(mPhone);
1075 audioOn();
1076 } else if (mForegroundCall.getState().isAlive()) {
1077 if (!isAudioOn()) {
1078 // Transfer audio from AG to HS
1079 audioOn();
1080 } else {
1081 if (mHeadset.getDirection() == HeadsetBase.DIRECTION_INCOMING &&
1082 (System.currentTimeMillis() - mHeadset.getConnectTimestamp()) < 5000) {
1083 // Headset made a recent ACL connection to us - and
1084 // made a mandatory AT+CKPD request to connect
1085 // audio which races with our automatic audio
1086 // setup. ignore
1087 } else {
1088 // Hang up the call
1089 audioOff();
1090 PhoneUtils.hangup(mPhone);
1091 }
1092 }
1093 } else {
1094 // No current call - redial last number
1095 return redial();
1096 }
1097 return new AtCommandResult(AtCommandResult.OK);
1098 }
1099 @Override
1100 public AtCommandResult handleActionCommand() {
1101 return headsetButtonPress();
1102 }
1103 @Override
1104 public AtCommandResult handleSetCommand(Object[] args) {
1105 return headsetButtonPress();
1106 }
1107 });
1108 }
1109
1110 /**
1111 * Register AT Command handlers to implement the Handsfree profile
1112 */
1113 private void initializeHandsfreeAtParser() {
1114 if (DBG) log("Registering Handsfree AT commands");
1115 AtParser parser = mHeadset.getAtParser();
1116
1117 // Answer
1118 parser.register('A', new AtCommandHandler() {
1119 @Override
1120 public AtCommandResult handleBasicCommand(String args) {
1121 PhoneUtils.answerCall(mPhone);
1122 return new AtCommandResult(AtCommandResult.OK);
1123 }
1124 });
1125 parser.register('D', new AtCommandHandler() {
1126 @Override
1127 public AtCommandResult handleBasicCommand(String args) {
1128 if (args.length() > 0) {
1129 if (args.charAt(0) == '>') {
1130 // Yuck - memory dialling requested.
1131 // Just dial last number for now
1132 if (args.startsWith(">9999")) { // for PTS test
1133 return new AtCommandResult(AtCommandResult.ERROR);
1134 }
1135 return redial();
1136 } else {
1137 // Remove trailing ';'
1138 if (args.charAt(args.length() - 1) == ';') {
1139 args = args.substring(0, args.length() - 1);
1140 }
1141 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
1142 Uri.fromParts("tel", args, null));
1143 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1144 mContext.startActivity(intent);
1145
1146 waitForCallStart();
1147 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
1148 }
1149 }
1150 return new AtCommandResult(AtCommandResult.ERROR);
1151 }
1152 });
1153
1154 // Hang-up command
1155 parser.register("+CHUP", new AtCommandHandler() {
1156 @Override
1157 public AtCommandResult handleActionCommand() {
1158 audioOff();
1159
1160 PhoneUtils.hangup(mPhone);
1161
1162 return new AtCommandResult(AtCommandResult.OK);
1163 }
1164 });
1165
1166 // Bluetooth Retrieve Supported Features command
1167 parser.register("+BRSF", new AtCommandHandler() {
1168 private AtCommandResult sendBRSF() {
1169 // Bit 0: 3 way calling
1170 // 1: EC / NR
1171 // 2: CLI Presentation
1172 // 5: call reject
1173 // 6: Enhanced call status
1174 return new AtCommandResult("+BRSF: 99");
1175 }
1176 @Override
1177 public AtCommandResult handleSetCommand(Object[] args) {
1178 // AT+BRSF=<handsfree supported features bitmap>
1179 // Handsfree is telling us which features it supports. We
1180 // ignore its report for now. But we do respond with our own
1181 // feature bitmap.
1182 return sendBRSF();
1183 }
1184 @Override
1185 public AtCommandResult handleActionCommand() {
1186 // This seems to be out of spec, but lets do the nice thing
1187 return sendBRSF();
1188 }
1189 @Override
1190 public AtCommandResult handleReadCommand() {
1191 // This seems to be out of spec, but lets do the nice thing
1192 return sendBRSF();
1193 }
1194 });
1195
1196 // Call waiting notification on/off
1197 parser.register("+CCWA", new AtCommandHandler() {
1198 @Override
1199 public AtCommandResult handleActionCommand() {
1200 // Seems to be out of spec, but lets return nicely
1201 return new AtCommandResult(AtCommandResult.OK);
1202 }
1203 @Override
1204 public AtCommandResult handleReadCommand() {
1205 // Call waiting is always on
1206 return new AtCommandResult("+CCWA: 1");
1207 }
1208 @Override
1209 public AtCommandResult handleSetCommand(Object[] args) {
1210 // AT+CCWA=<n>
1211 // Handsfree is trying to enable/disable call waiting. We
1212 // cannot disable in the current implementation.
1213 return new AtCommandResult(AtCommandResult.OK);
1214 }
1215 @Override
1216 public AtCommandResult handleTestCommand() {
1217 // Request for range of supported CCWA paramters
1218 return new AtCommandResult("+CCWA: (\"n\",(1))");
1219 }
1220 });
1221
1222 // Mobile Equipment Event Reporting enable/disable command
1223 // Of the full 3GPP syntax paramters (mode, keyp, disp, ind, bfr) we
1224 // only support paramter ind (disable/enable evert reporting using
1225 // +CDEV)
1226 parser.register("+CMER", new AtCommandHandler() {
1227 @Override
1228 public AtCommandResult handleReadCommand() {
1229 return new AtCommandResult(
1230 "+CMER: 3,0,0," + (mIndicatorsEnabled ? "1" : "0"));
1231 }
1232 @Override
1233 public AtCommandResult handleSetCommand(Object[] args) {
1234 if (args.length < 4) {
1235 // This is a syntax error
1236 return new AtCommandResult(AtCommandResult.ERROR);
1237 } else if (args[0].equals(3) && args[1].equals(0) &&
1238 args[2].equals(0)) {
1239 if (args[3].equals(0)) {
1240 mIndicatorsEnabled = false;
1241 return new AtCommandResult(AtCommandResult.OK);
1242 } else if (args[3].equals(1)) {
1243 mIndicatorsEnabled = true;
1244 return new AtCommandResult(AtCommandResult.OK);
1245 }
1246 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1247 } else {
1248 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1249 }
1250 }
1251 @Override
1252 public AtCommandResult handleTestCommand() {
1253 return new AtCommandResult("+CMER: (3),(0),(0),(0-1)");
1254 }
1255 });
1256
1257 // Mobile Equipment Error Reporting enable/disable
1258 parser.register("+CMEE", new AtCommandHandler() {
1259 @Override
1260 public AtCommandResult handleActionCommand() {
1261 // out of spec, assume they want to enable
1262 mCmee = true;
1263 return new AtCommandResult(AtCommandResult.OK);
1264 }
1265 @Override
1266 public AtCommandResult handleReadCommand() {
1267 return new AtCommandResult("+CMEE: " + (mCmee ? "1" : "0"));
1268 }
1269 @Override
1270 public AtCommandResult handleSetCommand(Object[] args) {
1271 // AT+CMEE=<n>
1272 if (args.length == 0) {
1273 // <n> ommitted - default to 0
1274 mCmee = false;
1275 return new AtCommandResult(AtCommandResult.OK);
1276 } else if (!(args[0] instanceof Integer)) {
1277 // Syntax error
1278 return new AtCommandResult(AtCommandResult.ERROR);
1279 } else {
1280 mCmee = ((Integer)args[0] == 1);
1281 return new AtCommandResult(AtCommandResult.OK);
1282 }
1283 }
1284 @Override
1285 public AtCommandResult handleTestCommand() {
1286 // Probably not required but spec, but no harm done
1287 return new AtCommandResult("+CMEE: (0-1)");
1288 }
1289 });
1290
1291 // Bluetooth Last Dialled Number
1292 parser.register("+BLDN", new AtCommandHandler() {
1293 @Override
1294 public AtCommandResult handleActionCommand() {
1295 return redial();
1296 }
1297 });
1298
1299 // Indicator Update command
1300 parser.register("+CIND", new AtCommandHandler() {
1301 @Override
1302 public AtCommandResult handleReadCommand() {
1303 return mPhoneState.toCindResult();
1304 }
1305 @Override
1306 public AtCommandResult handleTestCommand() {
1307 return mPhoneState.getCindTestResult();
1308 }
1309 });
1310
1311 // Query Signal Quality (legacy)
1312 parser.register("+CSQ", new AtCommandHandler() {
1313 @Override
1314 public AtCommandResult handleActionCommand() {
1315 return mPhoneState.toCsqResult();
1316 }
1317 });
1318
1319 // Query network registration state
1320 parser.register("+CREG", new AtCommandHandler() {
1321 @Override
1322 public AtCommandResult handleReadCommand() {
1323 return new AtCommandResult(mPhoneState.toCregString());
1324 }
1325 });
1326
1327 // Send DTMF. I don't know if we are also expected to play the DTMF tone
1328 // locally, right now we don't
1329 parser.register("+VTS", new AtCommandHandler() {
1330 @Override
1331 public AtCommandResult handleSetCommand(Object[] args) {
1332 if (args.length >= 1) {
1333 char c;
1334 if (args[0] instanceof Integer) {
1335 c = ((Integer) args[0]).toString().charAt(0);
1336 } else {
1337 c = ((String) args[0]).charAt(0);
1338 }
1339 if (isValidDtmf(c)) {
1340 mPhone.sendDtmf(c);
1341 return new AtCommandResult(AtCommandResult.OK);
1342 }
1343 }
1344 return new AtCommandResult(AtCommandResult.ERROR);
1345 }
1346 private boolean isValidDtmf(char c) {
1347 switch (c) {
1348 case '#':
1349 case '*':
1350 return true;
1351 default:
1352 if (Character.digit(c, 14) != -1) {
1353 return true; // 0-9 and A-D
1354 }
1355 return false;
1356 }
1357 }
1358 });
1359
1360 // List calls
1361 parser.register("+CLCC", new AtCommandHandler() {
1362 @Override
1363 public AtCommandResult handleActionCommand() {
1364 return getClccResult();
1365 }
1366 });
1367
1368 // Call Hold and Multiparty Handling command
1369 parser.register("+CHLD", new AtCommandHandler() {
1370 @Override
1371 public AtCommandResult handleSetCommand(Object[] args) {
1372 if (args.length >= 1) {
1373 if (args[0].equals(0)) {
1374 boolean result;
1375 if (mRingingCall.isRinging()) {
1376 result = PhoneUtils.hangupRingingCall(mPhone);
1377 } else {
1378 result = PhoneUtils.hangupHoldingCall(mPhone);
1379 }
1380 if (result) {
1381 return new AtCommandResult(AtCommandResult.OK);
1382 } else {
1383 return new AtCommandResult(AtCommandResult.ERROR);
1384 }
1385 } else if (args[0].equals(1)) {
1386 // Hangup active call, answer held call
1387 if (PhoneUtils.answerAndEndActive(mPhone)) {
1388 return new AtCommandResult(AtCommandResult.OK);
1389 } else {
1390 return new AtCommandResult(AtCommandResult.ERROR);
1391 }
1392 } else if (args[0].equals(2)) {
1393 PhoneUtils.switchHoldingAndActive(mPhone);
1394 return new AtCommandResult(AtCommandResult.OK);
1395 } else if (args[0].equals(3)) {
1396 if (mForegroundCall.getState().isAlive() &&
1397 mBackgroundCall.getState().isAlive()) {
1398 PhoneUtils.mergeCalls(mPhone);
1399 }
1400 return new AtCommandResult(AtCommandResult.OK);
1401 }
1402 }
1403 return new AtCommandResult(AtCommandResult.ERROR);
1404 }
1405 @Override
1406 public AtCommandResult handleTestCommand() {
1407 return new AtCommandResult("+CHLD: (0,1,2,3)");
1408 }
1409 });
1410
1411 // Get Network operator name
1412 parser.register("+COPS", new AtCommandHandler() {
1413 @Override
1414 public AtCommandResult handleReadCommand() {
1415 String operatorName = mPhone.getServiceState().getOperatorAlphaLong();
1416 if (operatorName != null) {
1417 if (operatorName.length() > 16) {
1418 operatorName = operatorName.substring(0, 16);
1419 }
1420 return new AtCommandResult(
1421 "+COPS: 0,0,\"" + operatorName + "\"");
1422 } else {
1423 return new AtCommandResult(
1424 "+COPS: 0,0,\"UNKNOWN\",0");
1425 }
1426 }
1427 @Override
1428 public AtCommandResult handleSetCommand(Object[] args) {
1429 // Handsfree only supports AT+COPS=3,0
1430 if (args.length != 2 || !(args[0] instanceof Integer)
1431 || !(args[1] instanceof Integer)) {
1432 // syntax error
1433 return new AtCommandResult(AtCommandResult.ERROR);
1434 } else if ((Integer)args[0] != 3 || (Integer)args[1] != 0) {
1435 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1436 } else {
1437 return new AtCommandResult(AtCommandResult.OK);
1438 }
1439 }
1440 @Override
1441 public AtCommandResult handleTestCommand() {
1442 // Out of spec, but lets be friendly
1443 return new AtCommandResult("+COPS: (3),(0)");
1444 }
1445 });
1446
1447 // Mobile PIN
1448 // AT+CPIN is not in the handsfree spec (although it is in 3GPP)
1449 parser.register("+CPIN", new AtCommandHandler() {
1450 @Override
1451 public AtCommandResult handleReadCommand() {
1452 return new AtCommandResult("+CPIN: READY");
1453 }
1454 });
1455
1456 // Bluetooth Response and Hold
1457 // Only supported on PDC (Japan) and CDMA networks.
1458 parser.register("+BTRH", new AtCommandHandler() {
1459 @Override
1460 public AtCommandResult handleReadCommand() {
1461 // Replying with just OK indicates no response and hold
1462 // features in use now
1463 return new AtCommandResult(AtCommandResult.OK);
1464 }
1465 @Override
1466 public AtCommandResult handleSetCommand(Object[] args) {
1467 // Neeed PDC or CDMA
1468 return new AtCommandResult(AtCommandResult.ERROR);
1469 }
1470 });
1471
1472 // Request International Mobile Subscriber Identity (IMSI)
1473 // Not in bluetooth handset spec
1474 parser.register("+CIMI", new AtCommandHandler() {
1475 @Override
1476 public AtCommandResult handleActionCommand() {
1477 // AT+CIMI
1478 String imsi = mPhone.getSubscriberId();
1479 if (imsi == null || imsi.length() == 0) {
1480 return reportAtError(CME_ERROR_SIM_FAILURE);
1481 } else {
1482 return new AtCommandResult(imsi);
1483 }
1484 }
1485 });
1486
1487 // Calling Line Identification Presentation
1488 parser.register("+CLIP", new AtCommandHandler() {
1489 @Override
1490 public AtCommandResult handleReadCommand() {
1491 // Currently assumes the network is provisioned for CLIP
1492 return new AtCommandResult("+CLIP: " + (mClip ? "1" : "0") + ",1");
1493 }
1494 @Override
1495 public AtCommandResult handleSetCommand(Object[] args) {
1496 // AT+CLIP=<n>
1497 if (args.length >= 1 && (args[0].equals(0) || args[0].equals(1))) {
1498 mClip = args[0].equals(1);
1499 return new AtCommandResult(AtCommandResult.OK);
1500 } else {
1501 return new AtCommandResult(AtCommandResult.ERROR);
1502 }
1503 }
1504 @Override
1505 public AtCommandResult handleTestCommand() {
1506 return new AtCommandResult("+CLIP: (0-1)");
1507 }
1508 });
1509
1510 // Noise Reduction and Echo Cancellation control
1511 parser.register("+NREC", new AtCommandHandler() {
1512 @Override
1513 public AtCommandResult handleSetCommand(Object[] args) {
1514 if (args[0].equals(0)) {
1515 mAudioManager.setParameter(HEADSET_NREC, "off");
1516 return new AtCommandResult(AtCommandResult.OK);
1517 } else if (args[0].equals(1)) {
1518 mAudioManager.setParameter(HEADSET_NREC, "on");
1519 return new AtCommandResult(AtCommandResult.OK);
1520 }
1521 return new AtCommandResult(AtCommandResult.ERROR);
1522 }
1523 });
1524
1525 // Retrieve Subscriber Number
1526 parser.register("+CNUM", new AtCommandHandler() {
1527 @Override
1528 public AtCommandResult handleActionCommand() {
1529 String pb = "\"ME\"";
1530 PhoneBookEntry pbe = mPhoneBooks.get(pb);
1531 initPhoneBookEntry(pb, pbe);
1532
1533 final Cursor phones = pbe.mPhonebookCursor;
1534 if (phones.moveToNext()) {
1535 String number =
1536 phones.getString(pbe.mCursorNumberColumn);
1537 int type =
1538 phones.getInt(pbe.mCursorNumberTypeColumn);
1539 // 4 -- voice, 5 -- fax
1540 type = (type != PhonesColumns.TYPE_FAX_WORK) ? 4 : 5;
1541 return new AtCommandResult(
1542 "+CNUM: ,\"" + number + "\"," +
1543 PhoneNumberUtils.toaFromString(number) +
1544 ",," + type);
1545 }
1546 return new AtCommandResult(AtCommandResult.OK);
1547 }
1548 });
1549
1550 // Phone activity status
1551 parser.register("+CPAS", new AtCommandHandler() {
1552 @Override
1553 public AtCommandResult handleActionCommand() {
1554 int status = 0;
1555 switch (mPhone.getState()) {
1556 case IDLE:
1557 status = 0;
1558 break;
1559 case RINGING:
1560 status = 3;
1561 break;
1562 case OFFHOOK:
1563 status = 4;
1564 break;
1565 }
1566 return new AtCommandResult("+CPAS: " + status);
1567 }
1568 });
1569
1570 // Select Character Set
1571 // We support IRA and GSM (although we behave the same for both)
1572 parser.register("+CSCS", new AtCommandHandler() {
1573 @Override
1574 public AtCommandResult handleReadCommand() {
1575 return new AtCommandResult("+CSCS: \"IRA\"");
1576 }
1577 @Override
1578 public AtCommandResult handleSetCommand(Object[] args) {
1579 if (args.length < 1) {
1580 return new AtCommandResult(AtCommandResult.ERROR);
1581 }
1582 if (((String)args[0]).equals("\"GSM\"") || ((String)args[0]).equals("\"IRA\"")) {
1583 return new AtCommandResult(AtCommandResult.OK);
1584 } else {
1585 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1586 }
1587 }
1588 @Override
1589 public AtCommandResult handleTestCommand() {
1590 return new AtCommandResult( "+CSCS: (\"IRA\",\"GSM\")");
1591 }
1592 });
1593
1594 // Select PhoneBook memory Storage
1595 parser.register("+CPBS", new AtCommandHandler() {
1596 @Override
1597 public AtCommandResult handleReadCommand() {
1598 if (mSelectedPB == null) {
1599 return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
1600 } else {
1601 if (mSelectedPB.equals("\"SM\"")) {
1602 return new AtCommandResult("+CPBS: \"SM\",0," + MAX_PHONEBOOK_SIZE);
1603 }
1604 PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
1605 if (!initPhoneBookEntry(mSelectedPB, pbe)) {
1606 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1607 } else {
1608 int size = pbe.mPhonebookCursor.getCount();
1609 return new AtCommandResult("+CPBS: " + mSelectedPB + "," + size +
1610 "," + MAX_PHONEBOOK_SIZE);
1611 }
1612 }
1613 }
1614 @Override
1615 public AtCommandResult handleSetCommand(Object[] args) {
1616 if (args.length < 1) {
1617 return new AtCommandResult(AtCommandResult.ERROR);
1618 }
1619 mSelectedPB = (String)args[0];
1620 if (mSelectedPB.equals("\"SM\"")) {
1621 return new AtCommandResult(AtCommandResult.OK);
1622 }
1623 if (!mPhoneBooks.containsKey(mSelectedPB)) {
1624 mSelectedPB = null;
1625 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1626 } else {
1627 return new AtCommandResult(AtCommandResult.OK);
1628 }
1629 }
1630 @Override
1631 public AtCommandResult handleTestCommand() {
1632 // DC -- dialled calls
1633 // RC -- received calls
1634 // MC -- missed (or unanswered) calls
1635 // ME -- MT phonebook
1636 // SM -- SIM phonebook ** special cased until we get support
1637 return new AtCommandResult(
1638 "+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\")");
1639 }
1640 });
1641
1642
1643 // Read PhoneBook Entries
1644 parser.register("+CPBR", new AtCommandHandler() {
1645 @Override
1646 public AtCommandResult handleSetCommand(Object[] args) {
1647 // AT+CPBR=<index1>[,<index2>]
1648 // Phone Book Read Request
1649 if (mSelectedPB == null) {
1650 return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
1651 } else if (args.length < 2 || !(args[0] instanceof Integer) ||
1652 !(args[1] instanceof Integer)) {
1653 return new AtCommandResult(AtCommandResult.ERROR);
1654 }
1655
1656 if (mSelectedPB.equals("\"SM\"")) {
1657 return new AtCommandResult(AtCommandResult.OK);
1658 }
1659
1660 PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
1661 int errorDetected = -1; // no error
1662 if (pbe.mPhonebookCursor.getCount() > 0) {
1663 // Parse out index1 and index2
1664 if (args.length < 1) {
1665 return new AtCommandResult(AtCommandResult.ERROR);
1666 }
1667 if (!(args[0] instanceof Integer)) {
1668 return reportAtError(CME_ERROR_TEXT_HAS_INVALID_CHARS);
1669 }
1670 int index1 = (Integer)args[0];
1671 int index2 = index1;
1672 if (args.length >= 2) {
1673 if (args[1] instanceof Integer) {
1674 index2 = (Integer)args[1];
1675 } else {
1676 return reportAtError(CME_ERROR_TEXT_HAS_INVALID_CHARS);
1677 }
1678 }
1679
1680 // Process
1681 pbe.mPhonebookCursor.moveToPosition(index1 - 1);
1682 AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
1683 while (index1 <= index2) {
1684 String number = pbe.mPhonebookCursor.getString(pbe.mCursorNumberColumn);
1685 String name = null;
1686 int type = -1; // will show up as unknown
1687 if (pbe.mCursorNameColumn == -1) {
1688 // Do a caller ID lookup
1689 Cursor c = mPhone.getContext().getContentResolver().query(
1690 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, number),
1691 new String[] {Phones.NAME, Phones.TYPE}, null, null, null);
1692 if (c != null) {
1693 if (c.moveToFirst()) {
1694 name = c.getString(0);
1695 type = c.getInt(1);
1696 }
1697 c.close();
1698 }
1699 } else {
1700 name = pbe.mPhonebookCursor.getString(pbe.mCursorNameColumn);
1701 }
1702 if (TextUtils.isEmpty(name)) {
1703 name = "unknown";
1704 } else {
1705 name = name.trim();
1706 if (name.length() > 28) name = name.substring(0, 28);
1707 if (pbe.mCursorNumberTypeColumn != -1) {
1708 type = pbe.mPhonebookCursor.getInt(pbe.mCursorNumberTypeColumn);
1709 }
1710 name = name + "/" + getPhoneType(type);
1711 }
1712
1713 int regionType = PhoneNumberUtils.toaFromString(number);
1714
1715 number = number.trim();
1716 if (number.length() > 30) number = number.substring(0, 30);
1717
1718 result.addResponse("+CPBR: " + index1 + ",\"" + number + "\"," +
1719 regionType + ",\"" + name + "\"");
1720 if (pbe.mPhonebookCursor.moveToNext() == false) {
1721 break;
1722 }
1723 index1++;
1724 }
1725 return result;
1726 } else {
1727 if (DBG) log("No phone book entries for " + mSelectedPB);
1728 return new AtCommandResult(AtCommandResult.OK);
1729 }
1730 }
1731 @Override
1732 public AtCommandResult handleTestCommand() {
1733 // Obtain the number of calls according to the
1734 // phone book type. We save the result and just return the number
1735 // of entries in the respective list.
1736
1737 if (mSelectedPB == null) {
1738 return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
1739 }
1740 if (mSelectedPB.equals("\"SM\"")) {
1741 return new AtCommandResult(AtCommandResult.OK);
1742 }
1743 if (!mPhoneBooks.containsKey(mSelectedPB)) {
1744 mSelectedPB = null;
1745 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1746 }
1747
1748 PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
1749 int numEntries = 0;
1750
1751 if (!initPhoneBookEntry(mSelectedPB, pbe)) {
1752 return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
1753 }
1754
1755 numEntries = pbe.mPhonebookCursor.getCount();
1756 if (numEntries > 0) {
1757 return new AtCommandResult("+CPBR: (1-" + numEntries + "),30,30");
1758 }
1759 return new AtCommandResult(AtCommandResult.OK);
1760 }
1761 });
1762 }
1763
1764 private static final int START_CALL_TIMEOUT = 10000; // ms
1765
1766 private synchronized void waitForCallStart() {
1767 mWaitingForCallStart = true;
1768 Message msg = Message.obtain(mHandler, CHECK_CALL_STARTED);
1769 mHandler.sendMessageDelayed(msg, START_CALL_TIMEOUT);
1770 if (!mStartCallWakeLock.isHeld()) {
1771 mStartCallWakeLock.acquire(START_CALL_TIMEOUT);
1772 }
1773 }
1774
1775 private synchronized void callStarted() {
1776 if (mWaitingForCallStart) {
1777 mWaitingForCallStart = false;
1778 sendURC("OK");
1779 if (mStartCallWakeLock.isHeld()) {
1780 mStartCallWakeLock.release();
1781 }
1782 }
1783 }
1784
1785 private DebugThread mDebugThread;
1786
1787 private boolean inDebug() {
1788 return DBG && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE, false);
1789 }
1790
1791 private boolean allowAudioAnytime() {
1792 return inDebug() && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE_AUDIO_ANYTIME,
1793 false);
1794 }
1795
1796 private void startDebug() {
1797 if (DBG && mDebugThread == null) {
1798 mDebugThread = new DebugThread();
1799 mDebugThread.start();
1800 }
1801 }
1802
1803 private void stopDebug() {
1804 if (mDebugThread != null) {
1805 mDebugThread.interrupt();
1806 mDebugThread = null;
1807 }
1808 }
1809
1810 /** Debug thread to read debug properties - runs when debug.bt.hfp is true
1811 * at the time a bluetooth handsfree device is connected. Debug properties
1812 * are polled and mock updates sent every 1 second */
1813 private class DebugThread extends Thread {
1814 /** Turns on/off handsfree profile debugging mode */
1815 private static final String DEBUG_HANDSFREE = "debug.bt.hfp";
1816
1817 /** Mock battery level change - use 0 to 5 */
1818 private static final String DEBUG_HANDSFREE_BATTERY = "debug.bt.hfp.battery";
1819
1820 /** Mock no cellular service when false */
1821 private static final String DEBUG_HANDSFREE_SERVICE = "debug.bt.hfp.service";
1822
1823 /** Mock cellular roaming when true */
1824 private static final String DEBUG_HANDSFREE_ROAM = "debug.bt.hfp.roam";
1825
1826 /** false to true transition will force an audio (SCO) connection to
1827 * be established. true to false will force audio to be disconnected
1828 */
1829 private static final String DEBUG_HANDSFREE_AUDIO = "debug.bt.hfp.audio";
1830
1831 /** true allows incoming SCO connection out of call.
1832 */
1833 private static final String DEBUG_HANDSFREE_AUDIO_ANYTIME = "debug.bt.hfp.audio_anytime";
1834
1835 /** Mock signal strength change in ASU - use 0 to 31 */
1836 private static final String DEBUG_HANDSFREE_SIGNAL = "debug.bt.hfp.signal";
1837
1838 /** Debug AT+CLCC: print +CLCC result */
1839 private static final String DEBUG_HANDSFREE_CLCC = "debug.bt.hfp.clcc";
1840
1841 @Override
1842 public void run() {
1843 boolean oldService = true;
1844 boolean oldRoam = false;
1845 boolean oldAudio = false;
1846
1847 while (!isInterrupted() && inDebug()) {
1848 int batteryLevel = SystemProperties.getInt(DEBUG_HANDSFREE_BATTERY, -1);
1849 if (batteryLevel >= 0 && batteryLevel <= 5) {
1850 Intent intent = new Intent();
1851 intent.putExtra("level", batteryLevel);
1852 intent.putExtra("scale", 5);
1853 mPhoneState.updateBatteryState(intent);
1854 }
1855
1856 boolean serviceStateChanged = false;
1857 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_SERVICE, true) != oldService) {
1858 oldService = !oldService;
1859 serviceStateChanged = true;
1860 }
1861 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_ROAM, false) != oldRoam) {
1862 oldRoam = !oldRoam;
1863 serviceStateChanged = true;
1864 }
1865 if (serviceStateChanged) {
1866 Bundle b = new Bundle();
1867 b.putString("state", oldService ? "IN_SERVICE" : "OUT_OF_SERVICE");
1868 b.putBoolean("roaming", oldRoam);
1869 mPhoneState.updateServiceState(true, ServiceState.newFromBundle(b));
1870 }
1871
1872 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_AUDIO, false) != oldAudio) {
1873 oldAudio = !oldAudio;
1874 if (oldAudio) {
1875 audioOn();
1876 } else {
1877 audioOff();
1878 }
1879 }
1880
1881 int signalLevel = SystemProperties.getInt(DEBUG_HANDSFREE_SIGNAL, -1);
1882 if (signalLevel >= 0 && signalLevel <= 31) {
1883 Intent intent = new Intent();
1884 intent.putExtra("asu", signalLevel);
1885 mPhoneState.updateSignalState(intent);
1886 }
1887
1888 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_CLCC, false)) {
1889 log(getClccResult().toString());
1890 }
1891 try {
1892 sleep(1000); // 1 second
1893 } catch (InterruptedException e) {
1894 break;
1895 }
1896 }
1897 }
1898 };
1899
1900 private void log(String msg) {
1901 Log.d(TAG, msg);
1902 }
1903}