OSDN Git Service

Add support for Bluetooth Sim Access Profile (3/4)
authorCasper Bonde <c.bonde@samsung.com>
Thu, 19 Mar 2015 09:01:53 +0000 (10:01 +0100)
committerAndre Eisenbach <eisenbach@google.com>
Thu, 16 Apr 2015 07:11:09 +0000 (00:11 -0700)
Server side of the Sim Access Profile. Enables a Bluetooth device
to take over control of the SIM. This is usefull in cars where the
internal antenna in the phone can have a low signal level.

For this profile to work, the RIL driver must allow for this feature
to be used, and it must provide the API needed. The API is based on
protoBuf.

This change includes some SAP test cases.

Change-Id: Ia46493383efed6b8a89ca270bdafa60fc1a150c1

14 files changed:
Android.mk
AndroidManifest.xml
res/values/config.xml
res/values/strings_sap.xml [new file with mode: 0644]
src/com/android/bluetooth/btservice/AdapterService.java
src/com/android/bluetooth/btservice/BondStateMachine.java
src/com/android/bluetooth/btservice/Config.java
src/com/android/bluetooth/sap/SapMessage.java [new file with mode: 0644]
src/com/android/bluetooth/sap/SapRilReceiver.java [new file with mode: 0644]
src/com/android/bluetooth/sap/SapServer.java [new file with mode: 0644]
src/com/android/bluetooth/sap/SapService.java [new file with mode: 0644]
tests/Android.mk
tests/src/com/android/bluetooth/tests/SapServerTest.java [new file with mode: 0644]
tests/src/com/android/bluetooth/tests/SapSocketTest.java [new file with mode: 0644]

index 7d2fdfa..e03acdc 100644 (file)
@@ -21,8 +21,8 @@ LOCAL_PACKAGE_NAME := Bluetooth
 LOCAL_CERTIFICATE := platform
 
 LOCAL_JNI_SHARED_LIBRARIES := libbluetooth_jni
-LOCAL_JAVA_LIBRARIES := javax.obex telephony-common
-LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard  bluetooth.mapsapi
+LOCAL_JAVA_LIBRARIES := javax.obex telephony-common libprotobuf-java-micro
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.vcard  bluetooth.mapsapi sap-api-java-static
 
 LOCAL_REQUIRED_MODULES := bluetooth.default
 LOCAL_MULTILIB := 32
index 47b8b9c..9bd6729 100644 (file)
         </provider>
         <service
             android:process="@string/process"
+            android:name=".sap.SapService"
+            android:enabled="@bool/profile_supported_sap" >
+            <intent-filter>
+                <action android:name="android.bluetooth.IBluetoothSap" />
+            </intent-filter>
+        </service>
+        <service
+            android:process="@string/process"
             android:name = ".gatt.GattService"
             android:enabled="@bool/profile_supported_gatt">
             <intent-filter>
index f2b4ebb..c023e81 100644 (file)
@@ -27,4 +27,5 @@
     <bool name="pbap_use_profile_for_owner_vcard">true</bool>
     <bool name="profile_supported_map">true</bool>
     <bool name="profile_supported_avrcp_controller">false</bool>
+    <bool name="profile_supported_sap">true</bool>
 </resources>
diff --git a/res/values/strings_sap.xml b/res/values/strings_sap.xml
new file mode 100644 (file)
index 0000000..dd1dbcd
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="bluetooth_sap_notif_title">Bluetooth SIM access</string>
+    <string name="bluetooth_sap_notif_ticker">Bluetooth SIM Access</string>
+    <string name="bluetooth_sap_notif_message">Request client to disconnect?</string>
+    <string name="bluetooth_sap_notif_disconnecting">Waiting for client to disconnect</string>
+    <string name="bluetooth_sap_notif_disconnect_button">Disconnect</string>
+    <string name="bluetooth_sap_notif_force_disconnect_button">Force disconnect</string>
+</resources>
index f16f082..156d244 100644 (file)
@@ -119,6 +119,8 @@ public class AdapterService extends Service {
             "phonebook_access_permission";
     private static final String MESSAGE_ACCESS_PERMISSION_PREFERENCE_FILE =
             "message_access_permission";
+    private static final String SIM_ACCESS_PERMISSION_PREFERENCE_FILE =
+            "sim_access_permission";
 
     private static final int ADAPTER_SERVICE_TYPE=Service.START_STICKY;
 
@@ -1137,6 +1139,28 @@ public class AdapterService extends Service {
             return service.setMessageAccessPermission(device, value);
         }
 
+        public int getSimAccessPermission(BluetoothDevice device) {
+            if (!Utils.checkCaller()) {
+                Log.w(TAG, "getSimAccessPermission() - Not allowed for non-active user");
+                return BluetoothDevice.ACCESS_UNKNOWN;
+            }
+
+            AdapterService service = getService();
+            if (service == null) return BluetoothDevice.ACCESS_UNKNOWN;
+            return service.getSimAccessPermission(device);
+        }
+
+        public boolean setSimAccessPermission(BluetoothDevice device, int value) {
+            if (!Utils.checkCaller()) {
+                Log.w(TAG, "setSimAccessPermission() - Not allowed for non-active user");
+                return false;
+            }
+
+            AdapterService service = getService();
+            if (service == null) return false;
+            return service.setSimAccessPermission(device, value);
+        }
+
         public void sendConnectionStateChange(BluetoothDevice
                 device, int profile, int state, int prevState) {
             AdapterService service = getService();
@@ -1742,6 +1766,31 @@ public class AdapterService extends Service {
         return editor.commit();
     }
 
+    int getSimAccessPermission(BluetoothDevice device) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
+        SharedPreferences pref = getSharedPreferences(SIM_ACCESS_PERMISSION_PREFERENCE_FILE,
+                Context.MODE_PRIVATE);
+        if (!pref.contains(device.getAddress())) {
+            return BluetoothDevice.ACCESS_UNKNOWN;
+        }
+        return pref.getBoolean(device.getAddress(), false)
+                ? BluetoothDevice.ACCESS_ALLOWED : BluetoothDevice.ACCESS_REJECTED;
+    }
+
+    boolean setSimAccessPermission(BluetoothDevice device, int value) {
+        enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+                                       "Need BLUETOOTH PRIVILEGED permission");
+        SharedPreferences pref = getSharedPreferences(SIM_ACCESS_PERMISSION_PREFERENCE_FILE,
+                Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = pref.edit();
+        if (value == BluetoothDevice.ACCESS_UNKNOWN) {
+            editor.remove(device.getAddress());
+        } else {
+            editor.putBoolean(device.getAddress(), value == BluetoothDevice.ACCESS_ALLOWED);
+        }
+        return editor.commit();
+    }
+
      void sendConnectionStateChange(BluetoothDevice
             device, int profile, int state, int prevState) {
         // TODO(BT) permission check?
index e4d2349..7651e4d 100644 (file)
@@ -85,7 +85,7 @@ final class BondStateMachine extends StateMachine {
         bsm.start();
         return bsm;
     }
-            
+
     public void doQuit() {
         quitNow();
     }
@@ -196,6 +196,8 @@ final class BondStateMachine extends StateMachine {
                                     BluetoothDevice.ACCESS_UNKNOWN);
                             mAdapterService.setMessageAccessPermission(dev,
                                     BluetoothDevice.ACCESS_UNKNOWN);
+                            mAdapterService.setSimAccessPermission(dev,
+                                    BluetoothDevice.ACCESS_UNKNOWN);
                             // Set the profile Priorities to undefined
                             clearProfilePriorty(dev);
                         }
index 732d58e..4301cd9 100644 (file)
@@ -34,6 +34,7 @@ import com.android.bluetooth.hid.HidService;
 import com.android.bluetooth.pan.PanService;
 import com.android.bluetooth.gatt.GattService;
 import com.android.bluetooth.map.BluetoothMapService;
+import com.android.bluetooth.sap.SapService;
 
 public class Config {
     private static final String TAG = "AdapterServiceConfig";
@@ -54,6 +55,7 @@ public class Config {
         BluetoothMapService.class,
         HeadsetClientService.class,
         AvrcpControllerService.class,
+        SapService.class
     };
     /**
      * Resource flag to indicate whether profile is supported or not.
@@ -69,6 +71,7 @@ public class Config {
         R.bool.profile_supported_map,
         R.bool.profile_supported_hfpclient,
         R.bool.profile_supported_avrcp_controller,
+        R.bool.profile_supported_sap,
     };
 
     private static Class[] SUPPORTED_PROFILES = new Class[0];
diff --git a/src/com/android/bluetooth/sap/SapMessage.java b/src/com/android/bluetooth/sap/SapMessage.java
new file mode 100644 (file)
index 0000000..71e6a97
--- /dev/null
@@ -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 (file)
index 0000000..17d4fa1
--- /dev/null
@@ -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 (file)
index 0000000..4a1998f
--- /dev/null
@@ -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 (file)
index 0000000..5ca80b3
--- /dev/null
@@ -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);
+        }
+    }
+}
index 3da66d3..896d466 100755 (executable)
@@ -5,7 +5,7 @@ include $(CLEAR_VARS)
 LOCAL_MODULE_TAGS := optional
 LOCAL_CERTIFICATE := platform
 
-LOCAL_JAVA_LIBRARIES := javax.obex android.test.runner telephony-common mms-common
+LOCAL_JAVA_LIBRARIES := javax.obex android.test.runner telephony-common mms-common libprotobuf-java-2.3.0-micro
 LOCAL_STATIC_JAVA_LIBRARIES := com.android.emailcommon
 
 # Include all test java files.
diff --git a/tests/src/com/android/bluetooth/tests/SapServerTest.java b/tests/src/com/android/bluetooth/tests/SapServerTest.java
new file mode 100644 (file)
index 0000000..f0dc084
--- /dev/null
@@ -0,0 +1,583 @@
+package com.android.bluetooth.tests;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.bluetooth.sap.SapMessage;
+import com.android.bluetooth.sap.SapServer;
+
+/**
+ * Test either using the reference ril without a modem, or using a RIL implementing the
+ * BT SAP API, by providing the rild-bt socket as well as the extended API functions for SAP.
+ *
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class SapServerTest extends AndroidTestCase {
+    protected static String TAG = "SapServerTest";
+    protected static final boolean D = true;
+    private static final boolean rilTestModeEnabled = false; /* Set the RIL driver in test mode, where request stubs are used in stead of forwarding to the modem/sim */
+
+    private Context mContext = null;
+
+    public SapServerTest() {
+        super();
+    }
+
+    private void buildDefaultInitSeq(SapSequencer sequencer) throws IOException {
+        SapMessage connectReq = new SapMessage(SapMessage.ID_CONNECT_REQ);
+        connectReq.setMaxMsgSize(276);
+
+        SapMessage connectResp = new SapMessage(SapMessage.ID_CONNECT_RESP);
+        connectResp.setConnectionStatus(SapMessage.CON_STATUS_OK);
+        connectResp.setMaxMsgSize(0); /* shall be connection status (0)  on success */
+
+        SapMessage statusInd = new SapMessage(SapMessage.ID_STATUS_IND);
+        statusInd.setStatusChange(SapMessage.STATUS_CARD_RESET);
+
+        int index = sequencer.addStep(connectReq, connectResp);
+        sequencer.addSubStep(index, null, statusInd);
+
+    }
+    /**
+     * Test that the SapServer is capable of handling a connect request with no call ongoing.
+     */
+    public void testSapServerConnectSimple() {
+        mContext = this.getContext();
+
+        try {
+
+            SapSequencer sequencer = new SapSequencer();
+            if(rilTestModeEnabled) {
+                sequencer.testModeEnable(true);
+            }
+            /* Build a default init sequence */
+            buildDefaultInitSeq(sequencer);
+            SapMessage disconnectReq = new SapMessage(SapMessage.ID_DISCONNECT_REQ);
+            SapMessage disconnectResp = new SapMessage(SapMessage.ID_DISCONNECT_RESP);
+
+            SapMessage resetResp = new SapMessage(SapMessage.ID_RESET_SIM_RESP);
+            resetResp.setResultCode(SapMessage.RESULT_OK);
+
+            int index = sequencer.addStep(disconnectReq, disconnectResp);
+
+            assertTrue(sequencer.run());
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+    }
+
+    public void testSapServerApiComplete() {
+        mContext = this.getContext();
+        byte[] dummyBytes = {1, 2, 3, 4};
+
+        /* Select file '2FE2' - observed selected in modem init sequence.
+         * According to spec file '2FE2' is EF_ICCID (Elementary file
+         * ICC identification).
+         */
+
+        byte[] selectFileApdu = {(byte)0xa0, (byte)0xa4, (byte)0x00, (byte)0x00,
+            (byte)0x02, (byte)0x2f, (byte)0xe2};
+
+        /* Command succesfull '9F', length '0F' of response data */
+        byte[] selectFileApduResp = {(byte)0x9f, (byte)0x0f};
+
+        try {
+
+            SapSequencer sequencer = new SapSequencer();
+            if(rilTestModeEnabled) {
+                sequencer.testModeEnable(true);
+            }
+
+            /* Build a default init sequence */
+            buildDefaultInitSeq(sequencer);
+
+            SapMessage powerOffReq = new SapMessage(SapMessage.ID_POWER_SIM_OFF_REQ);
+
+            SapMessage powerOffResp = new SapMessage(SapMessage.ID_POWER_SIM_OFF_RESP);
+            powerOffResp.setResultCode(SapMessage.RESULT_OK);
+            sequencer.addStep(powerOffReq, powerOffResp);
+
+
+            SapMessage powerOnReq = new SapMessage(SapMessage.ID_POWER_SIM_ON_REQ);
+
+            SapMessage powerOnResp = new SapMessage(SapMessage.ID_POWER_SIM_ON_RESP);
+            powerOnResp.setResultCode(SapMessage.RESULT_OK);
+            sequencer.addStep(powerOnReq, powerOnResp);
+
+            SapMessage resetReq = new SapMessage(SapMessage.ID_RESET_SIM_REQ);
+
+            SapMessage resetResp = new SapMessage(SapMessage.ID_RESET_SIM_RESP);
+            resetResp.setResultCode(SapMessage.RESULT_OK);
+            int index = sequencer.addStep(resetReq, resetResp);
+
+            /* SapMessage statusInd = new SapMessage(SapMessage.ID_STATUS_IND); */
+            /* statusInd.setStatusChange(SapMessage.STATUS_CARD_RESET); */
+            /* sequencer.addSubStep(index, null, statusInd); */
+
+            SapMessage atrReq = new SapMessage(SapMessage.ID_TRANSFER_ATR_REQ);
+
+            SapMessage atrResp = new SapMessage(SapMessage.ID_TRANSFER_ATR_RESP);
+            atrResp.setResultCode(SapMessage.RESULT_OK);
+            if(rilTestModeEnabled) {
+                /* Use the hard coded return array, must match the test array in RIL */
+                byte[] atr = {1, 2, 3, 4};
+                atrResp.setAtr(atr);
+            } else {
+                atrResp.setAtr(null);
+            }
+            sequencer.addStep(atrReq, atrResp);
+
+
+            SapMessage apduReq = new SapMessage(SapMessage.ID_TRANSFER_APDU_REQ);
+            if(rilTestModeEnabled) {
+                apduReq.setApdu(dummyBytes);
+            } else {
+                apduReq.setApdu(selectFileApdu);
+            }
+
+            SapMessage apduResp = new SapMessage(SapMessage.ID_TRANSFER_APDU_RESP);
+            apduResp.setResultCode(SapMessage.RESULT_OK);
+            if(rilTestModeEnabled) {
+                apduResp.setApduResp(dummyBytes);
+            } else {
+                apduResp.setApduResp(selectFileApduResp);
+            }
+            sequencer.addStep(apduReq, apduResp);
+
+
+            SapMessage apdu7816Req = new SapMessage(SapMessage.ID_TRANSFER_APDU_REQ);
+            if(rilTestModeEnabled) {
+                apdu7816Req.setApdu7816(dummyBytes);
+            } else {
+                apdu7816Req.setApdu7816(selectFileApdu);
+            }
+
+            SapMessage apdu7816Resp = new SapMessage(SapMessage.ID_TRANSFER_APDU_RESP);
+            apdu7816Resp.setResultCode(SapMessage.RESULT_OK);
+            if(rilTestModeEnabled) {
+                apdu7816Resp.setApduResp(dummyBytes);
+            } else {
+                apdu7816Resp.setApduResp(selectFileApduResp);
+            }
+            sequencer.addStep(apdu7816Req, apdu7816Resp);
+
+            SapMessage transferCardReaderStatusReq = new SapMessage(SapMessage.ID_TRANSFER_CARD_READER_STATUS_REQ);
+
+            SapMessage transferCardReaderStatusResp = new SapMessage(SapMessage.ID_TRANSFER_CARD_READER_STATUS_RESP);
+            transferCardReaderStatusResp.setResultCode(SapMessage.RESULT_OK);
+            sequencer.addStep(transferCardReaderStatusReq, transferCardReaderStatusResp);
+
+            SapMessage setTransportProtocolReq = new SapMessage(SapMessage.ID_SET_TRANSPORT_PROTOCOL_REQ);
+            setTransportProtocolReq.setTransportProtocol(0x01); // T=1
+
+            SapMessage setTransportProtocolResp = new SapMessage(SapMessage.ID_SET_TRANSPORT_PROTOCOL_RESP);
+            setTransportProtocolResp.setResultCode(SapMessage.RESULT_OK);
+            sequencer.addStep(setTransportProtocolReq, setTransportProtocolResp);
+
+            SapMessage disconnectReq = new SapMessage(SapMessage.ID_DISCONNECT_REQ);
+
+            SapMessage disconnectResp = new SapMessage(SapMessage.ID_DISCONNECT_RESP);
+            sequencer.addStep(disconnectReq, disconnectResp);
+
+            assertTrue(sequencer.run());
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+    }
+
+    /**
+     * This test fails if the apdu request generates a response before the reset request is handled.
+     * This happens if the reference ril is used in test mode.
+     */
+    public void testSapServerResetWhileWritingApdu() {
+        mContext = this.getContext();
+        byte[] dummyBytes = {1, 2, 3, 4};
+        int index;
+
+        try {
+
+            SapSequencer sequencer = new SapSequencer();
+            if(rilTestModeEnabled) {
+                sequencer.testModeEnable(true);
+            }
+
+            /* Build a default init sequence */
+            buildDefaultInitSeq(sequencer);
+
+            SapMessage apduReq = new SapMessage(SapMessage.ID_TRANSFER_APDU_REQ);
+            apduReq.setApdu(dummyBytes);
+
+            SapMessage apduResp = null; /* expect no response as we send a SIM_RESET before the write APDU completes
+                                            TODO: Consider adding a real response, and add an optional flag. */
+            index = sequencer.addStep(apduReq, apduResp);
+
+            SapMessage resetReq = new SapMessage(SapMessage.ID_RESET_SIM_REQ);
+            SapMessage resetResp = new SapMessage(SapMessage.ID_RESET_SIM_RESP);
+            resetResp.setResultCode(SapMessage.RESULT_OK);
+            sequencer.addSubStep(index, resetReq, resetResp);
+
+            SapMessage statusInd = new SapMessage(SapMessage.ID_STATUS_IND);
+            statusInd.setStatusChange(SapMessage.STATUS_CARD_RESET);
+            sequencer.addSubStep(index, null, statusInd);
+
+            assertTrue(sequencer.run());
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+    }
+
+    /**
+     * Test that SapServer can disconnect based on a disconnect intent.
+     * TODO: This test could validate the timeout values.
+     * TODO: We need to add a IAction and add an action to a step, to be able to send
+     *       the disconnect intent at the right time.
+     */
+    public void testSapServerTimeouts() {
+        Intent sapDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
+        sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, SapMessage.DISC_IMMEDIATE);
+        mContext = this.getContext();
+
+        try {
+
+            SapSequencer sequencer = new SapSequencer();
+            if(rilTestModeEnabled) {
+                sequencer.testModeEnable(true);
+            }
+            /* Build a default init sequence */
+            buildDefaultInitSeq(sequencer);
+
+            SapMessage disconnectReq = new SapMessage(SapMessage.ID_DISCONNECT_REQ);
+            SapMessage disconnectResp = new SapMessage(SapMessage.ID_DISCONNECT_RESP);
+
+            SapMessage resetResp = new SapMessage(SapMessage.ID_RESET_SIM_RESP);
+            resetResp.setResultCode(SapMessage.RESULT_OK);
+
+            int index = sequencer.addStep(disconnectReq, disconnectResp);
+
+            assertTrue(sequencer.run());
+
+            mContext.sendBroadcast(sapDisconnectIntent);
+
+        } catch (IOException e) {
+            Log.e(TAG, "IOException", e);
+        }
+    }
+    public void testSapServerTimeoutsActionDiscIntent() {
+
+    }
+
+    public class SapSequencer implements Callback {
+
+        private final static int MSG_ID_TIMEOUT = 0x01;
+        private final static int TIMEOUT_VALUE = 100*2000; // ms
+        private ArrayList<SeqStep> sequence = null;
+        private HandlerThread handlerThread = null;
+        private Handler messageHandler = null;
+
+        private SapServer sapServer = null;
+
+        private PipedInputStream inStream = null; // Used to write requests to SapServer
+        private PipedOutputStream outStream = null; // Used to read commands from the SapServer
+
+
+        public class SeqStep {
+            public ArrayList<SapMessage> requests = null;
+            public ArrayList<SapMessage> responses = null;
+            public int index = 0; /* requests with same index are executed in parallel (without waiting for a response) */
+            public SeqStep(SapMessage request, SapMessage response) {
+                requests = new ArrayList<SapMessage>();
+                responses = new ArrayList<SapMessage>();
+                this.requests.add(request);
+                this.responses.add(response);
+            }
+
+            public void add(SapMessage request, SapMessage response) {
+                this.requests.add(request);
+                this.responses.add(response);
+            }
+
+            /**
+             * Examine if the step has any expected response.
+             * @return true if one or more of the responses are != null. False otherwise.
+             */
+            public boolean hasResponse() {
+                if(responses == null)
+                    return false;
+                for(SapMessage response : responses) {
+                    if(response != null)
+                        return true;
+                }
+                return false;
+            }
+        }
+
+        public SapSequencer() throws IOException {
+            /* Setup the looper thread to handle messages */
+            handlerThread = new HandlerThread("SapTestTimeoutHandler", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+            handlerThread.start();
+            Looper testLooper = handlerThread.getLooper();
+            messageHandler = new Handler(testLooper, this);
+
+            /* Initialize members */
+            sequence = new ArrayList<SeqStep>();
+
+            /* Create a SapServer. Fake the BtSocket using piped input/output streams*/
+            inStream = new PipedInputStream(8092);
+            outStream = new PipedOutputStream();
+            sapServer = new SapServer(null, mContext, new PipedInputStream(outStream, 8092), new PipedOutputStream(inStream));
+            sapServer.start();
+        }
+
+        /* TODO:
+         *  - Add support for actions ?
+         *  */
+
+        /**
+         * Enable/Disable test mode during the next connect request.
+         * @param enable
+         */
+        public void testModeEnable(boolean enable) {
+            if(enable)
+                sapServer.setTestMode(SapMessage.TEST_MODE_ENABLE);
+            else
+                sapServer.setTestMode(SapMessage.TEST_MODE_DISABLE);
+        }
+
+        /**
+         * Add a test step to the sequencer
+         * @param request The request to send to the SAP server
+         * @param response The response to EXPECT from the SAP server
+         * @return The created step index, which can be used when adding events or actions.
+         */
+        public int addStep(SapMessage request, SapMessage response) { // TODO: should we add a step trigger? (in stead of just executing in sequence)
+            SeqStep newStep = new SeqStep(request, response);
+            sequence.add(newStep);
+            return sequence.indexOf(newStep);
+        }
+
+        /**
+         * Add a sub-step to a sequencer step. All requests added to the same index will be send to the
+         * SapServer in the order added before listening for the response.
+         * The response order is not validated - hence for each response received the entire list of
+         * responses in the step will be searched for a match.
+         * @param index the index returned from addStep() to which the sub-step is to be added.
+         * @param request The request to send to the SAP server
+         * @param response The response to EXPECT from the SAP server
+         */
+        public void addSubStep(int index, SapMessage request, SapMessage response) {
+            SeqStep step = sequence.get(index);
+            step.add(request, response);
+        }
+
+
+        /**
+         * Run the sequence, by writing a request and wait for a response. Validate the response
+         * is either the expected response or one of the expected events.
+         * @return
+         */
+        public boolean run() throws IOException {
+            SapMessage inMsg = null;
+            boolean done;
+            for(SeqStep step : sequence) {
+
+                /* Write all requests - if any */
+                if(step.requests != null) {
+                    for(SapMessage request : step.requests) {
+                        if(request != null) {
+                            Log.i(TAG, "Writing request: " + SapMessage.getMsgTypeName(request.getMsgType()));
+                            writeSapMessage(request, false); /* write the message without flushing */
+                        }
+                    }
+                    writeSapMessage(null, true); /* flush the pipe */
+                }
+
+                /* Handle and validate all responses - if any */
+                if(step.hasResponse() == true) {
+                    done = false;
+                    boolean foundMatch = false;
+                    SapMessage responseMatch;
+                    while(!done) {
+                        for(SapMessage response : step.responses) {
+                            if(response != null)
+                                Log.i(TAG, "Waiting for the response: " + SapMessage.getMsgTypeName(response.getMsgType()));
+                        }
+                        inMsg = readSapMessage();
+                        if(inMsg != null)
+                            Log.i(TAG, "Read message: " + SapMessage.getMsgTypeName(inMsg.getMsgType()));
+                        else
+                            assertTrue("Failed to read message.", false);
+
+                        responseMatch = null;
+                        for(SapMessage response : step.responses) {
+                            if(response != null
+                                    && inMsg.getMsgType() == response.getMsgType()
+                                    && compareSapMessages(inMsg, response) == true) {
+                                foundMatch = true;
+                                responseMatch = response;
+                                break;
+                            }
+                        }
+
+                        if(responseMatch != null)
+                            step.responses.remove(responseMatch);
+
+                        /* If we are expecting no more responses for this step, continue. */
+                        if(step.hasResponse() != true) {
+                            done = true;
+                        }
+                        /* Ensure what we received was expected */
+                        assertTrue("wrong message received.", foundMatch);
+                    }
+                }
+            }
+            return true;
+        }
+
+        private void startTimer() {
+            Message timeoutMessage = messageHandler.obtainMessage(MSG_ID_TIMEOUT);
+            messageHandler.sendMessageDelayed(timeoutMessage, TIMEOUT_VALUE);
+        }
+
+        private void stopTimer() {
+            messageHandler.removeMessages(MSG_ID_TIMEOUT);
+        }
+
+        /**
+         * Compare two messages by comparing each member variable
+         * @param received message
+         * @param expected message
+         * @return true if equal, else false
+         */
+        private boolean compareSapMessages(SapMessage received, SapMessage expected) {
+            boolean retVal = true;
+            if(expected.getCardReaderStatus() != -1 && received.getCardReaderStatus() != expected.getCardReaderStatus()) {
+                Log.i(TAG, "received.getCardReaderStatus() != expected.getCardReaderStatus() "
+                            + received.getCardReaderStatus() +" != " + expected.getCardReaderStatus());
+                retVal = false;
+            }
+            if(received.getConnectionStatus() != expected.getConnectionStatus()) {
+                Log.i(TAG, "received.getConnectionStatus() != expected.getConnectionStatus() "
+                            + received.getConnectionStatus() +" != " + expected.getConnectionStatus());
+                retVal = false;
+            }
+            if(received.getDisconnectionType() != expected.getDisconnectionType()) {
+                Log.i(TAG, "received.getDisconnectionType() != expected.getDisconnectionType() "
+                            + received.getDisconnectionType() +" != " + expected.getDisconnectionType());
+                retVal = false;
+            }
+            if(received.getMaxMsgSize() != expected.getMaxMsgSize()) {
+                Log.i(TAG, "received.getMaxMsgSize() != expected.getMaxMsgSize() "
+                        + received.getMaxMsgSize() +" != " + expected.getMaxMsgSize());
+                retVal = false;
+            }
+            if(received.getMsgType() != expected.getMsgType()) {
+                Log.i(TAG, "received.getMsgType() != expected.getMsgType() "
+                        + received.getMsgType() +" != " + expected.getMsgType());
+                retVal = false;
+            }
+            if(received.getResultCode() != expected.getResultCode()) {
+                Log.i(TAG, "received.getResultCode() != expected.getResultCode() "
+                        + received.getResultCode() +" != " + expected.getResultCode());
+                retVal = false;
+            }
+            if(received.getStatusChange() != expected.getStatusChange()) {
+                Log.i(TAG, "received.getStatusChange() != expected.getStatusChange() "
+                        + received.getStatusChange() +" != " + expected.getStatusChange());
+                retVal = false;
+            }
+            if(received.getTransportProtocol() != expected.getTransportProtocol()) {
+                Log.i(TAG, "received.getTransportProtocol() != expected.getTransportProtocol() "
+                        + received.getTransportProtocol() +" != " + expected.getTransportProtocol());
+                retVal = false;
+            }
+            if(!Arrays.equals(received.getApdu(), expected.getApdu())) {
+                Log.i(TAG, "received.getApdu() != expected.getApdu() "
+                        + Arrays.toString(received.getApdu()) +" != " + Arrays.toString(expected.getApdu()));
+                retVal = false;
+            }
+            if(!Arrays.equals(received.getApdu7816(), expected.getApdu7816())) {
+                Log.i(TAG, "received.getApdu7816() != expected.getApdu7816() "
+                        + Arrays.toString(received.getApdu7816()) +" != " + Arrays.toString(expected.getApdu7816()));
+                retVal = false;
+            }
+            if(expected.getApduResp() != null && !Arrays.equals(received.getApduResp(), expected.getApduResp())) {
+                Log.i(TAG, "received.getApduResp() != expected.getApduResp() "
+                        + Arrays.toString(received.getApduResp()) +" != " + Arrays.toString(expected.getApduResp()));
+                retVal = false;
+            }
+            if(expected.getAtr() != null && !Arrays.equals(received.getAtr(), expected.getAtr())) {
+                Log.i(TAG, "received.getAtr() != expected.getAtr() "
+                        + Arrays.toString(received.getAtr()) +" != " + Arrays.toString(expected.getAtr()));
+                retVal = false;
+            }
+            return retVal;
+        }
+
+        private SapMessage readSapMessage() throws IOException {
+            startTimer();
+            int requestType = inStream.read();
+            Log.i(TAG,"Received message with type: " + SapMessage.getMsgTypeName(requestType));
+            SapMessage msg = SapMessage.readMessage(requestType, inStream);
+            stopTimer();
+            if(requestType != -1) {
+                return msg;
+            } else
+            {
+                assertTrue("Reached EOF too early...", false);
+            }
+            return msg;
+        }
+
+        private void writeSapMessage(SapMessage message, boolean flush) throws IOException {
+            startTimer();
+            if(message != null)
+                message.write(outStream);
+            if(flush == true)
+                outStream.flush();
+            stopTimer();
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+
+            Log.i(TAG,"Handling message ID: " + msg.what);
+
+            switch(msg.what) {
+            case MSG_ID_TIMEOUT:
+                Log.w(TAG, "Timeout occured!");
+                try {
+                    inStream.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "failed to close inStream", e);
+                }
+                try {
+                    outStream.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "failed to close outStream", e);
+                }
+                break;
+            default:
+                /* Message not handled */
+                return false;
+            }
+            return true; // Message handles
+        }
+
+    }
+
+}
diff --git a/tests/src/com/android/bluetooth/tests/SapSocketTest.java b/tests/src/com/android/bluetooth/tests/SapSocketTest.java
new file mode 100644 (file)
index 0000000..9bd8031
--- /dev/null
@@ -0,0 +1,174 @@
+package com.android.bluetooth.tests;
+
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import org.android.btsap.SapApi;
+import org.android.btsap.SapApi.MsgHeader;
+import org.android.btsap.SapApi.RIL_SIM_SAP_CONNECT_REQ;
+
+import com.google.protobuf.micro.ByteStringMicro;
+import com.google.protobuf.micro.CodedOutputStreamMicro;
+import com.google.protobuf.micro.CodedInputStreamMicro;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.util.Arrays;
+
+public class SapSocketTest extends AndroidTestCase {
+
+    protected static String TAG = "SapSocketTest";
+    protected static final boolean D = true;
+
+    private static final String SOCKET_NAME_RIL_BT = "sap_uim_socket1";
+
+    public SapSocketTest() {
+        super();
+    }
+
+    private void writeLegacyLength(int length, OutputStream rawOut) throws IOException {
+        byte[] dataLength = new byte[4];
+        dataLength[0] = dataLength[1] = 0;
+        dataLength[2] = (byte)((length >> 8) & 0xff);
+        dataLength[3] = (byte)((length) & 0xff);
+        rawOut.write(dataLength);
+    }
+
+    private void dumpMsgHeader(MsgHeader msg){
+        Log.d(TAG,"MsgHeader: ID =   " + msg.getId());
+        Log.d(TAG,"           Type=  " + msg.getType());
+        Log.d(TAG,"           Token= " + msg.getToken());
+        Log.d(TAG,"           Error= " + msg.getError());
+        Log.d(TAG,"           Length=" + msg.getSerializedSize());
+        if(msg.hasPayload()){
+        Log.d(TAG,"Payload:   Length=" + msg.getPayload().size());
+        Log.d(TAG,"           Data=  " + Arrays.toString(msg.getPayload().toByteArray()));
+        }
+
+    }
+    private void readLegacyLength(InputStream rawIn) throws IOException{
+        byte[] buffer = new byte[4];
+        int countRead;
+        int offset;
+        int remaining;
+        int messageLength;
+
+        // Read in the length of the message
+        offset = 0;
+        remaining = 4;
+        do {
+            countRead = rawIn.read(buffer, offset, remaining);
+
+            if (countRead < 0 ) {
+                Log.e(TAG, "Hit EOS reading message length");
+                return;
+            }
+
+            offset += countRead;
+            remaining -= countRead;
+        } while (remaining > 0);
+
+        messageLength = ((buffer[0] & 0xff) << 24)
+                | ((buffer[1] & 0xff) << 16)
+                | ((buffer[2] & 0xff) << 8)
+                | (buffer[3] & 0xff);
+
+        Log.d(TAG, "The length is: " + messageLength + " - discarding as we do not need it");
+
+    }
+
+/**
+Precondition:
+Add the sap_uim_socket1 to rild in init.rc:
+    socket sap_uim_socket1 stream 666 root bluetooth
+
+Ensure the socket is present in /dev/socket:
+srw-rw-rw- root     bluetooth          1970-04-16 06:27 sap_uim_socket1
+
+Build:
+mmm packages/apps/Bluetooth/tests
+
+rebuild with a make in the root folder to get the
+android.test.InstrumentationTestRunner class included.
+
+Run the test(remove line breaks):
+adb shell am instrument -w -e class com.android.bluetooth.
+tests.SapSocketTest#testSapServerConnectSimple com.android.
+bluetooth.tests/android.test.InstrumentationTestRunner
+
+Validate you do not get a permission denied IOException.
+
+Validate you do not get an error in the kernel log:
+type=1400 audit(1404244298.582:25): avc:  denied  { write }
+for  pid=2421 comm="ationTestRunner" name="sap_uim_socket1"
+dev="tmpfs" ino=6703 scontext=u:r:bluetooth:s0
+tcontext=u:object_r:socket_device:s0 tclass=sock_file
+*/
+
+    /**
+     * Precondition: Add the sap_uim_socket1 to rild in init.rc: socket
+     * sap_uim_socket1 stream 666 root bluetooth
+     *
+     * Ensure the socket is present in /dev/socket: srw-rw-rw- root bluetooth
+     * 1970-04-16 06:27 sap_uim_socket1
+     *
+     * Build: mmm packages/apps/Bluetooth/tests
+     *
+     * rebuild with a make in the root folder to get the
+     * android.test.InstrumentationTestRunner class included.
+     *
+     * Run the test(remove line breaks): adb shell am instrument -w -e class
+     * com.android.bluetooth. tests.SapSocketTest#testSapServerConnectSimple
+     * com.android. bluetooth.tests/android.test.InstrumentationTestRunner
+     *
+     * Validate you do not get a permission denied IOException.
+     *
+     * Validate you do not get an error in the kernel log: type=1400
+     * audit(1404244298.582:25): avc: denied { write } for pid=2421
+     * comm="ationTestRunner" name="sap_uim_socket1" dev="tmpfs" ino=6703
+     * scontext=u:r:bluetooth:s0 tcontext=u:object_r:socket_device:s0
+     * tclass=sock_file
+     */
+    public void testSapServerConnectSimple() {
+        LocalSocketAddress address;
+        LocalSocket rilSocket = new LocalSocket();
+        try {
+            address = new LocalSocketAddress(SOCKET_NAME_RIL_BT,
+                    LocalSocketAddress.Namespace.RESERVED);
+            rilSocket.connect(address);
+            CodedInputStreamMicro in = CodedInputStreamMicro.newInstance(rilSocket.getInputStream());
+            OutputStream rawOut = rilSocket.getOutputStream();
+            CodedOutputStreamMicro out = CodedOutputStreamMicro.newInstance(rilSocket.getOutputStream());
+            InputStream rawIn = rilSocket.getInputStream();
+            int rilSerial = 1;
+            SapApi.MsgHeader msg = new MsgHeader();
+            /* Common variables for all requests */
+            msg.setToken(rilSerial);
+            msg.setType(SapApi.REQUEST);
+            msg.setError(SapApi.RIL_E_UNUSED);
+            SapApi.RIL_SIM_SAP_CONNECT_REQ reqMsg = new RIL_SIM_SAP_CONNECT_REQ();
+            reqMsg.setMaxMessageSize(1234);
+            msg.setId(SapApi.RIL_SIM_SAP_CONNECT);
+            msg.setPayload(ByteStringMicro.copyFrom(reqMsg.toByteArray()));
+            writeLegacyLength(msg.getSerializedSize(), rawOut);
+            msg.writeTo(out);
+            out.flush();
+            readLegacyLength(rawIn);
+            msg = MsgHeader.parseFrom(in);
+            dumpMsgHeader(msg);
+            assertTrue("Invalid response type", msg.getType()== SapApi.RESPONSE);
+            assertTrue("Invalid response id", msg.getId()== SapApi.RIL_SIM_SAP_CONNECT);
+        } catch (IOException e){
+            Log.e(TAG, "IOException:", e);
+            assertTrue("Failed to connect to the socket " + SOCKET_NAME_RIL_BT + ":" + e, false);
+        } finally {
+            try {
+                rilSocket.close();
+            } catch (IOException e2) {}
+
+        }
+    }
+}