diff options
Diffstat (limited to 'src/com/android/bluetooth/sap')
-rw-r--r-- | src/com/android/bluetooth/sap/SapMessage.java | 1245 | ||||
-rw-r--r-- | src/com/android/bluetooth/sap/SapRilReceiver.java | 248 | ||||
-rw-r--r-- | src/com/android/bluetooth/sap/SapServer.java | 787 | ||||
-rw-r--r-- | src/com/android/bluetooth/sap/SapService.java | 795 |
4 files changed, 3075 insertions, 0 deletions
diff --git a/src/com/android/bluetooth/sap/SapMessage.java b/src/com/android/bluetooth/sap/SapMessage.java new file mode 100644 index 000000000..71e6a9743 --- /dev/null +++ b/src/com/android/bluetooth/sap/SapMessage.java @@ -0,0 +1,1245 @@ +package com.android.bluetooth.sap; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidParameterException; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.android.btsap.SapApi; +import org.android.btsap.SapApi.*; +import com.google.protobuf.micro.*; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * SapMessage is used for incoming and outgoing messages. + * + * For incoming messages + * + */ +public class SapMessage { + + public static final String TAG = "SapMessage"; + public static final boolean DEBUG = SapService.DEBUG; + public static final boolean VERBOSE = SapService.VERBOSE; + public static final boolean TEST = SapService.PTS_TEST; + + /* Message IDs - SAP specification */ + public static final int ID_CONNECT_REQ = 0x00; + public static final int ID_CONNECT_RESP = 0x01; + + public static final int ID_DISCONNECT_REQ = 0x02; + public static final int ID_DISCONNECT_RESP = 0x03; + public static final int ID_DISCONNECT_IND = 0x04; + + public static final int ID_TRANSFER_APDU_REQ = 0x05; + public static final int ID_TRANSFER_APDU_RESP = 0x06; + + public static final int ID_TRANSFER_ATR_REQ = 0x07; + public static final int ID_TRANSFER_ATR_RESP = 0x08; + + public static final int ID_POWER_SIM_OFF_REQ = 0x09; + public static final int ID_POWER_SIM_OFF_RESP = 0x0A; + + public static final int ID_POWER_SIM_ON_REQ = 0x0B; + public static final int ID_POWER_SIM_ON_RESP = 0x0C; + + public static final int ID_RESET_SIM_REQ = 0x0D; + public static final int ID_RESET_SIM_RESP = 0x0E; + + public static final int ID_TRANSFER_CARD_READER_STATUS_REQ = 0x0F; + public static final int ID_TRANSFER_CARD_READER_STATUS_RESP = 0x10; + + public static final int ID_STATUS_IND = 0x11; + public static final int ID_ERROR_RESP = 0x12; + + public static final int ID_SET_TRANSPORT_PROTOCOL_REQ = 0x13; + public static final int ID_SET_TRANSPORT_PROTOCOL_RESP = 0x14; + + /* Message IDs - RIL specific unsolicited */ + public static final int ID_RIL_BASE = 0x100; // First RIL message id + public static final int ID_RIL_UNSOL_CONNECTED = 0x100; // RIL_UNSOL_RIL_CONNECTED + public static final int ID_RIL_UNSOL_DISCONNECT_IND = 0x102; // A disconnect ind from RIL will be converted after handled locally + public static final int ID_RIL_UNKNOWN = 0x1ff; // All others + + /* Message IDs - RIL specific solicited */ + public static final int ID_RIL_GET_SIM_STATUS_REQ = 0x200; // RIL_REQUEST_GET_SIM_STATUS + /* Test signals used to set the reference ril in test mode */ + public static final int ID_RIL_SIM_ACCESS_TEST_REQ = 0x201; // RIL_REQUEST_SIM_ACCESS_TEST + public static final int ID_RIL_SIM_ACCESS_TEST_RESP = 0x202; // response for RIL_REQUEST_SIM_ACCESS_TEST + + /* Parameter IDs and lengths */ + public static final int PARAM_MAX_MSG_SIZE_ID = 0x00; + public static final int PARAM_MAX_MSG_SIZE_LENGTH = 2; + + public static final int PARAM_CONNECTION_STATUS_ID = 0x01; + public static final int PARAM_CONNECTION_STATUS_LENGTH = 1; + + public static final int PARAM_RESULT_CODE_ID = 0x02; + public static final int PARAM_RESULT_CODE_LENGTH = 1; + + public static final int PARAM_DISCONNECT_TYPE_ID = 0x03; + public static final int PARAM_DISCONNECT_TYPE_LENGTH = 1; + + public static final int PARAM_COMMAND_APDU_ID = 0x04; + + public static final int PARAM_COMMAND_APDU7816_ID = 0x10; + + public static final int PARAM_RESPONSE_APDU_ID = 0x05; + + public static final int PARAM_ATR_ID = 0x06; + + public static final int PARAM_CARD_READER_STATUS_ID = 0x07; + public static final int PARAM_CARD_READER_STATUS_LENGTH = 1; + + public static final int PARAM_STATUS_CHANGE_ID = 0x08; + public static final int PARAM_STATUS_CHANGE_LENGTH = 1; + + public static final int PARAM_TRANSPORT_PROTOCOL_ID = 0x09; + public static final int PARAM_TRANSPORT_PROTOCOL_LENGTH = 1; + + /* Result codes */ + public static final int RESULT_OK = 0x00; + public static final int RESULT_ERROR_NO_REASON = 0x01; + public static final int RESULT_ERROR_CARD_NOT_ACCESSIBLE = 0x02; + public static final int RESULT_ERROR_CARD_POWERED_OFF = 0x03; + public static final int RESULT_ERROR_CARD_REMOVED = 0x04; + public static final int RESULT_ERROR_CARD_POWERED_ON = 0x05; + public static final int RESULT_ERROR_DATA_NOT_AVAILABLE = 0x06; + public static final int RESULT_ERROR_NOT_SUPPORTED = 0x07; + + /* Connection Status codes */ + public static final int CON_STATUS_OK = 0x00; + public static final int CON_STATUS_ERROR_CONNECTION = 0x01; + public static final int CON_STATUS_ERROR_MAX_MSG_SIZE_UNSUPPORTED = 0x02; + public static final int CON_STATUS_ERROR_MAX_MSG_SIZE_TOO_SMALL = 0x03; + public static final int CON_STATUS_OK_ONGOING_CALL = 0x04; + + /* Disconnection type */ + public static final int DISC_GRACEFULL = 0x00; + public static final int DISC_IMMEDIATE = 0x01; + public static final int DISC_FORCED = 0x100; // Used internal only + public static final int DISC_RFCOMM = 0x101; // Used internal only + + /* Status Change */ + public static final int STATUS_UNKNOWN_ERROR = 0x00; + public static final int STATUS_CARD_RESET = 0x01; + public static final int STATUS_CARD_NOT_ACCESSIBLE = 0x02; + public static final int STATUS_CARD_REMOVED = 0x03; + public static final int STATUS_CARD_INSERTED = 0x04; + public static final int STATUS_RECOVERED = 0x05; + + /* Transport Protocol */ + public static final int TRANS_PROTO_T0 = 0x00; + public static final int TRANS_PROTO_T1 = 0x01; + + /* Test Mode */ + public static final int TEST_MODE_DISABLE = 0x00; + public static final int TEST_MODE_ENABLE = 0x01; + + /* Used to detect uninitialized values */ + public static final int INVALID_VALUE = -1; + + /* Stuff related to communicating with rild-bt */ + static final int RESPONSE_SOLICITED = 0; + static final int RESPONSE_UNSOLICITED = 1; + static AtomicInteger sNextSerial = new AtomicInteger(1); + + // Map<rilSerial, RequestType> - HashTable is synchronized + private static Map<Integer, Integer> sOngoingRequests = new Hashtable<Integer, Integer>(); + private boolean mSendToRil = false; // set to true for messages that needs to go to through the RIL + private boolean mClearRilQueue = false; /* set to true for messages that needs to cause the + sOngoingRequests to be cleared. */ + + /* Instance members */ + private int mMsgType = INVALID_VALUE; // The SAP message ID + + private int mMaxMsgSize = INVALID_VALUE; + private int mConnectionStatus = INVALID_VALUE; + private int mResultCode = INVALID_VALUE; + private int mDisconnectionType = INVALID_VALUE; + private int mCardReaderStatus = INVALID_VALUE; + private int mStatusChange = INVALID_VALUE; + private int mTransportProtocol = INVALID_VALUE; + private int mTestMode = INVALID_VALUE; + private byte[] mApdu = null; + private byte[] mApdu7816 = null; + private byte[] mApduResp = null; + private byte[] mAtr = null; + + /** + * Create a SapMessage + * @param msgType the SAP message type + */ + public SapMessage(int msgType){ + this.mMsgType = msgType; + } + + private static void resetPendingRilMessages() { + int numMessages = sOngoingRequests.size(); + if(numMessages != 0) { + Log.w(TAG, "Clearing message queue with size: " + numMessages); + sOngoingRequests.clear(); + } + } + + public static int getNumPendingRilMessages() { + return sOngoingRequests.size(); + } + + public int getMsgType() { + return mMsgType; + } + + public void setMsgType(int msgType) { + this.mMsgType = msgType; + } + + public int getMaxMsgSize() { + return mMaxMsgSize; + } + + public void setMaxMsgSize(int maxMsgSize) { + this.mMaxMsgSize = maxMsgSize; + } + + public int getConnectionStatus() { + return mConnectionStatus; + } + + public void setConnectionStatus(int connectionStatus) { + this.mConnectionStatus = connectionStatus; + } + + public int getResultCode() { + return mResultCode; + } + + public void setResultCode(int resultCode) { + this.mResultCode = resultCode; + } + + public int getDisconnectionType() { + return mDisconnectionType; + } + + public void setDisconnectionType(int disconnectionType) { + this.mDisconnectionType = disconnectionType; + } + + public int getCardReaderStatus() { + return mCardReaderStatus; + } + + public void setCardReaderStatus(int cardReaderStatus) { + this.mCardReaderStatus = cardReaderStatus; + } + + public int getStatusChange() { + return mStatusChange; + } + + public void setStatusChange(int statusChange) { + this.mStatusChange = statusChange; + } + + public int getTransportProtocol() { + return mTransportProtocol; + } + + public void setTransportProtocol(int transportProtocol) { + this.mTransportProtocol = transportProtocol; + } + + public byte[] getApdu() { + return mApdu; + } + + public void setApdu(byte[] apdu) { + this.mApdu = apdu; + } + + public byte[] getApdu7816() { + return mApdu7816; + } + + public void setApdu7816(byte[] apdu) { + this.mApdu7816 = apdu; + } + + public byte[] getApduResp() { + return mApduResp; + } + + public void setApduResp(byte[] apduResp) { + this.mApduResp = apduResp; + } + + public byte[] getAtr() { + return mAtr; + } + + public void setAtr(byte[] atr) { + this.mAtr = atr; + } + + public boolean getSendToRil() { + return mSendToRil; + } + + public void setSendToRil(boolean sendToRil) { + this.mSendToRil = sendToRil; + } + + public boolean getClearRilQueue() { + return mClearRilQueue; + } + + public void setClearRilQueue(boolean clearRilQueue) { + this.mClearRilQueue = clearRilQueue; + } + + public int getTestMode() { + return mTestMode; + } + + public void setTestMode(int testMode) { + this.mTestMode = testMode; + } + + private int getParamCount() { + int paramCount = 0; + if(mMaxMsgSize != INVALID_VALUE) + paramCount++; + if(mConnectionStatus != INVALID_VALUE) + paramCount++; + if(mResultCode != INVALID_VALUE) + paramCount++; + if(mDisconnectionType != INVALID_VALUE) + paramCount++; + if(mCardReaderStatus != INVALID_VALUE) + paramCount++; + if(mStatusChange != INVALID_VALUE) + paramCount++; + if(mTransportProtocol != INVALID_VALUE) + paramCount++; + if(mApdu != null) + paramCount++; + if(mApdu7816 != null) + paramCount++; + if(mApduResp != null) + paramCount++; + if(mAtr != null) + paramCount++; + return paramCount; + } + + /** + * Construct a SapMessage based on the incoming rfcomm request. + * @param requestType The type of the request + * @param is the input stream to read the data from + * @return the resulting message, or null if an error occurs + */ + @SuppressWarnings("unused") + public static SapMessage readMessage(int requestType, InputStream is) { + SapMessage newMessage = new SapMessage(requestType); + + /* Read in all the parameters (if any) */ + int paramCount; + try { + paramCount = is.read(); + skip(is, 2); // Skip the 2 padding bytes + if(paramCount > 0) { + if(VERBOSE) Log.i(TAG, "Parsing message with paramCount: " + paramCount); + if(newMessage.parseParameters(paramCount, is) == false) + return null; + } + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + if(DEBUG) Log.i(TAG, "readMessage() Read message: " + getMsgTypeName(requestType)); + + /* Validate parameters */ + switch(requestType) { + case ID_CONNECT_REQ: + if(newMessage.getMaxMsgSize() == INVALID_VALUE) { + Log.e(TAG, "Missing MaxMsgSize parameter in CONNECT_REQ"); + return null; + } + break; + case ID_TRANSFER_APDU_REQ: + if(newMessage.getApdu() == null && + newMessage.getApdu7816() == null) { + Log.e(TAG, "Missing Apdu parameter in TRANSFER_APDU_REQ"); + return null; + } + newMessage.setSendToRil(true); + break; + case ID_SET_TRANSPORT_PROTOCOL_REQ: + if(newMessage.getTransportProtocol() == INVALID_VALUE) { + Log.e(TAG, "Missing TransportProtocol parameter in SET_TRANSPORT_PROTOCOL_REQ"); + return null; + } + newMessage.setSendToRil(true); + break; + case ID_TRANSFER_ATR_REQ: /* No params */ + case ID_POWER_SIM_OFF_REQ: /* No params */ + case ID_POWER_SIM_ON_REQ: /* No params */ + case ID_RESET_SIM_REQ: /* No params */ + case ID_TRANSFER_CARD_READER_STATUS_REQ: /* No params */ + newMessage.setSendToRil(true); + break; + case ID_DISCONNECT_REQ: /* No params */ + break; + default: + if(TEST == false) { + Log.e(TAG, "Unknown request type"); + return null; + } + } + return newMessage; + } + + /** + * Blocking read of an entire array of data. + * @param is the input stream to read from + * @param buffer the buffer to read into - the length of the buffer will + * determine how many bytes will be read. + */ + private static void read(InputStream is, byte[] buffer) throws IOException { + int bytesToRead = buffer.length; + int bytesRead = 0; + int tmpBytesRead; + while (bytesRead < bytesToRead) { + tmpBytesRead = is.read(buffer, bytesRead, bytesToRead-bytesRead); + if(tmpBytesRead == -1) + throw new IOException("EOS reached while reading a byte array."); + else + bytesRead += tmpBytesRead; + } + } + + /** + * Skip a number of bytes in an InputStream. + * @param is the input stream + * @param count the number of bytes to skip + * @throws IOException In case of reaching EOF or a stream error + */ + private static void skip(InputStream is, int count) throws IOException { + for(int i = 0; i < count; i++) { + is.read(); // Do not use the InputStream.skip as it fails for some stream types + } + } + + /** + * Read the parameters from the stream and update the relevant members. + * This function will ensure that all parameters are read from the stream, even + * if an error is detected. + * @param count the number of parameters to read + * @param is the input stream + * @return True if all parameters were successfully parsed, False if an error were detected. + * @throws IOException + */ + private boolean parseParameters(int count, InputStream is) throws IOException { + int paramId; + int paramLength; + boolean success = true; + for(int i = 0; i < count; i++) { + paramId = is.read(); + is.read(); // Skip the reserved byte + paramLength = is.read(); + paramLength = paramLength << 8 | is.read(); + if(VERBOSE) Log.i(TAG, "parsing paramId: " + paramId + " with length: " + paramLength); + switch(paramId) { + case PARAM_MAX_MSG_SIZE_ID: + if(paramLength != PARAM_MAX_MSG_SIZE_LENGTH) { + Log.e(TAG, "Received PARAM_MAX_MSG_SIZE with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mMaxMsgSize = is.read(); + mMaxMsgSize = mMaxMsgSize << 8 | is.read(); + skip(is, 4 - PARAM_MAX_MSG_SIZE_LENGTH); + } + break; + case PARAM_COMMAND_APDU_ID: + mApdu = new byte[paramLength]; + read(is, mApdu); + skip(is, 4 - (paramLength % 4)); + break; + case PARAM_COMMAND_APDU7816_ID: + mApdu7816 = new byte[paramLength]; + read(is, mApdu7816); + skip(is, 4 - (paramLength % 4)); + break; + case PARAM_TRANSPORT_PROTOCOL_ID: + if(paramLength != PARAM_TRANSPORT_PROTOCOL_LENGTH) { + Log.e(TAG, "Received PARAM_TRANSPORT_PROTOCOL with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mTransportProtocol = is.read(); + skip(is, 4 - PARAM_TRANSPORT_PROTOCOL_LENGTH); + } + break; + case PARAM_CONNECTION_STATUS_ID: + // not needed - server -> client + if(TEST) { + if(paramLength != PARAM_CONNECTION_STATUS_LENGTH) { + Log.e(TAG, "Received PARAM_CONNECTION_STATUS with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mConnectionStatus = is.read(); + skip(is, 4 - PARAM_CONNECTION_STATUS_LENGTH); + } + break; + } // Fall through if TEST == false + case PARAM_CARD_READER_STATUS_ID: + // not needed - server -> client + if(TEST) { + if(paramLength != PARAM_CARD_READER_STATUS_LENGTH) { + Log.e(TAG, "Received PARAM_CARD_READER_STATUS with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mCardReaderStatus = is.read(); + skip(is, 4 - PARAM_CARD_READER_STATUS_LENGTH); + } + break; + } // Fall through if TEST == false + case PARAM_STATUS_CHANGE_ID: + // not needed - server -> client + if(TEST) { + if(paramLength != PARAM_STATUS_CHANGE_LENGTH) { + Log.e(TAG, "Received PARAM_STATUS_CHANGE with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mStatusChange = is.read(); + skip(is, 4 - PARAM_STATUS_CHANGE_LENGTH); + } + break; + } // Fall through if TEST == false + case PARAM_RESULT_CODE_ID: + // not needed - server -> client + if(TEST) { + if(paramLength != PARAM_RESULT_CODE_LENGTH) { + Log.e(TAG, "Received PARAM_RESULT_CODE with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mResultCode = is.read(); + skip(is, 4 - PARAM_RESULT_CODE_LENGTH); + } + break; + } // Fall through if TEST == false + case PARAM_DISCONNECT_TYPE_ID: + // not needed - server -> client + if(TEST) { + if(paramLength != PARAM_DISCONNECT_TYPE_LENGTH) { + Log.e(TAG, "Received PARAM_DISCONNECT_TYPE_ID with wrong length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + success = false; + } else { + mDisconnectionType = is.read(); + skip(is, 4 - PARAM_DISCONNECT_TYPE_LENGTH); + } + break; + } // Fall through if TEST == false + case PARAM_RESPONSE_APDU_ID: + // not needed - server -> client + if(TEST) { + mApduResp = new byte[paramLength]; + read(is, mApduResp); + skip(is, 4 - (paramLength % 4)); + break; + } // Fall through if TEST == false + case PARAM_ATR_ID: + // not needed - server -> client + if(TEST) { + mAtr = new byte[paramLength]; + read(is, mAtr); + skip(is, 4 - (paramLength % 4)); + break; + } // Fall through if TEST == false + default: + Log.e(TAG, "Received unknown parameter ID: " + paramId + " length: " + + paramLength + " skipping this parameter."); + skip(is, paramLength + (4 - (paramLength % 4))); + } + } + return success; + } + + /** + * Writes a single value parameter of 1 or 2 bytes in length. + * @param os The BufferedOutputStream to write to. + * @param id The Parameter ID + * @param value The parameter value + * @param length The length of the parameter value + * @throws IOException if the write to os fails + */ + private static void writeParameter(OutputStream os, int id, int value, int length) + throws IOException { + + /* Parameter Header*/ + os.write(id); + os.write(0); + os.write(0); + os.write(length); + + switch(length) { + case 1: + os.write(value & 0xff); + os.write(0); // Padding + os.write(0); // Padding + os.write(0); // padding + break; + case 2: + os.write((value >> 8) & 0xff); + os.write(value & 0xff); + os.write(0); // Padding + os.write(0); // padding + break; + default: + throw new IOException("Unable to write value of length: " + length); + } + } + + /** + * Writes a byte[] parameter of any length. + * @param os The BufferedOutputStream to write to. + * @param id The Parameter ID + * @param value The byte array to write, the length will be extracted from the array. + * @throws IOException if the write to os fails + */ + private static void writeParameter(OutputStream os, int id, byte[] value) throws IOException { + + /* Parameter Header*/ + os.write(id); + os.write(0); // reserved + os.write((value.length >> 8) & 0xff); + os.write(value.length & 0xff); + + /* Payload */ + os.write(value); + for(int i = 0, n = 4 - (value.length % 4) ; i < n; i++) { + os.write(0); // Padding + } + } + + public void write(OutputStream os) throws IOException { + /* Write the header */ + os.write(mMsgType); + os.write(getParamCount()); + os.write(0); // padding + os.write(0); // padding + + /* write the parameters */ + if(mConnectionStatus != INVALID_VALUE) { + writeParameter(os,PARAM_CONNECTION_STATUS_ID, mConnectionStatus, + PARAM_CONNECTION_STATUS_LENGTH); + } + if(mMaxMsgSize != INVALID_VALUE) { + writeParameter(os, PARAM_MAX_MSG_SIZE_ID, mMaxMsgSize, + PARAM_MAX_MSG_SIZE_LENGTH); + } + if(mResultCode != INVALID_VALUE) { + writeParameter(os, PARAM_RESULT_CODE_ID, mResultCode, + PARAM_RESULT_CODE_LENGTH); + } + if(mDisconnectionType != INVALID_VALUE && TEST) { + writeParameter(os, PARAM_DISCONNECT_TYPE_ID, mDisconnectionType, + PARAM_DISCONNECT_TYPE_LENGTH); + } + if(mCardReaderStatus != INVALID_VALUE) { + writeParameter(os, PARAM_CARD_READER_STATUS_ID, mCardReaderStatus, + PARAM_CARD_READER_STATUS_LENGTH); + } + if(mStatusChange != INVALID_VALUE) { + writeParameter(os, PARAM_STATUS_CHANGE_ID, mStatusChange, + PARAM_STATUS_CHANGE_LENGTH); + } + if(mTransportProtocol != INVALID_VALUE && TEST) { + writeParameter(os, PARAM_TRANSPORT_PROTOCOL_ID, mTransportProtocol, + PARAM_TRANSPORT_PROTOCOL_LENGTH); + } + if(mApdu != null && TEST) { + writeParameter(os, PARAM_COMMAND_APDU_ID, mApdu); + } + if(mApdu7816 != null && TEST) { + writeParameter(os, PARAM_COMMAND_APDU7816_ID, mApdu7816); + } + if(mApduResp != null) { + writeParameter(os, PARAM_RESPONSE_APDU_ID, mApduResp); + } + if(mAtr != null) { + writeParameter(os, PARAM_ATR_ID, mAtr); + } + } + + /*************************************************************************** + * RILD Interface message conversion functions. + ***************************************************************************/ + + /** + * We use this function to + * @param length + * @param rawOut + * @throws IOException + */ + private void writeLength(int length, CodedOutputStreamMicro out) throws IOException { + byte[] dataLength = new byte[4]; + dataLength[0] = dataLength[1] = 0; + dataLength[2] = (byte)((length >> 8) & 0xff); + dataLength[3] = (byte)((length) & 0xff); + out.writeRawBytes(dataLength); + } + /** + * Write this SAP message as a rild compatible protobuf message. + * Solicited Requests are formed as follows: + * int type - the rild-bt type + * int serial - an number incrementing for each message. + */ + public void writeReqToStream(CodedOutputStreamMicro out) throws IOException { + + int rilSerial = sNextSerial.getAndIncrement(); + SapApi.MsgHeader msg = new MsgHeader(); + /* Common variables for all requests */ + msg.setToken(rilSerial); + msg.setType(SapApi.REQUEST); + msg.setError(SapApi.RIL_E_UNUSED); + + switch(mMsgType) { + case ID_CONNECT_REQ: + { + SapApi.RIL_SIM_SAP_CONNECT_REQ reqMsg = new RIL_SIM_SAP_CONNECT_REQ(); + reqMsg.setMaxMessageSize(mMaxMsgSize); + msg.setId(SapApi.RIL_SIM_SAP_CONNECT); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_DISCONNECT_REQ: + { + SapApi.RIL_SIM_SAP_DISCONNECT_REQ reqMsg = new RIL_SIM_SAP_DISCONNECT_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_DISCONNECT); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_TRANSFER_APDU_REQ: + { + SapApi.RIL_SIM_SAP_APDU_REQ reqMsg = new RIL_SIM_SAP_APDU_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_APDU); + if(mApdu != null) { + reqMsg.setType(SapApi.RIL_SIM_SAP_APDU_REQ.RIL_TYPE_APDU); + reqMsg.setCommand(ByteStringMicro.copyFrom(mApdu)); + } else if (mApdu7816 != null) { + reqMsg.setType(SapApi.RIL_SIM_SAP_APDU_REQ.RIL_TYPE_APDU7816); + reqMsg.setCommand(ByteStringMicro.copyFrom(mApdu7816)); + } else { + Log.e(TAG, "Missing Apdu parameter in TRANSFER_APDU_REQ"); + throw new IllegalArgumentException(); + } + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_SET_TRANSPORT_PROTOCOL_REQ: + { + SapApi.RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_REQ reqMsg = + new RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_SET_TRANSFER_PROTOCOL); + + if(mTransportProtocol == TRANS_PROTO_T0) { + reqMsg.setProtocol(SapApi.RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_REQ.t0); + } else if(mTransportProtocol == TRANS_PROTO_T1) { + reqMsg.setProtocol(SapApi.RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_REQ.t1); + } else { + Log.e(TAG, "Missing or invalid TransportProtocol parameter in"+ + " SET_TRANSPORT_PROTOCOL_REQ: "+ mTransportProtocol ); + throw new IllegalArgumentException(); + } + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_TRANSFER_ATR_REQ: + { + SapApi.RIL_SIM_SAP_TRANSFER_ATR_REQ reqMsg = new RIL_SIM_SAP_TRANSFER_ATR_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_TRANSFER_ATR); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_POWER_SIM_OFF_REQ: + { + SapApi.RIL_SIM_SAP_POWER_REQ reqMsg = new RIL_SIM_SAP_POWER_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_POWER); + reqMsg.setState(false); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_POWER_SIM_ON_REQ: + { + SapApi.RIL_SIM_SAP_POWER_REQ reqMsg = new RIL_SIM_SAP_POWER_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_POWER); + reqMsg.setState(true); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_RESET_SIM_REQ: + { + SapApi.RIL_SIM_SAP_RESET_SIM_REQ reqMsg = new RIL_SIM_SAP_RESET_SIM_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_RESET_SIM); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + case ID_TRANSFER_CARD_READER_STATUS_REQ: + { + SapApi.RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_REQ reqMsg = + new RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_REQ(); + msg.setId(SapApi.RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS); + msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray())); + writeLength(msg.getSerializedSize(), out); + msg.writeTo(out); + break; + } + default: + if(TEST == false) { + Log.e(TAG, "Unknown request type"); + throw new IllegalArgumentException(); + } + } + /* Update the ongoing requests queue */ + if(mClearRilQueue == true) { + resetPendingRilMessages(); + } + // No need to synchronize this, as the HashList is already doing this. + sOngoingRequests.put(rilSerial, mMsgType); + out.flush(); + } + + public static SapMessage newInstance(MsgHeader msg) throws IOException { + return new SapMessage(msg); + } + + private SapMessage(MsgHeader msg) throws IOException { + // All header members are "required" hence the hasXxxx() is not needed for those + try{ + switch(msg.getType()){ + case SapApi.UNSOL_RESPONSE: + createUnsolicited(msg); + break; + case SapApi.RESPONSE: + createSolicited(msg); + break; + default: + throw new IOException("Wrong msg header received: Type: " + msg.getType()); + } + } catch (InvalidProtocolBufferMicroException e) { + Log.w(TAG, "Error occured parsing a RIL message", e); + throw new IOException("Error occured parsing a RIL message"); + } + } + + private void createUnsolicited(MsgHeader msg) + throws IOException, InvalidProtocolBufferMicroException { + switch(msg.getId()) { +// TODO: +// Not sure when we use these? case RIL_UNSOL_RIL_CONNECTED: +// if(VERBOSE) Log.i(TAG, "RIL_UNSOL_RIL_CONNECTED received, ignoring"); +// msgType = ID_RIL_UNSOL_CONNECTED; +// break; + case SapApi.RIL_SIM_SAP_STATUS: + { + if(VERBOSE) Log.i(TAG, "RIL_SIM_SAP_STATUS_IND received"); + RIL_SIM_SAP_STATUS_IND indMsg = + RIL_SIM_SAP_STATUS_IND.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_STATUS_IND; + if(indMsg.hasStatusChange()) { + setStatusChange(indMsg.getStatusChange()); + if(VERBOSE) Log.i(TAG, "RIL_UNSOL_SIM_SAP_STATUS_IND received value = " + mStatusChange); + } else { + if(VERBOSE) Log.i(TAG, "Wrong number of parameters in SAP_STATUS_IND, ignoring..."); + mMsgType = ID_RIL_UNKNOWN; + } + break; + } + case SapApi.RIL_SIM_SAP_DISCONNECT: + { + if(VERBOSE) Log.i(TAG, "RIL_SIM_SAP_DISCONNECT_IND received"); + + RIL_SIM_SAP_DISCONNECT_IND indMsg = + RIL_SIM_SAP_DISCONNECT_IND.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_RIL_UNSOL_DISCONNECT_IND; // don't use ID_DISCONNECT_IND; + if(indMsg.hasDisconnectType()) { + setDisconnectionType(indMsg.getDisconnectType()); + if(VERBOSE) Log.i(TAG, "RIL_UNSOL_SIM_SAP_STATUS_IND received value = " + + mDisconnectionType); + } else { + if(VERBOSE) Log.i(TAG, "Wrong number of parameters in SAP_STATUS_IND, ignoring..."); + mMsgType = ID_RIL_UNKNOWN; + } + break; + } + default: + if(VERBOSE) Log.i(TAG, "Unused unsolicited message received, ignoring: " + msg.getId()); + mMsgType = ID_RIL_UNKNOWN; + } + } + + private void createSolicited(MsgHeader msg) throws IOException, + InvalidProtocolBufferMicroException{ + /* re-evaluate if we should just ignore these - we could simply catch the exception? */ + if(msg.hasToken() == false) throw new IOException("Token is missing"); + if(msg.hasError() == false) throw new IOException("Error code is missing"); + int serial = msg.getToken(); + int error = msg.getError(); + Integer reqType = null; + reqType = sOngoingRequests.remove(serial); + if(VERBOSE) Log.i(TAG, "RIL SOLICITED serial: " + serial + ", error: " + error + + " SapReqType: " + ((reqType== null)?"null":getMsgTypeName(reqType))); + + if(reqType == null) { + /* This can happen if we get a resp. for a canceled request caused by a power off, + * reset or disconnect + */ + Log.w(TAG, "Solicited response received on a command not initiated - ignoring."); + return; + } + mResultCode = mapRilErrorCode(error); + + switch(reqType) { + case ID_CONNECT_REQ: + { + RIL_SIM_SAP_CONNECT_RSP resMsg = + RIL_SIM_SAP_CONNECT_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_CONNECT_RESP; + if(resMsg.hasMaxMessageSize()) { + mMaxMsgSize = resMsg.getMaxMessageSize(); + + } + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_CONNECT_RSP.RIL_E_SUCCESS: + mConnectionStatus = CON_STATUS_OK; + break; + case RIL_SIM_SAP_CONNECT_RSP.RIL_E_SAP_CONNECT_OK_CALL_ONGOING: + mConnectionStatus = CON_STATUS_OK_ONGOING_CALL; + break; + case RIL_SIM_SAP_CONNECT_RSP.RIL_E_SAP_CONNECT_FAILURE: + mConnectionStatus = CON_STATUS_ERROR_CONNECTION; + break; + case RIL_SIM_SAP_CONNECT_RSP.RIL_E_SAP_MSG_SIZE_TOO_LARGE: + mConnectionStatus = CON_STATUS_ERROR_MAX_MSG_SIZE_UNSUPPORTED; + break; + case RIL_SIM_SAP_CONNECT_RSP.RIL_E_SAP_MSG_SIZE_TOO_SMALL: + mConnectionStatus = CON_STATUS_ERROR_MAX_MSG_SIZE_TOO_SMALL; + break; + default: + mConnectionStatus = CON_STATUS_ERROR_CONNECTION; // Cannot happen! + break; + } + mResultCode = INVALID_VALUE; + if(VERBOSE) Log.v(TAG, " ID_CONNECT_REQ: mMaxMsgSize: " + mMaxMsgSize + + " mConnectionStatus: " + mConnectionStatus); + break; + } + case ID_DISCONNECT_REQ: + mMsgType = ID_DISCONNECT_RESP; + mResultCode = INVALID_VALUE; + break; + case ID_TRANSFER_APDU_REQ: + { + RIL_SIM_SAP_APDU_RSP resMsg = + RIL_SIM_SAP_APDU_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_TRANSFER_APDU_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_APDU_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + /* resMsg.getType is unused as the client knows the type of request used. */ + if(resMsg.hasApduResponse()){ + mApduResp = resMsg.getApduResponse().toByteArray(); + } + break; + case RIL_SIM_SAP_APDU_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_APDU_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_APDU_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + case RIL_SIM_SAP_APDU_RSP.RIL_E_SIM_NOT_READY: + mResultCode = RESULT_ERROR_CARD_REMOVED; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_SET_TRANSPORT_PROTOCOL_REQ: + { + RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP resMsg = + RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.parseFrom( + msg.getPayload().toByteArray()); + mMsgType = ID_SET_TRANSPORT_PROTOCOL_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + break; + case RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + case RIL_SIM_SAP_SET_TRANSFER_PROTOCOL_RSP.RIL_E_SIM_NOT_READY: + mResultCode = RESULT_ERROR_CARD_REMOVED; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_TRANSFER_ATR_REQ: + { + RIL_SIM_SAP_TRANSFER_ATR_RSP resMsg = + RIL_SIM_SAP_TRANSFER_ATR_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType =ID_TRANSFER_ATR_RESP; + if(resMsg.hasAtr()) { + mAtr = resMsg.getAtr().toByteArray(); + } + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + break; + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_SIM_ALREADY_POWERED_ON: + mResultCode = RESULT_ERROR_CARD_POWERED_ON; + break; + case RIL_SIM_SAP_TRANSFER_ATR_RSP.RIL_E_SIM_DATA_NOT_AVAILABLE: + mResultCode = RESULT_ERROR_DATA_NOT_AVAILABLE; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_POWER_SIM_OFF_REQ: + { + RIL_SIM_SAP_POWER_RSP resMsg = + RIL_SIM_SAP_POWER_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_POWER_SIM_OFF_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_POWER_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ALREADY_POWERED_ON: + mResultCode = RESULT_ERROR_CARD_POWERED_ON; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_POWER_SIM_ON_REQ: + { + RIL_SIM_SAP_POWER_RSP resMsg = + RIL_SIM_SAP_POWER_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_POWER_SIM_ON_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_POWER_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + case RIL_SIM_SAP_POWER_RSP.RIL_E_SIM_ALREADY_POWERED_ON: + mResultCode = RESULT_ERROR_CARD_POWERED_ON; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_RESET_SIM_REQ: + { + RIL_SIM_SAP_RESET_SIM_RSP resMsg = + RIL_SIM_SAP_RESET_SIM_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_RESET_SIM_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_RESET_SIM_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + break; + case RIL_SIM_SAP_RESET_SIM_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_RESET_SIM_RSP.RIL_E_SIM_ABSENT: + mResultCode = RESULT_ERROR_CARD_NOT_ACCESSIBLE; + break; + case RIL_SIM_SAP_RESET_SIM_RSP.RIL_E_SIM_ALREADY_POWERED_OFF: + mResultCode = RESULT_ERROR_CARD_POWERED_OFF; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + case ID_TRANSFER_CARD_READER_STATUS_REQ: + { + RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_RSP resMsg = + RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_RSP.parseFrom(msg.getPayload().toByteArray()); + mMsgType = ID_TRANSFER_CARD_READER_STATUS_RESP; + switch(resMsg.getResponse()) { + case RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_RSP.RIL_E_SUCCESS: + mResultCode = RESULT_OK; + if(resMsg.hasCardReaderStatus()) { + mCardReaderStatus = resMsg.getCardReaderStatus(); + } else { + mResultCode = RESULT_ERROR_DATA_NOT_AVAILABLE; + } + break; + case RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_RSP.RIL_E_GENERIC_FAILURE: + mResultCode = RESULT_ERROR_NO_REASON; + break; + case RIL_SIM_SAP_TRANSFER_CARD_READER_STATUS_RSP.RIL_E_SIM_DATA_NOT_AVAILABLE: + mResultCode = RESULT_ERROR_DATA_NOT_AVAILABLE; + break; + default: + mResultCode = RESULT_ERROR_NO_REASON; + break; + } + break; + } + + case ID_RIL_SIM_ACCESS_TEST_REQ: // TODO: implement in RILD + mMsgType = ID_RIL_SIM_ACCESS_TEST_RESP; + break; + default: + Log.e(TAG, "Unknown request type: " + reqType); + + } + } + + + + /* Map from RIL header error codes to SAP error codes */ + private static int mapRilErrorCode(int rilErrorCode) { + switch(rilErrorCode) { + case SapApi.RIL_E_SUCCESS: + return RESULT_OK; + case SapApi.RIL_E_CANCELLED: + return RESULT_ERROR_NO_REASON; + case SapApi.RIL_E_GENERIC_FAILURE: + return RESULT_ERROR_NO_REASON; + case SapApi.RIL_E_RADIO_NOT_AVAILABLE: + return RESULT_ERROR_CARD_NOT_ACCESSIBLE; + case SapApi.RIL_E_INVALID_PARAMETER: + return RESULT_ERROR_NO_REASON; + case SapApi.RIL_E_REQUEST_NOT_SUPPORTED: + return RESULT_ERROR_NOT_SUPPORTED; + default: + return RESULT_ERROR_NO_REASON; + } + } + + + + public static String getMsgTypeName(int msgType) { + if(TEST || VERBOSE) { + switch (msgType) + { + case ID_CONNECT_REQ: return "ID_CONNECT_REQ"; + case ID_CONNECT_RESP: return "ID_CONNECT_RESP"; + case ID_DISCONNECT_REQ: return "ID_DISCONNECT_REQ"; + case ID_DISCONNECT_RESP: return "ID_DISCONNECT_RESP"; + case ID_DISCONNECT_IND: return "ID_DISCONNECT_IND"; + case ID_TRANSFER_APDU_REQ: return "ID_TRANSFER_APDU_REQ"; + case ID_TRANSFER_APDU_RESP: return "ID_TRANSFER_APDU_RESP"; + case ID_TRANSFER_ATR_REQ: return "ID_TRANSFER_ATR_REQ"; + case ID_TRANSFER_ATR_RESP: return "ID_TRANSFER_ATR_RESP"; + case ID_POWER_SIM_OFF_REQ: return "ID_POWER_SIM_OFF_REQ"; + case ID_POWER_SIM_OFF_RESP: return "ID_POWER_SIM_OFF_RESP"; + case ID_POWER_SIM_ON_REQ: return "ID_POWER_SIM_ON_REQ"; + case ID_POWER_SIM_ON_RESP: return "ID_POWER_SIM_ON_RESP"; + case ID_RESET_SIM_REQ: return "ID_RESET_SIM_REQ"; + case ID_RESET_SIM_RESP: return "ID_RESET_SIM_RESP"; + case ID_TRANSFER_CARD_READER_STATUS_REQ: return "ID_TRANSFER_CARD_READER_STATUS_REQ"; + case ID_TRANSFER_CARD_READER_STATUS_RESP: return "ID_TRANSFER_CARD_READER_STATUS_RESP"; + case ID_STATUS_IND: return "ID_STATUS_IND"; + case ID_ERROR_RESP: return "ID_ERROR_RESP"; + case ID_SET_TRANSPORT_PROTOCOL_REQ: return "ID_SET_TRANSPORT_PROTOCOL_REQ"; + case ID_SET_TRANSPORT_PROTOCOL_RESP: return "ID_SET_TRANSPORT_PROTOCOL_RESP"; + case ID_RIL_UNSOL_CONNECTED: return "ID_RIL_UNSOL_CONNECTED"; + case ID_RIL_UNKNOWN: return "ID_RIL_UNKNOWN"; + case ID_RIL_GET_SIM_STATUS_REQ: return "ID_RIL_GET_SIM_STATUS_REQ"; + case ID_RIL_SIM_ACCESS_TEST_REQ: return "ID_RIL_SIM_ACCESS_TEST_REQ"; + case ID_RIL_SIM_ACCESS_TEST_RESP: return "ID_RIL_SIM_ACCESS_TEST_RESP"; + default: return "Unknown Message Type (" + msgType + ")"; + } + } else { + return null; + } + } +} diff --git a/src/com/android/bluetooth/sap/SapRilReceiver.java b/src/com/android/bluetooth/sap/SapRilReceiver.java new file mode 100644 index 000000000..17d4fa15b --- /dev/null +++ b/src/com/android/bluetooth/sap/SapRilReceiver.java @@ -0,0 +1,248 @@ +package com.android.bluetooth.sap; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.android.btsap.SapApi.MsgHeader; + +import com.google.protobuf.micro.CodedInputStreamMicro; +import com.google.protobuf.micro.CodedOutputStreamMicro; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +public class SapRilReceiver implements Runnable { + + private static final String TAG = "SapRilReceiver"; + public static final boolean DEBUG = true; + public static final boolean VERBOSE = true; + + private static final String SOCKET_NAME_RIL_BT = "sap_uim_socket1"; + // match with constant in ril.cpp - as in RIL.java + private static final int SOCKET_OPEN_RETRY_MILLIS = 4 * 1000; + + LocalSocket mSocket = null; + CodedOutputStreamMicro mRilBtOutStream = null; + InputStream mRilBtInStream = null; + private Handler mSapServerMsgHandler = null; + + public static final int RIL_MAX_COMMAND_BYTES = (8 * 1024); + byte[] buffer = new byte[RIL_MAX_COMMAND_BYTES]; + + public SapRilReceiver(Handler SapServerMsgHandler) { + mSapServerMsgHandler = SapServerMsgHandler; + } + + /** + * Open the RIL-BT socket in rild. Will continuously try to open the BT socket until + * success. (Based on the approach used to open the rild socket in telephony) + * @return The socket handle + */ + public static LocalSocket openRilBtSocket() { + int retryCount = 0; + LocalSocket rilSocket = null; + + for (;;) { + LocalSocketAddress address; + + try { + rilSocket = new LocalSocket(); + address = new LocalSocketAddress(SOCKET_NAME_RIL_BT, + LocalSocketAddress.Namespace.RESERVED); + rilSocket.connect(address); + break; // Socket opened + } catch (IOException ex){ + try { + if (rilSocket != null) { + rilSocket.close(); + } + } catch (IOException ex2) { + //ignore failure to close after failure to connect + } + + // don't print an error message after the the first time + // or after the 8th time + if (retryCount == 8) { + Log.e (TAG, + "Couldn't find '" + SOCKET_NAME_RIL_BT + + "' socket after " + retryCount + + " times, continuing to retry silently"); + } else if (retryCount > 0 && retryCount < 8) { + Log.i (TAG, + "Couldn't find '" + SOCKET_NAME_RIL_BT + + "' socket; retrying after timeout"); + if(VERBOSE) Log.w(TAG, ex); + } + + try { + Thread.sleep(SOCKET_OPEN_RETRY_MILLIS); + } catch (InterruptedException er) { + } + + retryCount++; + continue; + } + } + return rilSocket; + } + + + public CodedOutputStreamMicro getRilBtOutStream() { + return mRilBtOutStream; + } + + private void onConnectComplete() { + if(mSapServerMsgHandler != null) + mSapServerMsgHandler.sendEmptyMessage(SapServer.SAP_MSG_RIL_CONNECT); + } + + /** + * This will terminate the SapRilReceiver thread, by closing the RIL-BT in-/output + * streams. + */ + public void shutdown() { + if(DEBUG) Log.i(TAG, "shutdown()"); + + /* On Android you need to close the IOstreams using Socket.shutdown* + * The IOstream close must not be used, as it some how decouples the + * stream from the socket, and when the socket is closed, the pending + * reads never return nor throw and exception. + * Hence here we use the shutdown method: */ + if(mSocket != null) { + try { + mSocket.shutdownOutput(); + } catch (IOException e) {} + try { + mSocket.shutdownInput(); + } catch (IOException e) {} + try { + mSocket.close(); + } catch (IOException ex) { + if(VERBOSE) Log.e(TAG,"Uncaught exception", ex); + } + mSocket = null; + } + } + + /** + * Read the message into buffer + * @param is + * @param buffer + * @return the length of the message + * @throws IOException + */ + private static int readMessage(InputStream is, byte[] buffer) throws IOException { + int countRead; + int offset; + int remaining; + int messageLength; + + // Read in the length of the message + offset = 0; + remaining = 4; + do { + countRead = is.read(buffer, offset, remaining); + + if (countRead < 0 ) { + Log.e(TAG, "Hit EOS reading message length"); + return -1; + } + + offset += countRead; + remaining -= countRead; + } while (remaining > 0); + + messageLength = ((buffer[0] & 0xff) << 24) + | ((buffer[1] & 0xff) << 16) + | ((buffer[2] & 0xff) << 8) + | (buffer[3] & 0xff); + if(VERBOSE) Log.e(TAG,"Message length found to be: "+messageLength); + // Read the message + offset = 0; + remaining = messageLength; + do { + countRead = is.read(buffer, offset, remaining); + + if (countRead < 0 ) { + Log.e(TAG, "Hit EOS reading message. messageLength=" + messageLength + + " remaining=" + remaining); + return -1; + } + + offset += countRead; + remaining -= countRead; + } while (remaining > 0); + + return messageLength; + } + + /** + * The RIL reader thread. Will handle open of the RIL-BT socket, and notify + * SapServer when done. + */ + @Override + public void run() { + + try { + int length = 0; + if(VERBOSE) Log.i(TAG, "Starting RilBtReceiverThread..."); + + mSocket = openRilBtSocket(); + mRilBtInStream = mSocket.getInputStream(); + mRilBtOutStream = CodedOutputStreamMicro.newInstance(mSocket.getOutputStream()); + + // Notify the SapServer that we have connected to the RilBtSocket + onConnectComplete(); + + // The main loop - read messages and forward to SAP server + for (;;) { + SapMessage sapMsg = null; + MsgHeader rilMsg; + + + if(VERBOSE) Log.i(TAG, "Waiting for incoming message..."); + length = readMessage(mRilBtInStream, buffer); + CodedInputStreamMicro msgStream = CodedInputStreamMicro.newInstance(buffer, 0, length); + rilMsg = MsgHeader.parseFrom(msgStream); + + if(VERBOSE) Log.i(TAG, "Message received."); + + sapMsg = SapMessage.newInstance(rilMsg); + + if(sapMsg != null && sapMsg.getMsgType() != SapMessage.INVALID_VALUE) + { + if(sapMsg.getMsgType() < SapMessage.ID_RIL_BASE) { + sendClientMessage(sapMsg); + } else { + sendRilIndMessage(sapMsg); + } + } // else simply ignore it + } + } catch (IOException e) { + shutdown(); /* Only needed in case of a connection error */ + Log.i(TAG, "'" + SOCKET_NAME_RIL_BT + "' socket inputStream closed", e); + } + finally { + Log.i(TAG, "Disconnected from '" + SOCKET_NAME_RIL_BT + "' socket"); + } + } + + /** + * Send message to the Sap Server Handler Thread + * @param sapMsg The message to send + */ + private void sendClientMessage(SapMessage sapMsg) { + Message newMsg = mSapServerMsgHandler.obtainMessage(SapServer.SAP_MSG_RFC_REPLY, sapMsg); + mSapServerMsgHandler.sendMessage(newMsg); + } + + private void sendRilIndMessage(SapMessage sapMsg) { + Message newMsg = mSapServerMsgHandler.obtainMessage(SapServer.SAP_MSG_RIL_IND, sapMsg); + mSapServerMsgHandler.sendMessage(newMsg); + } + +} diff --git a/src/com/android/bluetooth/sap/SapServer.java b/src/com/android/bluetooth/sap/SapServer.java new file mode 100644 index 000000000..4a1998feb --- /dev/null +++ b/src/com/android/bluetooth/sap/SapServer.java @@ -0,0 +1,787 @@ +package com.android.bluetooth.sap; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; + +import com.android.bluetooth.R; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SyncResult; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.SystemClock; +import android.telephony.TelephonyManager; +import android.util.Log; +//import com.android.internal.telephony.RIL; +import com.google.protobuf.micro.CodedOutputStreamMicro; + + +/** + * The SapServer uses two threads, one for reading messages from the RFCOMM socket and + * one for writing the responses. + * Incoming requests are decoded in the "main" SapServer, by the decoder build into SapMEssage. + * The relevant RIL calls are made from the message handler thread through the rild-bt socket. + * The RIL replies are read in the SapRilReceiver, and passed to the SapServer message handler + * to be written to the RFCOMM socket. + * All writes to the RFCOMM and rild-bt socket must be synchronized, hence to write e.g. an error + * response, send a message to the Sap Handler thread. (There are helper functions to do this) + * Communication to the RIL is through an intent, and a BroadcastReceiver. + */ +public class SapServer extends Thread implements Callback { + private static final String TAG = "SapServer"; + private static final String TAG_HANDLER = "SapServerHandler"; + public static final boolean DEBUG = SapService.DEBUG; + public static final boolean VERBOSE = SapService.VERBOSE; + public static final boolean PTS_TEST = SapService.PTS_TEST; + + private enum SAP_STATE { + DISCONNECTED, CONNECTING, CONNECTING_CALL_ONGOING, CONNECTED, + CONNECTED_BUSY, DISCONNECTING; + } + + private SAP_STATE mState = SAP_STATE.DISCONNECTED; + + private Context mContext = null; + /* RFCOMM socket I/O streams */ + private BufferedOutputStream mRfcommOut = null; + private BufferedInputStream mRfcommIn = null; + /* The RIL output stream - the input stream is owned by the SapRilReceiver object */ + private CodedOutputStreamMicro mRilBtOutStream = null; + /* References to the SapRilReceiver object */ + private SapRilReceiver mRilBtReceiver = null; + private Thread mRilBtReceiverThread = null; + /* The message handler members */ + private Handler mSapHandler = null; + private HandlerThread mHandlerThread = null; + /* Reference to the SAP service - which created this instance of the SAP server */ + private Handler mSapServiceHandler = null; + + /* flag for when user forces disconnect of rfcomm */ + private boolean mIsLocalInitDisconnect = false; + private CountDownLatch mDeinitSignal = new CountDownLatch(1); + + /* Message ID's handled by the message handler */ + public static final int SAP_MSG_RFC_REPLY = 0x00; + public static final int SAP_MSG_RIL_CONNECT = 0x01; + public static final int SAP_MSG_RIL_REQ = 0x02; + public static final int SAP_MSG_RIL_IND = 0x04; + + public static final String SAP_DISCONNECT_ACTION = "com.android.bluetooth.sap.action.DISCONNECT_ACTION"; + public static final String SAP_DISCONNECT_TYPE_EXTRA = "com.android.bluetooth.sap.extra.DISCONNECT_TYPE"; + public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; + private static final int DISCONNECT_TIMEOUT_IMMEDIATE = 5000; /* ms */ + private static final int DISCONNECT_TIMEOUT_RFCOMM = 2000; /* ms */ + private PendingIntent pDiscIntent = null; // Holds a reference to disconnect timeout intents + /* These are just used to evaluate the maxMessageSize which we are able to handle in the SAP profile. + * The RIL may set other limits, but this will be handled by the SAP connect request send ti the RIL */ +// TODO: REMOVE private static final int MAX_MAX_MESSAGE_SIZE = SapRilReceiver.RIL_MAX_COMMAND_BYTES; + /* We store the mMaxMessageSize, as we need a copy of it when the init. sequence completes */ + private int mMaxMsgSize = 0; + /* keep track of the current RIL test mode */ + private int mTestMode = SapMessage.INVALID_VALUE; // used to set the RIL in test mode + + + /** + * SapServer constructor + * @param serviceHandler The handler to send a SapService.MSG_SERVERSESSION_CLOSE when closing + * @param inStream The socket input stream + * @param outStream The socket output stream + */ + public SapServer(Handler serviceHandler, Context context, InputStream inStream, OutputStream outStream) { + mContext = context; + mSapServiceHandler = serviceHandler; + + /* Open in- and output streams */ + mRfcommIn = new BufferedInputStream(inStream); + mRfcommOut = new BufferedOutputStream(outStream); + + /* Register for phone state change and the RIL cfm message */ + IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + filter.addAction(SAP_DISCONNECT_ACTION); + mContext.registerReceiver(mIntentReceiver, filter); + } + + /** + * This handles the response from RIL. + */ + BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if(intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) { + if(VERBOSE) Log.i(TAG, "ACTION_PHONE_STATE_CHANGED intent received in state " + + mState.name() + + "PhoneState: " + + intent.getStringExtra(TelephonyManager.EXTRA_STATE)); + if(mState == SAP_STATE.CONNECTING_CALL_ONGOING) { + String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); + if(state != null) { + if(state.equals(TelephonyManager.EXTRA_STATE_IDLE)) { + if(DEBUG) Log.i(TAG, "sending RIL.ACTION_RIL_RECONNECT_OFF_REQ intent"); + // TODO: Send connect request to RIL + SapMessage fakeConReq = new SapMessage(SapMessage.ID_CONNECT_REQ); + fakeConReq.setMaxMsgSize(mMaxMsgSize); + onConnectRequest(fakeConReq); + } + } + } + } else if (intent.getAction().equals(SAP_DISCONNECT_ACTION) + && mState != SAP_STATE.DISCONNECTED + && mState != SAP_STATE.DISCONNECTING ) { + + int disconnectType = intent.getIntExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA,SapMessage.DISC_GRACEFULL); + Log.v(TAG, " - Received SAP_DISCONNECT_ACTION type: " + disconnectType); + sendDisconnectInd(disconnectType); + + } else { + Log.w(TAG, "RIL-BT received unexpected Intent: " + intent.getAction()); + } + } + }; + + /** + * Set RIL driver in test mode - only possible if SapMessage is build with TEST == true + * The value set by this function will take effect at the next connect request received + * in DISCONNECTED state. + * @param testMode Use SapMessage.TEST_MODE_XXX + */ + public void setTestMode(int testMode) { + if(SapMessage.TEST) { + mTestMode = testMode; + } + } + + private void sendDisconnectInd(int discType) { + if(VERBOSE) Log.v(TAG, "in sendDisconnectInd()"); + + if(discType != SapMessage.DISC_FORCED){ + if(VERBOSE) Log.d(TAG, "Sending disconnect ("+discType+") indication to client"); + /* Send disconnect to client */ + SapMessage discInd = new SapMessage(SapMessage.ID_DISCONNECT_IND); + discInd.setDisconnectionType(discType); + sendClientMessage(discInd); + + /* Handle local disconnect procedures */ + if (discType == SapMessage.DISC_GRACEFULL) + { + /* Update the notification to allow the user to initiate a force disconnect */ + setNotification(SapMessage.DISC_IMMEDIATE, PendingIntent.FLAG_CANCEL_CURRENT); + + } else if (discType == SapMessage.DISC_IMMEDIATE){ + /* Request an immediate disconnect, but start a timer to force disconnect if the client + * do not obey our request. */ + startDisconnectTimer(SapMessage.DISC_FORCED, DISCONNECT_TIMEOUT_IMMEDIATE); + } + + } else { + SapMessage msg = new SapMessage(SapMessage.ID_DISCONNECT_REQ); + /* Force disconnect of RFCOMM - but first we need to clean up. */ + clearPendingRilResponses(msg); + + // We simply need to forward to RIL, but not change state to busy - hence send and set message to null. + changeState(SAP_STATE.DISCONNECTING); + sendRilThreadMessage(msg); + mIsLocalInitDisconnect = true; + } + } + + + void setNotification(int type, int flags) + { + String title, text, button, ticker; + Notification notification; + if(VERBOSE) Log.i(TAG, "setNotification type: " + type); + /* put notification up for the user to be able to disconnect from the client*/ + Intent sapDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION); + if(type == SapMessage.DISC_GRACEFULL){ + title = mContext.getString(R.string.bluetooth_sap_notif_title); + button = mContext.getString(R.string.bluetooth_sap_notif_disconnect_button); + text = mContext.getString(R.string.bluetooth_sap_notif_message); + ticker = mContext.getString(R.string.bluetooth_sap_notif_ticker); + }else{ + title = mContext.getString(R.string.bluetooth_sap_notif_title); + button = mContext.getString(R.string.bluetooth_sap_notif_force_disconnect_button); + text = mContext.getString(R.string.bluetooth_sap_notif_disconnecting); + ticker = mContext.getString(R.string.bluetooth_sap_notif_ticker); + } + if(!PTS_TEST) + { + sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, type); + PendingIntent pIntentDisconnect = PendingIntent.getBroadcast(mContext, type, sapDisconnectIntent,flags); + notification = new Notification.Builder(mContext).setOngoing(true) + .addAction(android.R.drawable.stat_sys_data_bluetooth, button, pIntentDisconnect) + .setContentTitle(title) + .setTicker(ticker) + .setContentText(text) + .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) + .setAutoCancel(false) + .setPriority(Notification.PRIORITY_MAX) + .setOnlyAlertOnce(true) + .build(); + }else{ + + sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, SapMessage.DISC_GRACEFULL); + Intent sapForceDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION); + sapForceDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, SapMessage.DISC_IMMEDIATE); + PendingIntent pIntentDisconnect = PendingIntent.getBroadcast(mContext, SapMessage.DISC_GRACEFULL, sapDisconnectIntent,flags); + PendingIntent pIntentForceDisconnect = PendingIntent.getBroadcast(mContext, SapMessage.DISC_IMMEDIATE, sapForceDisconnectIntent,flags); + notification = new Notification.Builder(mContext).setOngoing(true) + .addAction(android.R.drawable.stat_sys_data_bluetooth, mContext.getString(R.string.bluetooth_sap_notif_disconnect_button), pIntentDisconnect) + .addAction(android.R.drawable.stat_sys_data_bluetooth, mContext.getString(R.string.bluetooth_sap_notif_force_disconnect_button), pIntentForceDisconnect) + .setContentTitle(title) + .setTicker(ticker) + .setContentText(text) + .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) + .setAutoCancel(false) + .setPriority(Notification.PRIORITY_MAX) + .setOnlyAlertOnce(true) + .build(); + } + + + notification.flags |= Notification.FLAG_NO_CLEAR |Notification.FLAG_ONLY_ALERT_ONCE; /* cannot be set with the builder */ + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.notify(NOTIFICATION_ID, notification); + } + /** + * The SapServer RFCOMM reader thread. Sets up the handler thread and handle + * all read from the RFCOMM in-socket. This thread also handle writes to the RIL socket. + */ + @Override + public void run() { + try { + /* SAP is not time critical, hence lowering priority to ensure critical tasks are executed + * in a timely manner. */ + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); + + /* Start the SAP message handler thread */ + mHandlerThread = new HandlerThread("SapServerHandler", android.os.Process.THREAD_PRIORITY_BACKGROUND); + mHandlerThread.start(); + Looper sapLooper = mHandlerThread.getLooper(); /* This will return when the looper is ready */ + mSapHandler = new Handler(sapLooper, this); + + mRilBtReceiver = new SapRilReceiver(mSapHandler); + mRilBtReceiverThread = new Thread(mRilBtReceiver, "RilBtReceiver"); + setNotification(SapMessage.DISC_GRACEFULL,0); + boolean done = false; + while (!done) { + if(VERBOSE) Log.i(TAG, "Waiting for incomming RFCOMM message..."); + int requestType = mRfcommIn.read(); + if(requestType == -1) { + done = true; // EOF reached + } else { + SapMessage msg = SapMessage.readMessage(requestType, mRfcommIn); + if(msg != null && mState != SAP_STATE.DISCONNECTING) + { + switch (requestType) { + case SapMessage.ID_CONNECT_REQ: + if(VERBOSE) Log.d(TAG, "CONNECT_REQ - MaxMsgSize: " + msg.getMaxMsgSize()); + onConnectRequest(msg); + msg = null; /* don't send ril connect yet */ + break; + case SapMessage.ID_DISCONNECT_REQ: /* No params */ + /* + * 1) send RIL_REQUEST_SIM_SAP_DISCONNECT (block for all incoming requests, as they are not + * allowed, don't even send an error_resp) + * 2) on response disconnect ril socket. + * 3) when disconnected send RIL.ACTION_RIL_RECONNECT_OFF_REQ + * 4) on RIL.ACTION_RIL_RECONNECT_CFM send SAP_DISCONNECT_RESP to client. + * 5) Start RFCOMM disconnect timer + * 6.a) on rfcomm disconnect: cancel timer and initiate cleanup + * 6.b) on rfcomm disc. timeout: close socket-streams and initiate cleanup */ + if(VERBOSE) Log.d(TAG, "DISCONNECT_REQ"); + + clearPendingRilResponses(msg); + // We simply need to forward to RIL, but not change state to busy - hence send and set message to null. + changeState(SAP_STATE.DISCONNECTING); /* do not enter disconnecting state for disconnect_ind + - we need to obtain normal operation until disconnect is received. */ + sendRilThreadMessage(msg); + msg = null; // don't send twice + /*cancel the timer for the hard-disconnect intent*/ + stopDisconnectTimer(); + break; + case SapMessage.ID_POWER_SIM_OFF_REQ: // Fall through + case SapMessage.ID_RESET_SIM_REQ: + /* Forward these to the RIL regardless of the state, and clear any pending resp */ + clearPendingRilResponses(msg); + break; + default: + /* remaining cases just needs to be forwarded to the RIL unless we are in busy state. */ + if(mState != SAP_STATE.CONNECTED) { + Log.w(TAG, "Message received in STATE != CONNECTED - state = " + mState.name()); + /* We shall only handle one request at the time, hence return error */ + SapMessage atrReply = new SapMessage(SapMessage.ID_ERROR_RESP); + sendClientMessage(atrReply); + msg = null; + } + } + + if(msg != null && msg.getSendToRil() == true) { + changeState(SAP_STATE.CONNECTED_BUSY); + sendRilThreadMessage(msg); + } + + } else { /* An unknown message or in disconnecting state - send error indication */ + Log.e(TAG, "Unable to parse message."); + SapMessage atrReply = new SapMessage(SapMessage.ID_ERROR_RESP); + sendClientMessage(atrReply); + } + } + } // end while + } catch (NullPointerException e) { + Log.w(TAG, e); + } catch (IOException e) { + /* This is expected during shutdown */ + Log.i(TAG, "IOException received, this is probably a shutdown signal, cleaning up..."); + } catch (Exception e) { + /* TODO: Change to the needed Exception types when done testing */ + Log.w(TAG, e); + } finally { + // Do cleanup even if an exception occurs + stopDisconnectTimer(); + /* In case of e.g. a RFCOMM close while connected: + * - Initiate a FORCED shutdown + * - Wait for RIL deinit to complete + */ + if(mState != SAP_STATE.DISCONNECTED) { + if(mState != SAP_STATE.DISCONNECTING && + mIsLocalInitDisconnect != true) { + sendDisconnectInd(SapMessage.DISC_FORCED); + } + if(DEBUG) Log.i(TAG, "Waiting for deinit to complete"); + try { + mDeinitSignal.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupt received while waitinf for de-init to complete", e); + } + } + mContext.unregisterReceiver(mIntentReceiver); + if(mHandlerThread != null) try { + mHandlerThread.quit(); + mHandlerThread.join(); + } catch (InterruptedException e) {} + if(mRilBtReceiverThread != null) try { + mRilBtReceiverThread.join(); + } catch (InterruptedException e) {} + + if(mRfcommIn != null) try { + if(VERBOSE) Log.i(TAG, "Closing mRfcommIn..."); + mRfcommIn.close(); + } catch (IOException e) {} + + if(mRfcommOut != null) try { + if(VERBOSE) Log.i(TAG, "Closing mRfcommOut..."); + mRfcommOut.close(); + } catch (IOException e) {} + + if (mSapServiceHandler != null) { + Message msg = Message.obtain(mSapServiceHandler); + msg.what = SapService.MSG_SERVERSESSION_CLOSE; + msg.sendToTarget(); + if (DEBUG) Log.d(TAG, "MSG_SERVERSESSION_CLOSE sent out."); + } + Log.i(TAG, "All done exiting thread..."); + } + } + + + /** + * This function needs to determine: + * - if the maxMsgSize is acceptable - else reply CON_STATUS_ERROR_MAX_MSG_SIZE_UNSUPPORTED + new maxMsgSize if too big + * - connect to the RIL-BT socket + * - if a call is ongoing reply CON_STATUS_OK_ONGOING_CALL. + * - if all ok, just respond CON_STATUS_OK. + * + * @param msg the incoming SapMessage + */ + private void onConnectRequest(SapMessage msg) { + SapMessage reply = new SapMessage(SapMessage.ID_CONNECT_RESP); + + if(mState == SAP_STATE.CONNECTING) { + /* A connect request might have been rejected because of maxMessageSize negotiation, and + * this is a new connect request. Simply forward to RIL, and stay in connecting state. + * */ + reply = null; + sendRilMessage(msg); + stopDisconnectTimer(); + + } else if(mState != SAP_STATE.DISCONNECTED && mState != SAP_STATE.CONNECTING_CALL_ONGOING) { + reply.setConnectionStatus(SapMessage.CON_STATUS_ERROR_CONNECTION); + } else { + // Store the MaxMsgSize for future use + mMaxMsgSize = msg.getMaxMsgSize(); + /* All parameters OK, examine if a call is ongoing and start the RIL-BT listener thread. */ + if (isCallOngoing() == true) { + /* If a call is ongoing we set the state, inform the SAP client and wait for a state change + * intent from the TelephonyManager with state IDLE. */ + changeState(SAP_STATE.CONNECTING_CALL_ONGOING); + reply.setConnectionStatus(SapMessage.CON_STATUS_OK_ONGOING_CALL); + } else { + /* no call is ongoing, initiate the connect sequence: + * 1) Start the SapRilReceiver thread (open the rild-bt socket) + * 2) Send a RIL_SIM_SAP_CONNECT request to RILD + * 3) Send a RIL_SIM_RESET request and a connect confirm to the SAP client */ + changeState(SAP_STATE.CONNECTING); + if(mRilBtReceiverThread != null) { + /* Open the RIL socket, and wait for the complete message: SAP_MSG_RIL_CONNECT */ + mRilBtReceiverThread.start(); + // Don't send reply yet + reply = null; + } else { + reply = new SapMessage(SapMessage.ID_CONNECT_RESP); + reply.setConnectionStatus(SapMessage.CON_STATUS_ERROR_CONNECTION); + sendClientMessage(reply); + } + } + } + if(reply != null) + sendClientMessage(reply); + } + + private void clearPendingRilResponses(SapMessage msg) { + if(mState == SAP_STATE.CONNECTED_BUSY) { + msg.setClearRilQueue(true); + } + } + /** + * Send RFCOMM message to the Sap Server Handler Thread + * @param sapMsg The message to send + */ + private void sendClientMessage(SapMessage sapMsg) { + Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RFC_REPLY, sapMsg); + mSapHandler.sendMessage(newMsg); + } + + /** + * Send a RIL message to the SapServer message handler thread + * @param sapMsg + */ + private void sendRilThreadMessage(SapMessage sapMsg) { + Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RIL_REQ, sapMsg); + mSapHandler.sendMessage(newMsg); + } + + /** + * Examine if a call is ongoing, by asking the telephony manager + * @return false if the phone is IDLE (can be used for SAP), true otherwise. + */ + private boolean isCallOngoing() { + TelephonyManager tManager =(TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); + if(tManager.getCallState() == TelephonyManager.CALL_STATE_IDLE) { + return false; + } + return true; + } + + /** + * Change the SAP Server state. + * We add thread protection, as we access the state from two threads. + * @param newState + */ + private void changeState(SAP_STATE newState) { + if(DEBUG) Log.i(TAG_HANDLER,"Changing state from " + mState.name() + + " to " + newState.name()); + synchronized (this) { + mState = newState; + } + } + + + /************************************************************************* + * SAP Server Message Handler Thread Functions + *************************************************************************/ + + /** + * The SapServer message handler thread implements the SAP state machine. + * - Handle all outgoing communication to the out-socket. Either replies from the RIL or direct + * messages send from the SapServe (e.g. connect_resp). + * - Handle all outgoing communication to the RIL-BT socket. + * - Handle all replies from the RIL + */ + @Override + public boolean handleMessage(Message msg) { + if(VERBOSE) Log.i(TAG_HANDLER,"Handling message (ID: " + msg.what + "): " + getMessageName(msg.what)); + + SapMessage sapMsg = null; + + switch(msg.what) { + case SAP_MSG_RFC_REPLY: + sapMsg = (SapMessage) msg.obj; + handleRfcommReply(sapMsg); + break; + case SAP_MSG_RIL_CONNECT: + /* The connection to rild-bt have been established. Store the outStream handle + * and send the connect request. */ + mRilBtOutStream = mRilBtReceiver.getRilBtOutStream(); + if(mTestMode != SapMessage.INVALID_VALUE) { + SapMessage rilTestModeReq = new SapMessage(SapMessage.ID_RIL_SIM_ACCESS_TEST_REQ); + rilTestModeReq.setTestMode(mTestMode); + sendRilMessage(rilTestModeReq); + mTestMode = SapMessage.INVALID_VALUE; + } + SapMessage rilSapConnect = new SapMessage(SapMessage.ID_CONNECT_REQ); + rilSapConnect.setMaxMsgSize(mMaxMsgSize); + sendRilMessage(rilSapConnect); + break; + case SAP_MSG_RIL_REQ: + sapMsg = (SapMessage) msg.obj; + if(sapMsg != null) { + sendRilMessage(sapMsg); + } + break; + case SAP_MSG_RIL_IND: + sapMsg = (SapMessage) msg.obj; + handleRilInd(sapMsg); + break; + default: + /* Message not handled */ + return false; + } + return true; // Message handles + } + + /** + * Close the in/out rfcomm streams, to trigger a shutdown of the SapServer main thread. + * Use this after completing the deinit sequence. + */ + private void shutdown() { + + if(DEBUG) Log.i(TAG_HANDLER, "in Shutdown()"); + try { + mRfcommOut.close(); + } catch (IOException e) {} + try { + mRfcommIn.close(); + + } catch (IOException e) {} + mRfcommIn = null; + mRfcommOut = null; + stopDisconnectTimer(); + } + + private void startDisconnectTimer(int discType, int timeMs) { + + stopDisconnectTimer(); + synchronized (this) { + Intent sapDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION); + sapDisconnectIntent.putExtra(SAP_DISCONNECT_TYPE_EXTRA, discType); + AlarmManager alarmManager = + (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + pDiscIntent = PendingIntent.getBroadcast(mContext, + discType, + sapDisconnectIntent, PendingIntent.FLAG_CANCEL_CURRENT); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + timeMs, pDiscIntent); + + if(VERBOSE) Log.d(TAG_HANDLER, "Setting alarm for " + timeMs + + " ms to activate disconnect type " + discType); + } + } + + private void stopDisconnectTimer() { + synchronized (this) { + if(pDiscIntent != null) + { + AlarmManager alarmManager = + (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pDiscIntent); + pDiscIntent.cancel(); + if(VERBOSE) { + Log.d(TAG_HANDLER, "Canceling disconnect alarm"); + } + pDiscIntent = null; + } + } + } + + /** + * Here we handle the replies to the SAP client, normally forwarded directly from the RIL. + * We do need to handle some of the messages in the SAP profile, hence we look at the messages + * here before they go to the client + * @param sapMsg the message to send to the SAP client + */ + private void handleRfcommReply(SapMessage sapMsg) { + if(sapMsg != null) { + + if(DEBUG) Log.i(TAG_HANDLER, "handleRfcommReply() handling " + SapMessage.getMsgTypeName(sapMsg.getMsgType())); + + switch(sapMsg.getMsgType()) { + + case SapMessage.ID_CONNECT_RESP: + if (sapMsg.getConnectionStatus() == SapMessage.CON_STATUS_OK) { + // This is successful connect response from RIL/modem. + changeState(SAP_STATE.CONNECTED); + } else if(sapMsg.getConnectionStatus() == SapMessage.CON_STATUS_OK_ONGOING_CALL + && mState != SAP_STATE.CONNECTING_CALL_ONGOING) { + changeState(SAP_STATE.CONNECTING_CALL_ONGOING); + } else if(mState == SAP_STATE.CONNECTING_CALL_ONGOING) { + // Hold back the connect resp if a call was ongoing when the connect req was received. + if(VERBOSE) Log.i(TAG, "Hold back the connect resp, as a call was ongoing when the initial response were sent."); + sapMsg = null; + } else if(sapMsg.getConnectionStatus() != SapMessage.CON_STATUS_OK) { + /* Most likely the peer will try to connect again, hence we keep the connection to RIL open + * and stay in connecting state. + */ + // Start timer to do shutdown if a new connect request is not received in time + startDisconnectTimer(SapMessage.DISC_FORCED, DISCONNECT_TIMEOUT_RFCOMM); + } + break; + case SapMessage.ID_DISCONNECT_RESP: + if(mState == SAP_STATE.DISCONNECTING) { + /* Close the RIL-BT output Stream and signal to SapRilReceiver to close down the input stream. */ + if(DEBUG) Log.i(TAG, "ID_DISCONNECT_RESP received in SAP_STATE.DISCONNECTING" + + " - shutdown bt-ril "); + /* We need to close down the RilBtReceiverThread before signaling to RILJ + * that it can re-open the socket to RILC. */ + mRilBtReceiver.shutdown(); + mRilBtOutStream = null; + try { + mRilBtReceiverThread.join(); + } catch (InterruptedException e) { + Log.e(TAG_HANDLER, "Exception occured while waiting for thread to exit.", e); + } + mRilBtReceiverThread = null; + + // Wait for the thread to close, as we need the socket to be closed before signaling the RIL to reopen. + // Disconnecting + // Send the disconnect resp, and wait for the client to close the Rfcomm, but start a + // timeout timer, just to be sure. Use alarm, to ensure we wake the host to close the + // connection to minimize power consumption. + SapMessage disconnectResp = new SapMessage(SapMessage.ID_DISCONNECT_RESP); + changeState(SAP_STATE.DISCONNECTED); + sapMsg = disconnectResp; + //since we are disconnected we remove the notification + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(SapServer.NOTIFICATION_ID); + } else { /* DISCONNECTED */ + mDeinitSignal.countDown(); /* Signal deinit complete */ + if(mIsLocalInitDisconnect == true) { + if(VERBOSE) Log.i(TAG_HANDLER, "This is a FORCED disconnect."); + /* We needed to force the disconnect, hence no hope for the client to close + * the RFCOMM connection, hence we do it here. */ + shutdown(); + sapMsg = null; + } else { + /* The client must disconnect the RFCOMM, but in case it does not, we need to do it. + * We start an alarm, and if it triggers, we must send the MSG_SERVERSESSION_CLOSE */ + if(VERBOSE) Log.i(TAG_HANDLER, "This is a NORMAL disconnect."); + startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM); + } + } + break; + case SapMessage.ID_STATUS_IND: + /* Some car-kits only "likes" status indication when connected, hence discard + * any arriving outside this state */ + if(mState == SAP_STATE.DISCONNECTED || + mState == SAP_STATE.CONNECTING || + mState == SAP_STATE.DISCONNECTING) { + sapMsg = null; + } + break; + default: + // Nothing special, just send the message + } + } + + /* Update state variable based on the number of pending commands. We are only able to + * handle one request at the time, except from disconnect, sim off and sim reset. + * Hence if one of these are received while in busy state, we might have a crossing + * response, hence we must stay in BUSY state if we have an ongoing RIL request. */ + if(mState == SAP_STATE.CONNECTED_BUSY) { + if(SapMessage.getNumPendingRilMessages() == 0) { + changeState(SAP_STATE.CONNECTED); + } + } + + // This is the default case - just send the message to the SAP client. + if(sapMsg != null) + sendReply(sapMsg); + } + + private void handleRilInd(SapMessage sapMsg) { + if(sapMsg == null) + return; + + switch(sapMsg.getMsgType()) { + case SapMessage.ID_DISCONNECT_IND: + { + if(mState != SAP_STATE.DISCONNECTED && mState != SAP_STATE.DISCONNECTING){ + /* we only send disconnect indication to the client if we are actually connected*/ + SapMessage reply = new SapMessage(SapMessage.ID_DISCONNECT_IND); + reply.setDisconnectionType(sapMsg.getDisconnectionType()) ; + sendClientMessage(reply); + } else { + /* TODO: This was introduced to handle disconnect indication from RIL */ + sendDisconnectInd(sapMsg.getDisconnectionType()); + } + break; + } + + default: + if(DEBUG) Log.w(TAG_HANDLER,"Unhandled message - type: " + SapMessage.getMsgTypeName(sapMsg.getMsgType())); + } + } + + /** + * This is only to be called from the handlerThread, else use sendRilThreadMessage(); + * @param sapMsg + */ + private void sendRilMessage(SapMessage sapMsg) { + if(VERBOSE) Log.i(TAG_HANDLER, "sendRilMessage() - " + SapMessage.getMsgTypeName(sapMsg.getMsgType())); + try { + sapMsg.writeReqToStream(mRilBtOutStream); + } catch (IOException e) { + Log.e(TAG_HANDLER, "Unable to send message to RIL", e); + SapMessage errorReply = new SapMessage(SapMessage.ID_ERROR_RESP); + sendClientMessage(errorReply); + } + } + + /** + * Only call this from the sapHandler thread. + */ + private void sendReply(SapMessage msg) { + if(VERBOSE) Log.i(TAG_HANDLER, "sendReply() RFCOMM - " + SapMessage.getMsgTypeName(msg.getMsgType())); + try { + msg.write(mRfcommOut); + mRfcommOut.flush(); + } catch (IOException e) { + Log.w(TAG_HANDLER, e); + } + } + + private static String getMessageName(int messageId) { + switch (messageId) { + case SAP_MSG_RFC_REPLY: + return "SAP_MSG_REPLY"; + case SAP_MSG_RIL_CONNECT: + return "SAP_MSG_RIL_CONNECT"; + case SAP_MSG_RIL_REQ: + return "SAP_MSG_RIL_REQ"; + case SAP_MSG_RIL_IND: + return "SAP_MSG_RIL_IND"; + default: + return "Unknown message ID"; + } + } + +} diff --git a/src/com/android/bluetooth/sap/SapService.java b/src/com/android/bluetooth/sap/SapService.java new file mode 100644 index 000000000..5ca80b355 --- /dev/null +++ b/src/com/android/bluetooth/sap/SapService.java @@ -0,0 +1,795 @@ +package com.android.bluetooth.sap; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.IBluetooth; +import android.bluetooth.IBluetoothSap; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.BluetoothSap; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.PowerManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import com.android.bluetooth.R; +import com.android.bluetooth.Utils; +import com.android.bluetooth.btservice.AdapterService; +import com.android.bluetooth.btservice.ProfileService; +import com.android.bluetooth.btservice.ProfileService.IProfileServiceBinder; + +@TargetApi(Build.VERSION_CODES.ECLAIR) +public class SapService extends ProfileService { + + private static final String TAG = "SapService"; + public static final boolean DEBUG = true; + public static final boolean VERBOSE = true; + public static final boolean PTS_TEST = true; + + /* Message ID's */ + private static final int START_LISTENER = 1; + private static final int USER_TIMEOUT = 2; + private static final int SHUTDOWN = 3; + + public static final int MSG_SERVERSESSION_CLOSE = 5000; + public static final int MSG_SESSION_ESTABLISHED = 5001; + public static final int MSG_SESSION_DISCONNECTED = 5002; + + /* Intent indicating timeout for user confirmation. */ + public static final String USER_CONFIRM_TIMEOUT_ACTION = + "com.android.bluetooth.sap.USER_CONFIRM_TIMEOUT"; + private static final int USER_CONFIRM_TIMEOUT_VALUE = 25000; + + private PowerManager.WakeLock mWakeLock = null; + private BluetoothAdapter mAdapter; + private SocketAcceptThread mAcceptThread = null; + private BluetoothServerSocket mServerSocket = null; + private BluetoothSocket mConnSocket = null; + private BluetoothDevice mRemoteDevice = null; + private static String sRemoteDeviceName = null; + private volatile boolean mInterrupted; + private int mState; + private SapServer mSapServer = null; + private AlarmManager mAlarmManager = null; + private boolean mRemoveTimeoutMsg = false; + + private boolean mIsWaitingAuthorization = false; + + // package and class name to which we send intent to check message access access permission + private static final String ACCESS_AUTHORITY_PACKAGE = "com.android.settings"; + private static final String ACCESS_AUTHORITY_CLASS = + "com.android.settings.bluetooth.BluetoothPermissionRequest"; + + private static final ParcelUuid[] SAP_UUIDS = { + BluetoothUuid.SAP, + }; + + + public SapService() { + mState = BluetoothSap.STATE_DISCONNECTED; + } + + private void startRfcommSocketListener() { + if (VERBOSE) Log.v(TAG, "Sap Service startRfcommSocketListener"); + + if (mAcceptThread == null) { + mAcceptThread = new SocketAcceptThread(); + mAcceptThread.setName("SapAcceptThread"); + mAcceptThread.start(); + } + } + + private final boolean initSocket() { + if (VERBOSE) Log.v(TAG, "Sap Service initSocket"); + + boolean initSocketOK = false; + final int CREATE_RETRY_TIME = 10; + + // It's possible that create will fail in some cases. retry for 10 times + for (int i = 0; i < CREATE_RETRY_TIME && !mInterrupted; i++) { + initSocketOK = true; + try { + // It is mandatory for MSE to support initiation of bonding and + // encryption. + mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord + ("SIM Access", BluetoothUuid.SAP.getUuid()); + + } catch (IOException e) { + Log.e(TAG, "Error create RfcommServerSocket ", e); + initSocketOK = false; + } + if (!initSocketOK) { + // Need to break out of this loop if BT is being turned off. + if (mAdapter == null) break; + int state = mAdapter.getState(); + if ((state != BluetoothAdapter.STATE_TURNING_ON) && + (state != BluetoothAdapter.STATE_ON)) { + Log.w(TAG, "initServerSocket failed as BT is (being) turned off"); + break; + } + try { + if (VERBOSE) Log.v(TAG, "wait 300 ms"); + Thread.sleep(300); + } catch (InterruptedException e) { + Log.e(TAG, "socketAcceptThread thread was interrupted (3)", e); + } + } else { + break; + } + } + if (mInterrupted) { + initSocketOK = false; + // close server socket to avoid resource leakage + closeServerSocket(); + } + + if (initSocketOK) { + if (VERBOSE) Log.v(TAG, "Succeed to create listening socket "); + + } else { + Log.e(TAG, "Error to create listening socket after " + CREATE_RETRY_TIME + " try"); + } + return initSocketOK; + } + + private final synchronized void closeServerSocket() { + // exit SocketAcceptThread early + if (mServerSocket != null) { + try { + // this will cause mServerSocket.accept() return early with IOException + mServerSocket.close(); + mServerSocket = null; + } catch (IOException ex) { + Log.e(TAG, "Close Server Socket error: ", ex); + } + } + } + private final synchronized void closeConnectionSocket() { + if (mConnSocket != null) { + try { + mConnSocket.close(); + mConnSocket = null; + } catch (IOException e) { + Log.e(TAG, "Close Connection Socket error: ", e); + } + } + } + + private final void closeService() { + if (VERBOSE) Log.v(TAG, "SAP Service closeService in"); + + // exit initSocket early + mInterrupted = true; + closeServerSocket(); + + if (mAcceptThread != null) { + try { + mAcceptThread.shutdown(); + mAcceptThread.join(); + mAcceptThread = null; + } catch (InterruptedException ex) { + Log.w(TAG, "mAcceptThread close error", ex); + } + } + + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + + closeConnectionSocket(); + + if (VERBOSE) Log.v(TAG, "SAP Service closeService out"); + } + + private final void startSapServerSession() throws IOException { + if (VERBOSE) Log.v(TAG, "Sap Service startSapServerSession"); + + // acquire the wakeLock before start SAP transaction thread + // TODO: Do we need this? I guess we will wake when ever incoming data is available? + // And/or when a SIM event occurs - same for MAP. + // UPDATE: Change to use same approach as for MAP with a timer based wake-lock + if (mWakeLock == null) { + PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "StartingSapTransaction"); + mWakeLock.setReferenceCounted(false); + mWakeLock.acquire(); + } + + setState(BluetoothSap.STATE_CONNECTED); + + /* Start the SAP I/O thread and associate with message handler */ + mSapServer = new SapServer(mSessionStatusHandler, this, mConnSocket.getInputStream(), mConnSocket.getOutputStream()); + mSapServer.start(); + /* Warning: at this point we most likely have already handled the initial connect + * request from the SAP client, hence we need to be prepared to handle the + * response. (the SapHandler should have been started before this point)*/ + + if (VERBOSE) { + Log.v(TAG, "startSapServerSession() success!"); + } + } + + private void stopSapServerSession() { + + /* When we reach this point, the SapServer is closed down, and the client is + * supposed to close the RFCOMM connection. */ + if (VERBOSE) Log.v(TAG, "SAP Service stopSapServerSession"); + + // Release the wake lock if SAP transactions is over + /* TODO: Why do we need to hold a wakeLock? I assume the lower layers will wake + * the host at incoming data, hence I'm not sure why we need this wake lock? + * Any SAP req/ind is triggered either by incoming data from the client, from the + * RIL deamon or a user initiated disconnect. + * Perhaps we should only hold a wakelock while handling a message? + */ + + mAcceptThread = null; + closeConnectionSocket(); + closeServerSocket(); + + setState(BluetoothSap.STATE_DISCONNECTED); + + if (mWakeLock != null) { + mWakeLock.release(); + mWakeLock = null; + } + + // Last SAP transaction is finished, we start to listen for incoming + // rfcomm connection again + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } + } + + /** + * A thread that runs in the background waiting for remote rfcomm + * connect.Once a remote socket connected, this thread shall be + * shutdown.When the remote disconnect,this thread shall run again waiting + * for next request. + */ + private class SocketAcceptThread extends Thread { + + private boolean stopped = false; + + @Override + public void run() { + BluetoothServerSocket serverSocket; + if (mServerSocket == null) { + if (!initSocket()) { + return; + } + } + + while (!stopped) { + try { + if (VERBOSE) Log.v(TAG, "Accepting socket connection..."); + serverSocket = mServerSocket; + if(serverSocket == null) { + Log.w(TAG, "mServerSocket is null"); + break; + } + mConnSocket = mServerSocket.accept(); + if (VERBOSE) Log.v(TAG, "Accepted socket connection..."); + synchronized (SapService.this) { + if (mConnSocket == null) { + Log.w(TAG, "mConnSocket is null"); + break; + } + mRemoteDevice = mConnSocket.getRemoteDevice(); + } + if (mRemoteDevice == null) { + Log.i(TAG, "getRemoteDevice() = null"); + break; + } + + sRemoteDeviceName = mRemoteDevice.getName(); + // In case getRemoteName failed and return null + if (TextUtils.isEmpty(sRemoteDeviceName)) { + sRemoteDeviceName = getString(R.string.defaultname); + } + int permission = mRemoteDevice.getSimAccessPermission(); + + if (VERBOSE) Log.v(TAG, "getSimAccessPermission() = " + permission); + + if (permission == BluetoothDevice.ACCESS_ALLOWED) { + try { + if (VERBOSE) Log.v(TAG, "incoming connection accepted from: " + + sRemoteDeviceName + " automatically as trusted device"); + startSapServerSession(); + } catch (IOException ex) { + Log.e(TAG, "catch exception starting obex server session", ex); + } + } else if (permission != BluetoothDevice.ACCESS_REJECTED){ + Intent intent = new + Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); + intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); + intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, + BluetoothDevice.REQUEST_TYPE_SIM_ACCESS); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); + intent.putExtra(BluetoothDevice.EXTRA_PACKAGE_NAME, getPackageName()); + + mIsWaitingAuthorization = true; + sendBroadcast(intent, BLUETOOTH_ADMIN_PERM); + + if (VERBOSE) Log.v(TAG, "waiting for authorization for connection from: " + + sRemoteDeviceName); + + } else { + // Assuming reject is the stored state - continue to accept new connection. + continue; + } + stopped = true; // job done ,close this thread; + } catch (IOException ex) { + stopped=true; + if (VERBOSE) Log.v(TAG, "Accept exception: ", ex); + } + } + } + + void shutdown() { + stopped = true; + interrupt(); + } + } + + private final Handler mSessionStatusHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (VERBOSE) Log.v(TAG, "Handler(): got msg=" + msg.what); + + switch (msg.what) { + case START_LISTENER: + if (mAdapter.isEnabled()) { + startRfcommSocketListener(); + } + break; + case USER_TIMEOUT: + if (mIsWaitingAuthorization){ + sendCancelUserConfirmationIntent(mRemoteDevice); + cancelUserTimeoutAlarm(); + mIsWaitingAuthorization = false; + stopSapServerSession(); // And restart RfcommListener if needed + } + break; + case MSG_SERVERSESSION_CLOSE: + stopSapServerSession(); + break; + case MSG_SESSION_ESTABLISHED: + break; + case MSG_SESSION_DISCONNECTED: + // handled elsewhere + break; + case SHUTDOWN: + /* Ensure to call close from this handler to avoid starting new stuff + because of pending messages */ + closeService(); + break; + default: + break; + } + } + }; + + private void setState(int state) { + setState(state, BluetoothSap.RESULT_SUCCESS); + } + + private synchronized void setState(int state, int result) { + if (state != mState) { + if (DEBUG) Log.d(TAG, "Sap state " + mState + " -> " + state + ", result = " + + result); + int prevState = mState; + mState = state; + Intent intent = new Intent(BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); + intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState); + intent.putExtra(BluetoothProfile.EXTRA_STATE, mState); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mRemoteDevice); + sendBroadcast(intent, BLUETOOTH_PERM); + AdapterService s = AdapterService.getAdapterService(); + if (s != null) { + s.onProfileConnectionStateChanged(mRemoteDevice, BluetoothProfile.SAP, + mState, prevState); + } + } + } + + public int getState() { + return mState; + } + + public BluetoothDevice getRemoteDevice() { + return mRemoteDevice; + } + + public static String getRemoteDeviceName() { + return sRemoteDeviceName; + } + + public boolean disconnect(BluetoothDevice device) { + boolean result = false; + synchronized (SapService.this) { + if (getRemoteDevice().equals(device)) { + switch (mState) { + case BluetoothSap.STATE_CONNECTED: + closeConnectionSocket(); + setState(BluetoothSap.STATE_DISCONNECTED, BluetoothSap.RESULT_CANCELED); + result = true; + break; + default: + break; + } + } + } + return result; + } + + public List<BluetoothDevice> getConnectedDevices() { + List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); + synchronized(this) { + if (mState == BluetoothSap.STATE_CONNECTED && mRemoteDevice != null) { + devices.add(mRemoteDevice); + } + } + return devices; + } + + public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { + List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>(); + Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); + int connectionState; + synchronized (this) { + for (BluetoothDevice device : bondedDevices) { + ParcelUuid[] featureUuids = device.getUuids(); + if (!BluetoothUuid.containsAnyUuid(featureUuids, SAP_UUIDS)) { + continue; + } + connectionState = getConnectionState(device); + for(int i = 0; i < states.length; i++) { + if (connectionState == states[i]) { + deviceList.add(device); + } + } + } + } + return deviceList; + } + + public int getConnectionState(BluetoothDevice device) { + synchronized(this) { + if (getState() == BluetoothSap.STATE_CONNECTED && getRemoteDevice().equals(device)) { + return BluetoothProfile.STATE_CONNECTED; + } else { + return BluetoothProfile.STATE_DISCONNECTED; + } + } + } + + public boolean setPriority(BluetoothDevice device, int priority) { + Settings.Global.putInt(getContentResolver(), + Settings.Global.getBluetoothSapPriorityKey(device.getAddress()), + priority); + if (DEBUG) Log.d(TAG, "Saved priority " + device + " = " + priority); + return true; + } + + public int getPriority(BluetoothDevice device) { + int priority = Settings.Global.getInt(getContentResolver(), + Settings.Global.getBluetoothSapPriorityKey(device.getAddress()), + BluetoothProfile.PRIORITY_UNDEFINED); + return priority; + } + + @Override + protected IProfileServiceBinder initBinder() { + return new SapBinder(this); + } + + @Override + protected boolean start() { + Log.v(TAG, "start()"); + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + filter.addAction(USER_CONFIRM_TIMEOUT_ACTION); + + try { + registerReceiver(mSapReceiver, filter); + } catch (Exception e) { + Log.w(TAG,"Unable to register sap receiver",e); + } + mInterrupted = false; + mAdapter = BluetoothAdapter.getDefaultAdapter(); + // start RFCOMM listener + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(START_LISTENER)); + return true; + } + + @Override + protected boolean stop() { + Log.v(TAG, "stop()"); + try { + unregisterReceiver(mSapReceiver); + } catch (Exception e) { + Log.w(TAG,"Unable to unregister sap receiver",e); + } + setState(BluetoothSap.STATE_DISCONNECTED, BluetoothSap.RESULT_CANCELED); + sendShutdownMessage(); + return true; + } + + public boolean cleanup() { + setState(BluetoothSap.STATE_DISCONNECTED, BluetoothSap.RESULT_CANCELED); + closeService(); + if(mSessionStatusHandler != null) { + mSessionStatusHandler.removeCallbacksAndMessages(null); + } + return true; + } + + private void setUserTimeoutAlarm(){ + if(DEBUG)Log.d(TAG,"SetUserTimeOutAlarm()"); + if(mAlarmManager == null){ + mAlarmManager =(AlarmManager) this.getSystemService (Context.ALARM_SERVICE); + } + mRemoveTimeoutMsg = true; + Intent timeoutIntent = + new Intent(USER_CONFIRM_TIMEOUT_ACTION); + PendingIntent pIntent = PendingIntent.getBroadcast(this, 0, timeoutIntent, 0); + mAlarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + + USER_CONFIRM_TIMEOUT_VALUE,pIntent); + } + + private void cancelUserTimeoutAlarm(){ + if(DEBUG)Log.d(TAG,"cancelUserTimeOutAlarm()"); + Intent intent = new Intent(this, SapService.class); + PendingIntent sender = PendingIntent.getBroadcast(this, 0, intent, 0); + AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(sender); + mRemoveTimeoutMsg = false; + } + + private void sendCancelUserConfirmationIntent(BluetoothDevice device) { + Intent intent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_CANCEL); + intent.setClassName(ACCESS_AUTHORITY_PACKAGE, ACCESS_AUTHORITY_CLASS); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + intent.putExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE, + BluetoothDevice.REQUEST_TYPE_SIM_ACCESS); + sendBroadcast(intent, BLUETOOTH_PERM); + } + + private void sendShutdownMessage() { + /* Any pending messages are no longer valid. + To speed up things, simply delete them. */ + if (mRemoveTimeoutMsg) { + Intent timeoutIntent = + new Intent(USER_CONFIRM_TIMEOUT_ACTION); + sendBroadcast(timeoutIntent, BLUETOOTH_PERM); + mIsWaitingAuthorization = false; + cancelUserTimeoutAlarm(); + } + mSessionStatusHandler.removeCallbacksAndMessages(null); + // Request release of all resources + mSessionStatusHandler.obtainMessage(SHUTDOWN).sendToTarget(); + } + + private void sendConnectTimeoutMessage() { + if (DEBUG) Log.d(TAG, "sendConnectTimeoutMessage()"); + if(mSessionStatusHandler != null) { + Message msg = mSessionStatusHandler.obtainMessage(USER_TIMEOUT); + msg.sendToTarget(); + } // Can only be null during shutdown + } + + private SapBroadcastReceiver mSapReceiver = new SapBroadcastReceiver(); + + private class SapBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + + if (VERBOSE) Log.v(TAG, "onReceive"); + String action = intent.getAction(); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + if (state == BluetoothAdapter.STATE_TURNING_OFF) { + if (DEBUG) Log.d(TAG, "STATE_TURNING_OFF"); + sendShutdownMessage(); + } else if (state == BluetoothAdapter.STATE_ON) { + if (DEBUG) Log.d(TAG, "STATE_ON"); + // start RFCOMM listener + mSessionStatusHandler.sendMessage(mSessionStatusHandler + .obtainMessage(START_LISTENER)); + } + } else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) { + Log.v(TAG, " - Received BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY"); + if (!mIsWaitingAuthorization) { + // this reply is not for us + return; + } + + mIsWaitingAuthorization = false; + + if (intent.getIntExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, + BluetoothDevice.CONNECTION_ACCESS_NO) == + BluetoothDevice.CONNECTION_ACCESS_YES) { + //bluetooth connection accepted by user + if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { + boolean result = mRemoteDevice.setSimAccessPermission( + BluetoothDevice.ACCESS_ALLOWED); + if (VERBOSE) { + Log.v(TAG, "setSimAccessPermission(ACCESS_ALLOWED) result=" + result); + } } + try { + if (mConnSocket != null) { + // start obex server and rfcomm connection + startSapServerSession(); + } else { + stopSapServerSession(); + } + } catch (IOException ex) { + Log.e(TAG, "Caught the error: ", ex); + } + } else { + if (intent.getBooleanExtra(BluetoothDevice.EXTRA_ALWAYS_ALLOWED, false)) { + boolean result = mRemoteDevice.setSimAccessPermission( + BluetoothDevice.ACCESS_REJECTED); + if (VERBOSE) { + Log.v(TAG, "setSimAccessPermission(ACCESS_REJECTED) result=" + + result); + } + } + stopSapServerSession(); + } + } else if (action.equals(USER_CONFIRM_TIMEOUT_ACTION)){ + if (DEBUG) Log.d(TAG, "USER_CONFIRM_TIMEOUT ACTION Received."); + // send us self a message about the timeout. + sendConnectTimeoutMessage(); + } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) && + mIsWaitingAuthorization) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + + if (mRemoteDevice == null || device == null) { + Log.e(TAG, "Unexpected error!"); + return; + } + + if (DEBUG) Log.d(TAG,"ACL disconnected for " + device); + + if (mRemoteDevice.equals(device) && mRemoveTimeoutMsg) { + // Send any pending timeout now, as ACL got disconnected. + mSessionStatusHandler.removeMessages(USER_TIMEOUT); + sendCancelUserConfirmationIntent(mRemoteDevice); + mIsWaitingAuthorization = false; + mRemoveTimeoutMsg = false; + } + } + } + }; + + //Binder object: Must be static class or memory leak may occur + /** + * This class implements the IBluetoothSap interface - or actually it validates the + * preconditions for calling the actual functionality in the SapService, and calls it. + */ + private static class SapBinder extends IBluetoothSap.Stub + implements IProfileServiceBinder { + private SapService mService; + + private SapService getService() { + if (!Utils.checkCaller()) { + Log.w(TAG,"call not allowed for non-active user"); + return null; + } + + if (mService != null && mService.isAvailable() ) { + mService.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); + return mService; + } + return null; + } + + SapBinder(SapService service) { + Log.v(TAG, "SapBinder()"); + mService = service; + } + + public boolean cleanup() { + mService = null; + return true; + } + + public int getState() { + Log.v(TAG, "getState()"); + SapService service = getService(); + if (service == null) return BluetoothSap.STATE_DISCONNECTED; + return getService().getState(); + } + + public BluetoothDevice getClient() { + Log.v(TAG, "getClient()"); + SapService service = getService(); + if (service == null) return null; + Log.v(TAG, "getClient() - returning " + service.getRemoteDevice()); + return service.getRemoteDevice(); + } + + public boolean isConnected(BluetoothDevice device) { + Log.v(TAG, "isConnected()"); + SapService service = getService(); + if (service == null) return false; + return (service.getState() == BluetoothSap.STATE_CONNECTED + && service.getRemoteDevice().equals(device)); + } + + public boolean connect(BluetoothDevice device) { + Log.v(TAG, "connect()"); + SapService service = getService(); + if (service == null) return false; + return false; + } + + public boolean disconnect(BluetoothDevice device) { + Log.v(TAG, "disconnect()"); + SapService service = getService(); + if (service == null) return false; + return service.disconnect(device); + } + + public List<BluetoothDevice> getConnectedDevices() { + Log.v(TAG, "getConnectedDevices()"); + SapService service = getService(); + if (service == null) return new ArrayList<BluetoothDevice>(0); + return service.getConnectedDevices(); + } + + public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { + Log.v(TAG, "getDevicesMatchingConnectionStates()"); + SapService service = getService(); + if (service == null) return new ArrayList<BluetoothDevice>(0); + return service.getDevicesMatchingConnectionStates(states); + } + + public int getConnectionState(BluetoothDevice device) { + Log.v(TAG, "getConnectionState()"); + SapService service = getService(); + if (service == null) return BluetoothProfile.STATE_DISCONNECTED; + return service.getConnectionState(device); + } + + public boolean setPriority(BluetoothDevice device, int priority) { + SapService service = getService(); + if (service == null) return false; + return service.setPriority(device, priority); + } + + public int getPriority(BluetoothDevice device) { + SapService service = getService(); + if (service == null) return BluetoothProfile.PRIORITY_UNDEFINED; + return service.getPriority(device); + } + } +} |