blob: fbb416d5cbefdcd96818398b0a3ebf2ce113ce85 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.phone;
import android.bluetooth.AtCommandHandler;
import android.bluetooth.AtCommandResult;
import android.bluetooth.AtParser;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.HeadsetBase;
import android.bluetooth.ScoSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
import android.os.AsyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemProperties;
import android.provider.CallLog.Calls;
import android.provider.Contacts.Phones;
import android.provider.Contacts.PhonesColumns;
import com.android.internal.telephony.Call;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.TelephonyIntents;
import android.telephony.PhoneNumberUtils;
import android.telephony.ServiceState;
import android.text.TextUtils;
import android.util.Log;
import java.util.LinkedList;
import java.util.HashMap;
/**
* Bluetooth headset manager for the Phone app.
* @hide
*/
public class BluetoothHandsfree {
private static final String TAG = "BT HS/HF";
private static final boolean DBG = false;
private static final boolean VDBG = false; // even more logging
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_HEADSET = 1;
public static final int TYPE_HANDSFREE = 2;
private Context mContext;
private Phone mPhone;
private ServiceState mServiceState;
private HeadsetBase mHeadset; // null when not connected
private int mHeadsetType;
private boolean mAudioPossible;
private ScoSocket mIncomingSco;
private ScoSocket mOutgoingSco;
private ScoSocket mConnectedSco;
private Call mForegroundCall;
private Call mBackgroundCall;
private Call mRingingCall;
private AudioManager mAudioManager;
private PowerManager mPowerManager;
private boolean mUserWantsAudio;
private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call
// AT command state
private static final int MAX_CONNECTIONS = 3; // Max connections for clcc indexing
// TODO: Verify
private boolean mClip = false; // Calling Line Information Presentation
private boolean mIndicatorsEnabled = false;
private boolean mCmee = false; // Extended Error reporting
private String mSelectedPB; // currently-selected phone book (includes quotes)
private BluetoothPhoneState mPhoneState; // for CIND and CIEV updates
private long[] mClccTimestamps; // Timestamps associated with each clcc index
private boolean[] mClccUsed; // Is this clcc index in use
private boolean mWaitingForCallStart;
// Audio parameters
private static final String HEADSET_NREC = "bt_headset_nrec";
private static final String HEADSET_NAME = "bt_headset_name";
private HashMap<String, PhoneBookEntry> mPhoneBooks = new HashMap<String, PhoneBookEntry>(5);
private class PhoneBookEntry {
public Cursor mPhonebookCursor; // result set of last phone-book query
public int mCursorNumberColumn;
public int mCursorNumberTypeColumn;
public int mCursorNameColumn;
};
public static String typeToString(int type) {
switch (type) {
case TYPE_UNKNOWN:
return "unknown";
case TYPE_HEADSET:
return "headset";
case TYPE_HANDSFREE:
return "handsfree";
}
return null;
}
/** The projection to use when querying the call log database in response
to AT+CPBR for the MC, RC, and DC phone books (missed, received, and
dialed calls respectively)
*/
private static final String[] CALLS_PROJECTION = new String[] {
Calls._ID, Calls.NUMBER
};
/** The projection to use when querying the contacts database in response
to AT+CPBR for the ME phonebook (saved phone numbers).
*/
private static final String[] PHONES_PROJECTION = new String[] {
Phones._ID, Phones.NAME,
Phones.NUMBER, Phones.TYPE
};
/** The projection to use when querying the contacts database in response
to AT+CNUM for the ME phonebook (saved phone numbers). We need only
the phone numbers here and the phone type.
*/
private static final String[] PHONES_LITE_PROJECTION = new String[] {
Phones._ID, Phones.NUMBER, Phones.TYPE
};
/** Android supports as many phonebook entries as the flash can hold, but
* BT periphals don't. Limit the number we'll report. */
private static final int MAX_PHONEBOOK_SIZE = 16384;
private static final String OUTGOING_CALL_WHERE =
Calls.TYPE + "=" + Calls.OUTGOING_TYPE;
private static final String INCOMING_CALL_WHERE =
Calls.TYPE + "=" + Calls.INCOMING_TYPE;
private static final String MISSED_CALL_WHERE =
Calls.TYPE + "=" + Calls.MISSED_TYPE;
public BluetoothHandsfree(Context context, Phone phone) {
mPhone = phone;
mContext = context;
BluetoothDevice bluetooth =
(BluetoothDevice)context.getSystemService(Context.BLUETOOTH_SERVICE);
boolean bluetoothCapable = (bluetooth != null);
mHeadset = null; // nothing connected yet
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
this.toString());
mStartCallWakeLock.setReferenceCounted(false);
if (bluetoothCapable) {
resetAtState();
}
mRingingCall = mPhone.getRingingCall();
mForegroundCall = mPhone.getForegroundCall();
mBackgroundCall = mPhone.getBackgroundCall();
mPhoneState = new BluetoothPhoneState();
mUserWantsAudio = true;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
/* package */ synchronized void onBluetoothEnabled() {
/* Bluez has a bug where it will always accept and then orphan
* incoming SCO connections, regardless of whether we have a listening
* SCO socket. So the best thing to do is always run a listening socket
* while bluetooth is on so that at least we can diconnect it
* immediately when we don't want it.
*/
if (mIncomingSco == null) {
mIncomingSco = createScoSocket();
mIncomingSco.accept();
}
}
/* package */ synchronized void onBluetoothDisabled() {
if (mConnectedSco != null) {
mAudioManager.setBluetoothScoOn(false);
mConnectedSco.close();
mConnectedSco = null;
}
if (mOutgoingSco != null) {
mOutgoingSco.close();
mOutgoingSco = null;
}
if (mIncomingSco != null) {
mIncomingSco.close();
mIncomingSco = null;
}
}
private boolean isHeadsetConnected() {
if (mHeadset == null) {
return false;
}
return mHeadset.isConnected();
}
/* package */ void connectHeadset(HeadsetBase headset, int headsetType) {
mHeadset = headset;
mHeadsetType = headsetType;
if (mHeadsetType == TYPE_HEADSET) {
initializeHeadsetAtParser();
} else {
initializeHandsfreeAtParser();
}
headset.startEventThread();
configAudioParameters();
if (inDebug()) {
startDebug();
}
if (isIncallAudio()) {
audioOn();
}
}
/* returns true if there is some kind of in-call audio we may wish to route
* bluetooth to */
private boolean isIncallAudio() {
Call.State state = mForegroundCall.getState();
return (state == Call.State.ACTIVE || state == Call.State.ALERTING);
}
/* package */ void disconnectHeadset() {
mHeadset = null;
stopDebug();
resetAtState();
}
private void resetAtState() {
mClip = false;
mIndicatorsEnabled = false;
mCmee = false;
mSelectedPB = null;
mPhoneBooks.clear();
// DC -- dialled calls
// RC -- received calls
// MC -- missed (or unanswered) calls
// ME -- MT phonebook
mPhoneBooks.put("\"DC\"", new PhoneBookEntry());
mPhoneBooks.put("\"RC\"", new PhoneBookEntry());
mPhoneBooks.put("\"MC\"", new PhoneBookEntry());
mPhoneBooks.put("\"ME\"", new PhoneBookEntry());
mClccTimestamps = new long[MAX_CONNECTIONS];
mClccUsed = new boolean[MAX_CONNECTIONS];
for (int i = 0; i < MAX_CONNECTIONS; i++) {
mClccUsed[i] = false;
}
}
private void configAudioParameters() {
String name = mHeadset.getName();
if (name == null) {
name = "<unknown>";
}
mAudioManager.setParameter(HEADSET_NAME, name);
mAudioManager.setParameter(HEADSET_NREC, "on");
}
/** Represents the data that we send in a +CIND or +CIEV command to the HF
*/
private class BluetoothPhoneState {
// 0: no service
// 1: service
private int mService;
// 0: no active call
// 1: active call (where active means audio is routed - not held call)
private int mCall;
// 0: not in call setup
// 1: incoming call setup
// 2: outgoing call setup
// 3: remote party being alerted in an outgoing call setup
private int mCallsetup;
// 0: no calls held
// 1: held call and active call
// 2: held call only
private int mCallheld;
// cellular signal strength of AG: 0-5
private int mSignal;
// cellular signal strength in CSQ rssi scale
private int mRssi; // for CSQ
// 0: roaming not active (home)
// 1: roaming active
private int mRoam;
// battery charge of AG: 0-5
private int mBattchg;
// 0: not registered
// 1: registered, home network
// 5: registered, roaming
private int mStat; // for CREG
private String mRingingNumber; // Context for in-progress RING's
private int mRingingType;
private boolean mIgnoreRing = false;
private static final int SERVICE_STATE_CHANGED = 1;
private static final int PHONE_STATE_CHANGED = 2;
private static final int RING = 3;
private Handler mStateChangeHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case RING:
AtCommandResult result = ring();
if (result != null) {
sendURC(result.toString());
}
break;
case SERVICE_STATE_CHANGED:
ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
updateServiceState(sendUpdate(), state);
break;
case PHONE_STATE_CHANGED:
Connection connection = null;
if (((AsyncResult) msg.obj).result instanceof Connection) {
connection = (Connection) ((AsyncResult) msg.obj).result;
}
updatePhoneState(sendUpdate(), connection);
break;
}
}
};
private BluetoothPhoneState() {
// init members
updateServiceState(false, mPhone.getServiceState());
updatePhoneState(false, null);
mBattchg = 5; // There is currently no API to get battery level
// on demand, so set to 5 and wait for an update
mSignal = asuToSignal(mPhone.getSignalStrengthASU());
// register for updates
mPhone.registerForServiceStateChanged(mStateChangeHandler,
SERVICE_STATE_CHANGED, null);
mPhone.registerForPhoneStateChanged(mStateChangeHandler,
PHONE_STATE_CHANGED, null);
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED);
mContext.registerReceiver(mStateReceiver, filter);
}
private boolean sendUpdate() {
return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mIndicatorsEnabled;
}
private boolean sendClipUpdate() {
return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mClip;
}
/* convert [0,31] ASU signal strength to the [0,5] expected by
* bluetooth devices. Scale is similar to status bar policy
*/
private int asuToSignal(int asu) {
if (asu >= 16) return 5;
else if (asu >= 8) return 4;
else if (asu >= 4) return 3;
else if (asu >= 2) return 2;
else if (asu >= 1) return 1;
else return 0;
}
/* convert [0,5] signal strength to a rssi signal strength for CSQ
* which is [0,31]. Despite the same scale, this is not the same value
* as ASU.
*/
private int signalToRssi(int signal) {
// using C4A suggested values
switch (signal) {
case 0: return 0;
case 1: return 4;
case 2: return 8;
case 3: return 13;
case 4: return 19;
case 5: return 31;
}
return 0;
}
private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) {
updateBatteryState(intent);
} else if (intent.getAction().equals(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED)) {
updateSignalState(intent);
}
}
};
private synchronized void updateBatteryState(Intent intent) {
int batteryLevel = intent.getIntExtra("level", -1);
int scale = intent.getIntExtra("scale", -1);
if (batteryLevel == -1 || scale == -1) {
return; // ignore
}
batteryLevel = batteryLevel * 5 / scale;
if (mBattchg != batteryLevel) {
mBattchg = batteryLevel;
if (sendUpdate()) {
sendURC("+CIEV: 7," + mBattchg);
}
}
}
private synchronized void updateSignalState(Intent intent) {
int signal;
signal = asuToSignal(intent.getIntExtra("asu", -1));
mRssi = signalToRssi(signal); // no unsolicited CSQ
if (signal != mSignal) {
mSignal = signal;
if (sendUpdate()) {
sendURC("+CIEV: 5," + mSignal);
}
}
}
private synchronized void updateServiceState(boolean sendUpdate, ServiceState state) {
int service = state.getState() == ServiceState.STATE_IN_SERVICE ? 1 : 0;
int roam = state.getRoaming() ? 1 : 0;
int stat;
AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
if (service == 0) {
stat = 0;
} else {
stat = (roam == 1) ? 5 : 1;
}
if (service != mService) {
mService = service;
if (sendUpdate) {
result.addResponse("+CIEV: 1," + mService);
}
}
if (roam != mRoam) {
mRoam = roam;
if (sendUpdate) {
result.addResponse("+CIEV: 6," + mRoam);
}
}
if (stat != mStat) {
mStat = stat;
if (sendUpdate) {
result.addResponse(toCregString());
}
}
sendURC(result.toString());
}
private synchronized void updatePhoneState(boolean sendUpdate, Connection connection) {
int call = 0;
int callsetup = 0;
int callheld = 0;
int prevCallsetup = mCallsetup;
AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
if (DBG) log("updatePhoneState()");
switch (mPhone.getState()) {
case IDLE:
mUserWantsAudio = true; // out of call - reset state
audioOff();
break;
default:
callStarted();
}
switch(mForegroundCall.getState()) {
case ACTIVE:
call = 1;
mAudioPossible = true;
break;
case DIALING:
callsetup = 2;
mAudioPossible = false;
break;
case ALERTING:
callsetup = 3;
mAudioPossible = true;
break;
default:
mAudioPossible = false;
}
switch(mRingingCall.getState()) {
case INCOMING:
case WAITING:
callsetup = 1;
break;
}
switch(mBackgroundCall.getState()) {
case HOLDING:
if (call == 1) {
callheld = 1;
} else {
call = 1;
callheld = 2;
}
break;
}
if (mCall != call) {
mCall = call;
if (sendUpdate) {
result.addResponse("+CIEV: 2," + mCall);
}
}
if (mCallsetup != callsetup) {
mCallsetup = callsetup;
if (sendUpdate) {
result.addResponse("+CIEV: 3," + mCallsetup);
}
}
if (mCallheld != callheld) {
mCallheld = callheld;
if (sendUpdate) {
result.addResponse("+CIEV: 4," + mCallheld);
}
}
if (callsetup == 1 && callsetup != prevCallsetup) {
// new incoming call
String number = null;
int type = 128;
if (sendUpdate) {
// find incoming phone number and type
if (connection == null) {
connection = mRingingCall.getEarliestConnection();
if (connection == null) {
Log.e(TAG, "Could not get a handle on Connection object for new " +
"incoming call");
}
}
if (connection != null) {
number = connection.getAddress();
if (number != null) {
type = PhoneNumberUtils.toaFromString(number);
}
}
if (number == null) {
number = "";
}
}
if ((call != 0 || callheld != 0) && sendUpdate) {
// call waiting
result.addResponse("+CCWA: \"" + number + "\"," + type);
} else {
// regular new incoming call
mRingingNumber = number;
mRingingType = type;
mIgnoreRing = false;
result.addResult(ring());
}
}
sendURC(result.toString());
}
private AtCommandResult ring() {
if (!mIgnoreRing && mRingingCall.isRinging()) {
AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED);
result.addResponse("RING");
if (sendClipUpdate()) {
result.addResponse("+CLIP: \"" + mRingingNumber + "\"," + mRingingType);
}
Message msg = mStateChangeHandler.obtainMessage(RING);
mStateChangeHandler.sendMessageDelayed(msg, 3000);
return result;
}
return null;
}
private synchronized String toCregString() {
return new String("+CREG: 1," + mStat);
}
private synchronized AtCommandResult toCindResult() {
AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
String status = "+CIND: " + mService + "," + mCall + "," + mCallsetup + "," +
mCallheld + "," + mSignal + "," + mRoam + "," + mBattchg;
result.addResponse(status);
return result;
}
private synchronized AtCommandResult toCsqResult() {
AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
String status = "+CSQ: " + mRssi + ",99";
result.addResponse(status);
return result;
}
private synchronized AtCommandResult getCindTestResult() {
return new AtCommandResult("+CIND: (\"service\",(0-1))," + "(\"call\",(0-1))," +
"(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5))," +
"(\"roam\",(0-1)),(\"battchg\",(0-5))");
}
private synchronized void ignoreRing() {
mCallsetup = 0;
mIgnoreRing = true;
if (sendUpdate()) {
sendURC("+CIEV: 3," + mCallsetup);
}
}
};
private static final int SCO_ACCEPTED = 1;
private static final int SCO_CONNECTED = 2;
private static final int SCO_CLOSED = 3;
private static final int CHECK_CALL_STARTED = 4;
private final Handler mHandler = new Handler() {
@Override
public synchronized void handleMessage(Message msg) {
switch (msg.what) {
case SCO_ACCEPTED:
if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
if (isHeadsetConnected() && mAudioPossible && mConnectedSco == null) {
Log.i(TAG, "Routing audio for incoming SCO connection");
mConnectedSco = (ScoSocket)msg.obj;
mAudioManager.setBluetoothScoOn(true);
} else {
Log.i(TAG, "Rejecting incoming SCO connection");
((ScoSocket)msg.obj).close();
}
} // else error trying to accept, try again
mIncomingSco = createScoSocket();
mIncomingSco.accept();
break;
case SCO_CONNECTED:
if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected() &&
mConnectedSco == null) {
if (DBG) log("Routing audio for outgoing SCO conection");
mConnectedSco = (ScoSocket)msg.obj;
mAudioManager.setBluetoothScoOn(true);
} else if (msg.arg1 == ScoSocket.STATE_CONNECTED) {
if (DBG) log("Rejecting new connected outgoing SCO socket");
((ScoSocket)msg.obj).close();
mOutgoingSco.close();
}
mOutgoingSco = null;
break;
case SCO_CLOSED:
if (mConnectedSco == (ScoSocket)msg.obj) {
mConnectedSco = null;
mAudioManager.setBluetoothScoOn(false);
} else if (mOutgoingSco == (ScoSocket)msg.obj) {
mOutgoingSco = null;
} else if (mIncomingSco == (ScoSocket)msg.obj) {
mIncomingSco = null;
}
break;
case CHECK_CALL_STARTED:
if (mWaitingForCallStart) {
mWaitingForCallStart = false;
Log.e(TAG, "Timeout waiting for call to start");
sendURC("ERROR");
if (mStartCallWakeLock.isHeld()) {
mStartCallWakeLock.release();
}
}
break;
}
}
};
private ScoSocket createScoSocket() {
return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED);
}
/** Request to establish SCO (audio) connection to bluetooth
* headset/handsfree, if one is connected. Does not block.
* Returns false if the user has requested audio off, or if there
* is some other immediate problem that will prevent BT audio.
*/
/* package */ synchronized boolean audioOn() {
if (VDBG) log("audioOn()");
if (!isHeadsetConnected()) {
if (DBG) log("audioOn(): headset is not connected!");
return false;
}
if (mConnectedSco != null) {
if (DBG) log("audioOn(): audio is already connected");
return true;
}
if (!mUserWantsAudio) {
if (DBG) log("audioOn(): user requested no audio, ignoring");
return false;
}
if (mOutgoingSco != null) {
if (DBG) log("audioOn(): outgoing SCO already in progress");
return true;
}
mOutgoingSco = createScoSocket();
if (!mOutgoingSco.connect(mHeadset.getAddress())) {
mOutgoingSco = null;
}
return true;
}
/** Used to indicate the user requested BT audio on.
* This will establish SCO (BT audio), even if the user requested it off
* previously on this call.
*/
/* package */ synchronized void userWantsAudioOn() {
mUserWantsAudio = true;
audioOn();
}
/** Used to indicate the user requested BT audio off.
* This will prevent us from establishing BT audio again during this call
* if audioOn() is called.
*/
/* package */ synchronized void userWantsAudioOff() {
mUserWantsAudio = false;
audioOff();
}
/** Request to disconnect SCO (audio) connection to bluetooth
* headset/handsfree, if one is connected. Does not block.
*/
/* package */ synchronized void audioOff() {
if (VDBG) log("audioOff()");
if (mConnectedSco != null) {
mAudioManager.setBluetoothScoOn(false);
mConnectedSco.close();
mConnectedSco = null;
}
if (mOutgoingSco != null) {
mOutgoingSco.close();
mOutgoingSco = null;
}
}
/* package */ boolean isAudioOn() {
return (mConnectedSco != null);
}
/* package */ void ignoreRing() {
mPhoneState.ignoreRing();
}
/* List of AT error codes specified by the Handsfree profile. */
private static final int CME_ERROR_AG_FAILURE = 0;
private static final int CME_ERROR_NO_CONNECTION_TO_PHONE = 1;
// private static final int CME_ERROR_ = 2;
private static final int CME_ERROR_OPERATION_NOT_ALLOWED = 3;
private static final int CME_ERROR_OPERATION_NOT_SUPPORTED = 4;
private static final int CME_ERROR_PIN_REQUIRED = 5;
// private static final int CME_ERROR_ = 6;
// private static final int CME_ERROR_ = 7;
// private static final int CME_ERROR_ = 8;
// private static final int CME_ERROR_ = 9;
private static final int CME_ERROR_SIM_MISSING = 10;
private static final int CME_ERROR_SIM_PIN_REQUIRED = 11;
private static final int CME_ERROR_SIM_PUK_REQUIRED = 12;
private static final int CME_ERROR_SIM_FAILURE = 13;
private static final int CME_ERROR_SIM_BUSY = 14;
// private static final int CME_ERROR_ = 15;
private static final int CME_ERROR_WRONG_PASSWORD = 16;
private static final int CME_ERROR_SIM_PIN2_REQUIRED = 17;
private static final int CME_ERROR_SIM_PUK2_REQUIRED = 18;
// private static final int CME_ERROR_ = 19;
private static final int CME_ERROR_MEMORY_FULL = 20;
private static final int CME_ERROR_INVALID_INDEX = 21;
// private static final int CME_ERROR_ = 22;
private static final int CME_ERROR_MEMORY_FAILURE = 23;
private static final int CME_ERROR_TEXT_TOO_LONG = 24;
private static final int CME_ERROR_TEXT_HAS_INVALID_CHARS = 25;
private static final int CME_ERROR_DIAL_STRING_TOO_LONG = 26;
private static final int CME_ERROR_DIAL_STRING_HAS_INVALID_CHARS = 27;
// private static final int CME_ERROR_ = 28;
// private static final int CME_ERROR_ = 29;
private static final int CME_ERROR_NO_SERVICE = 30;
// private static final int CME_ERROR_ = 31;
private static final int CME_ERROR_911_ONLY_ALLOWED = 32;
public AtCommandResult reportAtError(int error) {
if (mCmee) {
AtCommandResult result =
new AtCommandResult(AtCommandResult.UNSOLICITED);
result.addResponse("+CME ERROR: " + error);
return result;
} else {
return new AtCommandResult(AtCommandResult.ERROR);
}
}
private static String getPhoneType(int type) {
switch (type) {
case PhonesColumns.TYPE_HOME:
return "H";
case PhonesColumns.TYPE_MOBILE:
return "M";
case PhonesColumns.TYPE_WORK:
return "W";
case PhonesColumns.TYPE_FAX_HOME:
case PhonesColumns.TYPE_FAX_WORK:
return "F";
case PhonesColumns.TYPE_OTHER:
case PhonesColumns.TYPE_CUSTOM:
default:
return "O";
}
}
private boolean initPhoneBookEntry(String pb, PhoneBookEntry pbe) {
String where;
boolean ancillaryPhonebook = true;
if (pb.equals("\"ME\"")) {
ancillaryPhonebook = false;
where = null;
} else if (pb.equals("\"DC\"")) {
where = OUTGOING_CALL_WHERE;
} else if (pb.equals("\"RC\"")) {
where = INCOMING_CALL_WHERE;
} else if (pb.equals("\"MC\"")) {
where = MISSED_CALL_WHERE;
} else {
return false;
}
if (pbe.mPhonebookCursor == null) {
if (ancillaryPhonebook) {
// ContentValues values = new ContentValues();
// values.put(Calls.NEW, "0");
// mContext.getContentResolver().update(Calls.CONTENT_URI, values, where, null);
}
if (ancillaryPhonebook) {
pbe.mPhonebookCursor = mContext.getContentResolver().query(
Calls.CONTENT_URI, CALLS_PROJECTION, where, null,
Calls.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
pbe.mCursorNumberColumn = pbe.mPhonebookCursor.getColumnIndexOrThrow(Calls.NUMBER);
pbe.mCursorNumberTypeColumn = -1;
pbe.mCursorNameColumn = -1;
} else {
pbe.mPhonebookCursor = mContext.getContentResolver().query(
Phones.CONTENT_URI, PHONES_PROJECTION, where, null,
Phones.DEFAULT_SORT_ORDER + " LIMIT " + MAX_PHONEBOOK_SIZE);
pbe.mCursorNumberColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.NUMBER);
pbe.mCursorNumberTypeColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.TYPE);
pbe.mCursorNameColumn = pbe.mPhonebookCursor.getColumnIndex(Phones.NAME);
}
}
return true;
}
private void sendURC(String urc) {
if (isHeadsetConnected()) {
mHeadset.sendURC(urc);
}
}
/** helper to redial last dialled number */
private AtCommandResult redial() {
// Get the last dialled number from the phone book
String[] projection = {Calls.NUMBER};
Cursor cursor = mContext.getContentResolver().query(Calls.CONTENT_URI, projection,
Calls.TYPE + "=" + Calls.OUTGOING_TYPE, null, Calls.DEFAULT_SORT_ORDER +
" LIMIT 1");
if (cursor.getCount() < 1) {
// spec seems to suggest sending ERROR if we dont have a
// number to redial
if (DBG) log("Bluetooth redial requested (+BLDN), but no previous " +
"outgoing calls found. Ignoring");
cursor.close();
return new AtCommandResult(AtCommandResult.ERROR);
}
cursor.moveToNext();
int column = cursor.getColumnIndexOrThrow(Calls.NUMBER);
String number = cursor.getString(column);
cursor.close();
// Call it
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts("tel", number, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
// We do not immediately respond OK, wait until we get a phone state
// update. If we return OK now and the handsfree immeidately requests
// our phone state it will say we are not in call yet which confuses
// some devices
waitForCallStart();
return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
}
/** Build the +CLCC result
* The complexity arises from the fact that we need to maintain the same
* CLCC index even as a call moves between states. */
private synchronized AtCommandResult getClccResult() {
// Collect all known connections
Connection[] clccConnections = new Connection[MAX_CONNECTIONS]; // indexed by CLCC index
LinkedList<Connection> newConnections = new LinkedList<Connection>();
LinkedList<Connection> connections = new LinkedList<Connection>();
if (mRingingCall.getState().isAlive()) {
connections.addAll(mRingingCall.getConnections());
}
if (mForegroundCall.getState().isAlive()) {
connections.addAll(mForegroundCall.getConnections());
}
if (mBackgroundCall.getState().isAlive()) {
connections.addAll(mBackgroundCall.getConnections());
}
// Mark connections that we already known about
boolean clccUsed[] = new boolean[MAX_CONNECTIONS];
for (int i = 0; i < MAX_CONNECTIONS; i++) {
clccUsed[i] = mClccUsed[i];
mClccUsed[i] = false;
}
for (Connection c : connections) {
boolean found = false;
long timestamp = c.getCreateTime();
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (clccUsed[i] && timestamp == mClccTimestamps[i]) {
mClccUsed[i] = true;
found = true;
clccConnections[i] = c;
break;
}
}
if (!found) {
newConnections.add(c);
}
}
// Find a CLCC index for new connections
while (!newConnections.isEmpty()) {
// Find lowest empty index
int i = 0;
while (mClccUsed[i]) i++;
// Find earliest connection
long earliestTimestamp = newConnections.get(0).getCreateTime();
Connection earliestConnection = newConnections.get(0);
for (int j = 0; j < newConnections.size(); j++) {
long timestamp = newConnections.get(j).getCreateTime();
if (timestamp < earliestTimestamp) {
earliestTimestamp = timestamp;
earliestConnection = newConnections.get(j);
}
}
// update
mClccUsed[i] = true;
mClccTimestamps[i] = earliestTimestamp;
clccConnections[i] = earliestConnection;
newConnections.remove(earliestConnection);
}
// Build CLCC
AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
for (int i = 0; i < clccConnections.length; i++) {
if (mClccUsed[i]) {
String clccEntry = connectionToClccEntry(i, clccConnections[i]);
if (clccEntry != null) {
result.addResponse(clccEntry);
}
}
}
return result;
}
/** Convert a Connection object into a single +CLCC result */
private String connectionToClccEntry(int index, Connection c) {
int state;
switch (c.getState()) {
case ACTIVE:
state = 0;
break;
case HOLDING:
state = 1;
break;
case DIALING:
state = 2;
break;
case ALERTING:
state = 3;
break;
case INCOMING:
state = 4;
break;
case WAITING:
state = 5;
break;
default:
return null; // bad state
}
int mpty = 0;
Call call = c.getCall();
if (call != null) {
mpty = call.isMultiparty() ? 1 : 0;
}
int direction = c.isIncoming() ? 1 : 0;
String number = c.getAddress();
int type = -1;
if (number != null) {
type = PhoneNumberUtils.toaFromString(number);
}
String result = "+CLCC: " + (index + 1) + "," + direction + "," + state + ",0," + mpty;
if (number != null) {
result += "," + number + "," + type;
}
return result;
}
/**
* Register AT Command handlers to implement the Headset profile
*/
private void initializeHeadsetAtParser() {
if (DBG) log("Registering Headset AT commands");
AtParser parser = mHeadset.getAtParser();
// Headset's usually only have one button, which is meant to cause the
// HS to send us AT+CKPD=200 or AT+CKPD.
parser.register("+CKPD", new AtCommandHandler() {
private AtCommandResult headsetButtonPress() {
if (mRingingCall.isRinging()) {
// Answer the call
PhoneUtils.answerCall(mPhone);
audioOn();
} else if (mForegroundCall.getState().isAlive()) {
if (!isAudioOn()) {
// Transfer audio from AG to HS
audioOn();
} else {
if (mHeadset.getDirection() == HeadsetBase.DIRECTION_INCOMING &&
(System.currentTimeMillis() - mHeadset.getConnectTimestamp()) < 5000) {
// Headset made a recent ACL connection to us - and
// made a mandatory AT+CKPD request to connect
// audio which races with our automatic audio
// setup. ignore
} else {
// Hang up the call
audioOff();
PhoneUtils.hangup(mPhone);
}
}
} else {
// No current call - redial last number
return redial();
}
return new AtCommandResult(AtCommandResult.OK);
}
@Override
public AtCommandResult handleActionCommand() {
return headsetButtonPress();
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
return headsetButtonPress();
}
});
}
/**
* Register AT Command handlers to implement the Handsfree profile
*/
private void initializeHandsfreeAtParser() {
if (DBG) log("Registering Handsfree AT commands");
AtParser parser = mHeadset.getAtParser();
// Answer
parser.register('A', new AtCommandHandler() {
@Override
public AtCommandResult handleBasicCommand(String args) {
PhoneUtils.answerCall(mPhone);
return new AtCommandResult(AtCommandResult.OK);
}
});
parser.register('D', new AtCommandHandler() {
@Override
public AtCommandResult handleBasicCommand(String args) {
if (args.length() > 0) {
if (args.charAt(0) == '>') {
// Yuck - memory dialling requested.
// Just dial last number for now
if (args.startsWith(">9999")) { // for PTS test
return new AtCommandResult(AtCommandResult.ERROR);
}
return redial();
} else {
// Remove trailing ';'
if (args.charAt(args.length() - 1) == ';') {
args = args.substring(0, args.length() - 1);
}
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts("tel", args, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
waitForCallStart();
return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing
}
}
return new AtCommandResult(AtCommandResult.ERROR);
}
});
// Hang-up command
parser.register("+CHUP", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
audioOff();
PhoneUtils.hangup(mPhone);
return new AtCommandResult(AtCommandResult.OK);
}
});
// Bluetooth Retrieve Supported Features command
parser.register("+BRSF", new AtCommandHandler() {
private AtCommandResult sendBRSF() {
// Bit 0: 3 way calling
// 1: EC / NR
// 2: CLI Presentation
// 5: call reject
// 6: Enhanced call status
return new AtCommandResult("+BRSF: 99");
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// AT+BRSF=<handsfree supported features bitmap>
// Handsfree is telling us which features it supports. We
// ignore its report for now. But we do respond with our own
// feature bitmap.
return sendBRSF();
}
@Override
public AtCommandResult handleActionCommand() {
// This seems to be out of spec, but lets do the nice thing
return sendBRSF();
}
@Override
public AtCommandResult handleReadCommand() {
// This seems to be out of spec, but lets do the nice thing
return sendBRSF();
}
});
// Call waiting notification on/off
parser.register("+CCWA", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
// Seems to be out of spec, but lets return nicely
return new AtCommandResult(AtCommandResult.OK);
}
@Override
public AtCommandResult handleReadCommand() {
// Call waiting is always on
return new AtCommandResult("+CCWA: 1");
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// AT+CCWA=<n>
// Handsfree is trying to enable/disable call waiting. We
// cannot disable in the current implementation.
return new AtCommandResult(AtCommandResult.OK);
}
@Override
public AtCommandResult handleTestCommand() {
// Request for range of supported CCWA paramters
return new AtCommandResult("+CCWA: (\"n\",(1))");
}
});
// Mobile Equipment Event Reporting enable/disable command
// Of the full 3GPP syntax paramters (mode, keyp, disp, ind, bfr) we
// only support paramter ind (disable/enable evert reporting using
// +CDEV)
parser.register("+CMER", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
return new AtCommandResult(
"+CMER: 3,0,0," + (mIndicatorsEnabled ? "1" : "0"));
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args.length < 4) {
// This is a syntax error
return new AtCommandResult(AtCommandResult.ERROR);
} else if (args[0].equals(3) && args[1].equals(0) &&
args[2].equals(0)) {
if (args[3].equals(0)) {
mIndicatorsEnabled = false;
return new AtCommandResult(AtCommandResult.OK);
} else if (args[3].equals(1)) {
mIndicatorsEnabled = true;
return new AtCommandResult(AtCommandResult.OK);
}
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
} else {
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
}
}
@Override
public AtCommandResult handleTestCommand() {
return new AtCommandResult("+CMER: (3),(0),(0),(0-1)");
}
});
// Mobile Equipment Error Reporting enable/disable
parser.register("+CMEE", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
// out of spec, assume they want to enable
mCmee = true;
return new AtCommandResult(AtCommandResult.OK);
}
@Override
public AtCommandResult handleReadCommand() {
return new AtCommandResult("+CMEE: " + (mCmee ? "1" : "0"));
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// AT+CMEE=<n>
if (args.length == 0) {
// <n> ommitted - default to 0
mCmee = false;
return new AtCommandResult(AtCommandResult.OK);
} else if (!(args[0] instanceof Integer)) {
// Syntax error
return new AtCommandResult(AtCommandResult.ERROR);
} else {
mCmee = ((Integer)args[0] == 1);
return new AtCommandResult(AtCommandResult.OK);
}
}
@Override
public AtCommandResult handleTestCommand() {
// Probably not required but spec, but no harm done
return new AtCommandResult("+CMEE: (0-1)");
}
});
// Bluetooth Last Dialled Number
parser.register("+BLDN", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
return redial();
}
});
// Indicator Update command
parser.register("+CIND", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
return mPhoneState.toCindResult();
}
@Override
public AtCommandResult handleTestCommand() {
return mPhoneState.getCindTestResult();
}
});
// Query Signal Quality (legacy)
parser.register("+CSQ", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
return mPhoneState.toCsqResult();
}
});
// Query network registration state
parser.register("+CREG", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
return new AtCommandResult(mPhoneState.toCregString());
}
});
// Send DTMF. I don't know if we are also expected to play the DTMF tone
// locally, right now we don't
parser.register("+VTS", new AtCommandHandler() {
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args.length >= 1) {
char c;
if (args[0] instanceof Integer) {
c = ((Integer) args[0]).toString().charAt(0);
} else {
c = ((String) args[0]).charAt(0);
}
if (isValidDtmf(c)) {
mPhone.sendDtmf(c);
return new AtCommandResult(AtCommandResult.OK);
}
}
return new AtCommandResult(AtCommandResult.ERROR);
}
private boolean isValidDtmf(char c) {
switch (c) {
case '#':
case '*':
return true;
default:
if (Character.digit(c, 14) != -1) {
return true; // 0-9 and A-D
}
return false;
}
}
});
// List calls
parser.register("+CLCC", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
return getClccResult();
}
});
// Call Hold and Multiparty Handling command
parser.register("+CHLD", new AtCommandHandler() {
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args.length >= 1) {
if (args[0].equals(0)) {
boolean result;
if (mRingingCall.isRinging()) {
result = PhoneUtils.hangupRingingCall(mPhone);
} else {
result = PhoneUtils.hangupHoldingCall(mPhone);
}
if (result) {
return new AtCommandResult(AtCommandResult.OK);
} else {
return new AtCommandResult(AtCommandResult.ERROR);
}
} else if (args[0].equals(1)) {
// Hangup active call, answer held call
if (PhoneUtils.answerAndEndActive(mPhone)) {
return new AtCommandResult(AtCommandResult.OK);
} else {
return new AtCommandResult(AtCommandResult.ERROR);
}
} else if (args[0].equals(2)) {
PhoneUtils.switchHoldingAndActive(mPhone);
return new AtCommandResult(AtCommandResult.OK);
} else if (args[0].equals(3)) {
if (mForegroundCall.getState().isAlive() &&
mBackgroundCall.getState().isAlive()) {
PhoneUtils.mergeCalls(mPhone);
}
return new AtCommandResult(AtCommandResult.OK);
}
}
return new AtCommandResult(AtCommandResult.ERROR);
}
@Override
public AtCommandResult handleTestCommand() {
return new AtCommandResult("+CHLD: (0,1,2,3)");
}
});
// Get Network operator name
parser.register("+COPS", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
String operatorName = mPhone.getServiceState().getOperatorAlphaLong();
if (operatorName != null) {
if (operatorName.length() > 16) {
operatorName = operatorName.substring(0, 16);
}
return new AtCommandResult(
"+COPS: 0,0,\"" + operatorName + "\"");
} else {
return new AtCommandResult(
"+COPS: 0,0,\"UNKNOWN\",0");
}
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// Handsfree only supports AT+COPS=3,0
if (args.length != 2 || !(args[0] instanceof Integer)
|| !(args[1] instanceof Integer)) {
// syntax error
return new AtCommandResult(AtCommandResult.ERROR);
} else if ((Integer)args[0] != 3 || (Integer)args[1] != 0) {
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
} else {
return new AtCommandResult(AtCommandResult.OK);
}
}
@Override
public AtCommandResult handleTestCommand() {
// Out of spec, but lets be friendly
return new AtCommandResult("+COPS: (3),(0)");
}
});
// Mobile PIN
// AT+CPIN is not in the handsfree spec (although it is in 3GPP)
parser.register("+CPIN", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
return new AtCommandResult("+CPIN: READY");
}
});
// Bluetooth Response and Hold
// Only supported on PDC (Japan) and CDMA networks.
parser.register("+BTRH", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
// Replying with just OK indicates no response and hold
// features in use now
return new AtCommandResult(AtCommandResult.OK);
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// Neeed PDC or CDMA
return new AtCommandResult(AtCommandResult.ERROR);
}
});
// Request International Mobile Subscriber Identity (IMSI)
// Not in bluetooth handset spec
parser.register("+CIMI", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
// AT+CIMI
String imsi = mPhone.getSubscriberId();
if (imsi == null || imsi.length() == 0) {
return reportAtError(CME_ERROR_SIM_FAILURE);
} else {
return new AtCommandResult(imsi);
}
}
});
// Calling Line Identification Presentation
parser.register("+CLIP", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
// Currently assumes the network is provisioned for CLIP
return new AtCommandResult("+CLIP: " + (mClip ? "1" : "0") + ",1");
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// AT+CLIP=<n>
if (args.length >= 1 && (args[0].equals(0) || args[0].equals(1))) {
mClip = args[0].equals(1);
return new AtCommandResult(AtCommandResult.OK);
} else {
return new AtCommandResult(AtCommandResult.ERROR);
}
}
@Override
public AtCommandResult handleTestCommand() {
return new AtCommandResult("+CLIP: (0-1)");
}
});
// Noise Reduction and Echo Cancellation control
parser.register("+NREC", new AtCommandHandler() {
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args[0].equals(0)) {
mAudioManager.setParameter(HEADSET_NREC, "off");
return new AtCommandResult(AtCommandResult.OK);
} else if (args[0].equals(1)) {
mAudioManager.setParameter(HEADSET_NREC, "on");
return new AtCommandResult(AtCommandResult.OK);
}
return new AtCommandResult(AtCommandResult.ERROR);
}
});
// Retrieve Subscriber Number
parser.register("+CNUM", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
String pb = "\"ME\"";
PhoneBookEntry pbe = mPhoneBooks.get(pb);
initPhoneBookEntry(pb, pbe);
final Cursor phones = pbe.mPhonebookCursor;
if (phones.moveToNext()) {
String number =
phones.getString(pbe.mCursorNumberColumn);
int type =
phones.getInt(pbe.mCursorNumberTypeColumn);
// 4 -- voice, 5 -- fax
type = (type != PhonesColumns.TYPE_FAX_WORK) ? 4 : 5;
return new AtCommandResult(
"+CNUM: ,\"" + number + "\"," +
PhoneNumberUtils.toaFromString(number) +
",," + type);
}
return new AtCommandResult(AtCommandResult.OK);
}
});
// Phone activity status
parser.register("+CPAS", new AtCommandHandler() {
@Override
public AtCommandResult handleActionCommand() {
int status = 0;
switch (mPhone.getState()) {
case IDLE:
status = 0;
break;
case RINGING:
status = 3;
break;
case OFFHOOK:
status = 4;
break;
}
return new AtCommandResult("+CPAS: " + status);
}
});
// Select Character Set
// We support IRA and GSM (although we behave the same for both)
parser.register("+CSCS", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
return new AtCommandResult("+CSCS: \"IRA\"");
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args.length < 1) {
return new AtCommandResult(AtCommandResult.ERROR);
}
if (((String)args[0]).equals("\"GSM\"") || ((String)args[0]).equals("\"IRA\"")) {
return new AtCommandResult(AtCommandResult.OK);
} else {
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
}
}
@Override
public AtCommandResult handleTestCommand() {
return new AtCommandResult( "+CSCS: (\"IRA\",\"GSM\")");
}
});
// Select PhoneBook memory Storage
parser.register("+CPBS", new AtCommandHandler() {
@Override
public AtCommandResult handleReadCommand() {
if (mSelectedPB == null) {
return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
} else {
if (mSelectedPB.equals("\"SM\"")) {
return new AtCommandResult("+CPBS: \"SM\",0," + MAX_PHONEBOOK_SIZE);
}
PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
if (!initPhoneBookEntry(mSelectedPB, pbe)) {
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
} else {
int size = pbe.mPhonebookCursor.getCount();
return new AtCommandResult("+CPBS: " + mSelectedPB + "," + size +
"," + MAX_PHONEBOOK_SIZE);
}
}
}
@Override
public AtCommandResult handleSetCommand(Object[] args) {
if (args.length < 1) {
return new AtCommandResult(AtCommandResult.ERROR);
}
mSelectedPB = (String)args[0];
if (mSelectedPB.equals("\"SM\"")) {
return new AtCommandResult(AtCommandResult.OK);
}
if (!mPhoneBooks.containsKey(mSelectedPB)) {
mSelectedPB = null;
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
} else {
return new AtCommandResult(AtCommandResult.OK);
}
}
@Override
public AtCommandResult handleTestCommand() {
// DC -- dialled calls
// RC -- received calls
// MC -- missed (or unanswered) calls
// ME -- MT phonebook
// SM -- SIM phonebook ** special cased until we get support
return new AtCommandResult(
"+CPBS: (\"ME\",\"SM\",\"DC\",\"RC\",\"MC\")");
}
});
// Read PhoneBook Entries
parser.register("+CPBR", new AtCommandHandler() {
@Override
public AtCommandResult handleSetCommand(Object[] args) {
// AT+CPBR=<index1>[,<index2>]
// Phone Book Read Request
if (mSelectedPB == null) {
return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
} else if (args.length < 2 || !(args[0] instanceof Integer) ||
!(args[1] instanceof Integer)) {
return new AtCommandResult(AtCommandResult.ERROR);
}
if (mSelectedPB.equals("\"SM\"")) {
return new AtCommandResult(AtCommandResult.OK);
}
PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
int errorDetected = -1; // no error
if (pbe.mPhonebookCursor.getCount() > 0) {
// Parse out index1 and index2
if (args.length < 1) {
return new AtCommandResult(AtCommandResult.ERROR);
}
if (!(args[0] instanceof Integer)) {
return reportAtError(CME_ERROR_TEXT_HAS_INVALID_CHARS);
}
int index1 = (Integer)args[0];
int index2 = index1;
if (args.length >= 2) {
if (args[1] instanceof Integer) {
index2 = (Integer)args[1];
} else {
return reportAtError(CME_ERROR_TEXT_HAS_INVALID_CHARS);
}
}
// Process
pbe.mPhonebookCursor.moveToPosition(index1 - 1);
AtCommandResult result = new AtCommandResult(AtCommandResult.OK);
while (index1 <= index2) {
String number = pbe.mPhonebookCursor.getString(pbe.mCursorNumberColumn);
String name = null;
int type = -1; // will show up as unknown
if (pbe.mCursorNameColumn == -1) {
// Do a caller ID lookup
Cursor c = mPhone.getContext().getContentResolver().query(
Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, number),
new String[] {Phones.NAME, Phones.TYPE}, null, null, null);
if (c != null) {
if (c.moveToFirst()) {
name = c.getString(0);
type = c.getInt(1);
}
c.close();
}
} else {
name = pbe.mPhonebookCursor.getString(pbe.mCursorNameColumn);
}
if (TextUtils.isEmpty(name)) {
name = "unknown";
} else {
name = name.trim();
if (name.length() > 28) name = name.substring(0, 28);
if (pbe.mCursorNumberTypeColumn != -1) {
type = pbe.mPhonebookCursor.getInt(pbe.mCursorNumberTypeColumn);
}
name = name + "/" + getPhoneType(type);
}
int regionType = PhoneNumberUtils.toaFromString(number);
number = number.trim();
if (number.length() > 30) number = number.substring(0, 30);
result.addResponse("+CPBR: " + index1 + ",\"" + number + "\"," +
regionType + ",\"" + name + "\"");
if (pbe.mPhonebookCursor.moveToNext() == false) {
break;
}
index1++;
}
return result;
} else {
if (DBG) log("No phone book entries for " + mSelectedPB);
return new AtCommandResult(AtCommandResult.OK);
}
}
@Override
public AtCommandResult handleTestCommand() {
// Obtain the number of calls according to the
// phone book type. We save the result and just return the number
// of entries in the respective list.
if (mSelectedPB == null) {
return reportAtError(CME_ERROR_OPERATION_NOT_ALLOWED);
}
if (mSelectedPB.equals("\"SM\"")) {
return new AtCommandResult(AtCommandResult.OK);
}
if (!mPhoneBooks.containsKey(mSelectedPB)) {
mSelectedPB = null;
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
}
PhoneBookEntry pbe = mPhoneBooks.get(mSelectedPB);
int numEntries = 0;
if (!initPhoneBookEntry(mSelectedPB, pbe)) {
return reportAtError(CME_ERROR_OPERATION_NOT_SUPPORTED);
}
numEntries = pbe.mPhonebookCursor.getCount();
if (numEntries > 0) {
return new AtCommandResult("+CPBR: (1-" + numEntries + "),30,30");
}
return new AtCommandResult(AtCommandResult.OK);
}
});
}
private static final int START_CALL_TIMEOUT = 10000; // ms
private synchronized void waitForCallStart() {
mWaitingForCallStart = true;
Message msg = Message.obtain(mHandler, CHECK_CALL_STARTED);
mHandler.sendMessageDelayed(msg, START_CALL_TIMEOUT);
if (!mStartCallWakeLock.isHeld()) {
mStartCallWakeLock.acquire(START_CALL_TIMEOUT);
}
}
private synchronized void callStarted() {
if (mWaitingForCallStart) {
mWaitingForCallStart = false;
sendURC("OK");
if (mStartCallWakeLock.isHeld()) {
mStartCallWakeLock.release();
}
}
}
private DebugThread mDebugThread;
private boolean inDebug() {
return DBG && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE, false);
}
private boolean allowAudioAnytime() {
return inDebug() && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE_AUDIO_ANYTIME,
false);
}
private void startDebug() {
if (DBG && mDebugThread == null) {
mDebugThread = new DebugThread();
mDebugThread.start();
}
}
private void stopDebug() {
if (mDebugThread != null) {
mDebugThread.interrupt();
mDebugThread = null;
}
}
/** Debug thread to read debug properties - runs when debug.bt.hfp is true
* at the time a bluetooth handsfree device is connected. Debug properties
* are polled and mock updates sent every 1 second */
private class DebugThread extends Thread {
/** Turns on/off handsfree profile debugging mode */
private static final String DEBUG_HANDSFREE = "debug.bt.hfp";
/** Mock battery level change - use 0 to 5 */
private static final String DEBUG_HANDSFREE_BATTERY = "debug.bt.hfp.battery";
/** Mock no cellular service when false */
private static final String DEBUG_HANDSFREE_SERVICE = "debug.bt.hfp.service";
/** Mock cellular roaming when true */
private static final String DEBUG_HANDSFREE_ROAM = "debug.bt.hfp.roam";
/** false to true transition will force an audio (SCO) connection to
* be established. true to false will force audio to be disconnected
*/
private static final String DEBUG_HANDSFREE_AUDIO = "debug.bt.hfp.audio";
/** true allows incoming SCO connection out of call.
*/
private static final String DEBUG_HANDSFREE_AUDIO_ANYTIME = "debug.bt.hfp.audio_anytime";
/** Mock signal strength change in ASU - use 0 to 31 */
private static final String DEBUG_HANDSFREE_SIGNAL = "debug.bt.hfp.signal";
/** Debug AT+CLCC: print +CLCC result */
private static final String DEBUG_HANDSFREE_CLCC = "debug.bt.hfp.clcc";
@Override
public void run() {
boolean oldService = true;
boolean oldRoam = false;
boolean oldAudio = false;
while (!isInterrupted() && inDebug()) {
int batteryLevel = SystemProperties.getInt(DEBUG_HANDSFREE_BATTERY, -1);
if (batteryLevel >= 0 && batteryLevel <= 5) {
Intent intent = new Intent();
intent.putExtra("level", batteryLevel);
intent.putExtra("scale", 5);
mPhoneState.updateBatteryState(intent);
}
boolean serviceStateChanged = false;
if (SystemProperties.getBoolean(DEBUG_HANDSFREE_SERVICE, true) != oldService) {
oldService = !oldService;
serviceStateChanged = true;
}
if (SystemProperties.getBoolean(DEBUG_HANDSFREE_ROAM, false) != oldRoam) {
oldRoam = !oldRoam;
serviceStateChanged = true;
}
if (serviceStateChanged) {
Bundle b = new Bundle();
b.putString("state", oldService ? "IN_SERVICE" : "OUT_OF_SERVICE");
b.putBoolean("roaming", oldRoam);
mPhoneState.updateServiceState(true, ServiceState.newFromBundle(b));
}
if (SystemProperties.getBoolean(DEBUG_HANDSFREE_AUDIO, false) != oldAudio) {
oldAudio = !oldAudio;
if (oldAudio) {
audioOn();
} else {
audioOff();
}
}
int signalLevel = SystemProperties.getInt(DEBUG_HANDSFREE_SIGNAL, -1);
if (signalLevel >= 0 && signalLevel <= 31) {
Intent intent = new Intent();
intent.putExtra("asu", signalLevel);
mPhoneState.updateSignalState(intent);
}
if (SystemProperties.getBoolean(DEBUG_HANDSFREE_CLCC, false)) {
log(getClccResult().toString());
}
try {
sleep(1000); // 1 second
} catch (InterruptedException e) {
break;
}
}
}
};
private void log(String msg) {
Log.d(TAG, msg);
}
}