Update Bluetooth Client to Support AIDL

Tag: #feature
Bug: 241969533
Test: atest BluetoothInstrumentationTests
Change-Id: I9eec18393f0e60555a712a5029f5c6a4a7d6f5e8
diff --git a/android/app/Android.bp b/android/app/Android.bp
index 4c45dff..bd7bc18 100644
--- a/android/app/Android.bp
+++ b/android/app/Android.bp
@@ -153,6 +153,7 @@
     ],
     static_libs: [
         "android.hardware.radio-V1.0-java",
+        "android.hardware.radio.sap-V1-java",
         "androidx.core_core",
         "androidx.legacy_legacy-support-v4",
         "androidx.lifecycle_lifecycle-livedata",
diff --git a/android/app/src/com/android/bluetooth/sap/ISapRilReceiver.java b/android/app/src/com/android/bluetooth/sap/ISapRilReceiver.java
new file mode 100644
index 0000000..01d4c29
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/sap/ISapRilReceiver.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.sap;
+
+import android.hardware.radio.sap.ISap;
+
+/**
+ * ISapRilReceiver is used to send messages
+ */
+public interface ISapRilReceiver extends ISap {
+    /**
+     * Set mSapProxy to null
+     */
+    void resetSapProxy();
+
+    /**
+     * Notify SapServer that this class is ready for shutdown.
+     */
+    void notifyShutdown();
+
+    /**
+     * Notify SapServer that the RIL socket is connected
+     */
+    void sendRilConnectMessage();
+
+    /**
+     * Get mSapProxyLock
+     */
+    Object getSapProxyLock();
+
+    /**
+     * Verifies mSapProxy is not null
+     */
+    boolean isProxyValid();
+}
diff --git a/android/app/src/com/android/bluetooth/sap/SapMessage.java b/android/app/src/com/android/bluetooth/sap/SapMessage.java
index df3c1cf..c88b61b 100644
--- a/android/app/src/com/android/bluetooth/sap/SapMessage.java
+++ b/android/app/src/com/android/bluetooth/sap/SapMessage.java
@@ -1,14 +1,12 @@
 package com.android.bluetooth.sap;
 
-import android.hardware.radio.V1_0.ISap;
-import android.hardware.radio.V1_0.SapApduType;
-import android.hardware.radio.V1_0.SapTransferProtocol;
+import android.hardware.radio.sap.SapApduType;
+import android.hardware.radio.sap.SapTransferProtocol;
 import android.os.RemoteException;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 
-import com.google.protobuf.micro.CodedOutputStreamMicro;
 import com.google.protobuf.micro.InvalidProtocolBufferMicroException;
 
 import org.android.btsap.SapApi;
@@ -26,7 +24,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -35,7 +32,6 @@
  * SapMessage is used for incoming and outgoing messages.
  *
  * For incoming messages
- *
  */
 public class SapMessage {
 
@@ -194,6 +190,7 @@
 
     /**
      * Create a SapMessage
+     *
      * @param msgType the SAP message type
      */
     public SapMessage(int msgType) {
@@ -374,8 +371,9 @@
 
     /**
      * 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
+     * @param is          the input stream to read the data from
      * @return the resulting message, or null if an error occurs
      */
     @SuppressWarnings("unused")
@@ -443,9 +441,10 @@
 
     /**
      * Blocking read of an entire array of data.
-     * @param is the input stream to read from
+     *
+     * @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.
+     *               determine how many bytes will be read.
      */
     private static void read(InputStream is, byte[] buffer) throws IOException {
         int bytesToRead = buffer.length;
@@ -463,7 +462,8 @@
 
     /**
      * Skip a number of bytes in an InputStream.
-     * @param is the input stream
+     *
+     * @param is    the input stream
      * @param count the number of bytes to skip
      * @throws IOException In case of reaching EOF or a stream error
      */
@@ -477,10 +477,10 @@
      * 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
+     * @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;
@@ -621,9 +621,10 @@
 
     /**
      * 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 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
      */
@@ -656,8 +657,9 @@
 
     /**
      * Writes a byte[] parameter of any length.
-     * @param os The BufferedOutputStream to write to.
-     * @param id The Parameter ID
+     *
+     * @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
      */
@@ -729,18 +731,11 @@
      * RILD Interface message conversion functions.
      ***************************************************************************/
 
-    private ArrayList<Byte> primitiveArrayToContainerArrayList(byte[] arr) {
-        ArrayList<Byte> arrayList = new ArrayList<>(arr.length);
-        for (byte b : arr) {
-            arrayList.add(b);
-        }
-        return arrayList;
-    }
 
     /**
      * Send the message by calling corresponding ISap api.
      */
-    public void send(ISap sapProxy) throws RemoteException, RuntimeException {
+    public void send(ISapRilReceiver sapProxy) throws RemoteException, RuntimeException {
         int rilSerial = sNextSerial.getAndIncrement();
 
         Log.e(TAG, "callISapReq: called for mMsgType " + mMsgType + " rilSerial " + rilSerial);
@@ -763,13 +758,13 @@
             }
             case ID_TRANSFER_APDU_REQ: {
                 int type;
-                ArrayList<Byte> command;
+                byte[] command;
                 if (mApdu != null) {
                     type = SapApduType.APDU;
-                    command = primitiveArrayToContainerArrayList(mApdu);
+                    command = mApdu;
                 } else if (mApdu7816 != null) {
                     type = SapApduType.APDU7816;
-                    command = primitiveArrayToContainerArrayList(mApdu7816);
+                    command = mApdu7816;
                 } else {
                     Log.e(TAG, "Missing Apdu parameter in TRANSFER_APDU_REQ");
                     throw new IllegalArgumentException();
@@ -976,7 +971,7 @@
                 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. */
+                        /* resMsg.getType is unused as the client knows the type of request used. */
                         if (resMsg.hasApduResponse()) {
                             mApduResp = resMsg.getApduResponse().toByteArray();
                         }
diff --git a/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java b/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
index f7eb8f4..acf58ce 100644
--- a/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
+++ b/android/app/src/com/android/bluetooth/sap/SapRilReceiver.java
@@ -1,31 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.android.bluetooth.sap;
 
-import android.hardware.radio.V1_0.ISap;
-import android.hardware.radio.V1_0.ISapCallback;
+import android.hardware.radio.sap.ISap;
+import android.hardware.radio.sap.ISapCallback;
 import android.os.Handler;
-import android.os.HwBinder;
+import android.os.IBinder;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.util.Log;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
+import com.android.modules.utils.build.SdkLevel;
+
 import java.util.concurrent.atomic.AtomicLong;
 
-public class SapRilReceiver {
+/**
+ * SapRiilReceiver is the AIDL implementation of ISapRilReceiver
+ */
+public class SapRilReceiver implements ISapRilReceiver {
     private static final String TAG = "SapRilReceiver";
     public static final boolean DEBUG = true;
     public static final boolean VERBOSE = true;
 
-    private static final String SERVICE_NAME_RIL_BT = "slot1";
-    // match with constant in ril.cpp - as in RIL.java
-    private static final int SOCKET_OPEN_RETRY_MILLIS = 4 * 1000;
+    // todo: add support for slot2 and slot3
+    private static final String HAL_INSTANCE_NAME = ISap.DESCRIPTOR + "/slot1";
 
     SapCallback mSapCallback;
     volatile ISap mSapProxy = null;
-    Object mSapProxyLock = new Object();
+    final Object mSapProxyLock = new Object();
     final AtomicLong mSapProxyCookie = new AtomicLong(0);
     final SapProxyDeathRecipient mSapProxyDeathRecipient;
 
@@ -35,15 +52,129 @@
     public static final int RIL_MAX_COMMAND_BYTES = (8 * 1024);
     public byte[] buffer = new byte[RIL_MAX_COMMAND_BYTES];
 
-    final class SapProxyDeathRecipient implements HwBinder.DeathRecipient {
+    /**
+     * TRANSFER_APDU_REQ from SAP 1.1 spec 5.1.6
+     *
+     * @param serial  Id to match req-resp. Resp must include same serial.
+     * @param type    APDU command type
+     * @param command CommandAPDU/CommandAPDU7816 parameter depending on type
+     */
+    @Override
+    public void apduReq(int serial, int type, byte[] command) throws android.os.RemoteException {
+        mSapProxy.apduReq(serial, type, command);
+    }
+
+    /**
+     * CONNECT_REQ from SAP 1.1 spec 5.1.1
+     *
+     * @param serial          Id to match req-resp. Resp must include same serial.
+     * @param maxMsgSizeBytes MaxMsgSize to be used for SIM Access Profile connection
+     */
+    @Override
+    public void connectReq(int serial, int maxMsgSizeBytes) throws android.os.RemoteException {
+        mSapProxy.connectReq(serial, maxMsgSizeBytes);
+    }
+
+    /**
+     * DISCONNECT_REQ from SAP 1.1 spec 5.1.3
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void disconnectReq(int serial) throws android.os.RemoteException {
+        mSapProxy.disconnectReq(serial);
+    }
+
+    /**
+     * POWER_SIM_OFF_REQ and POWER_SIM_ON_REQ from SAP 1.1 spec 5.1.10 + 5.1.12
+     *
+     * @param serial  Id to match req-resp. Resp must include same serial.
+     * @param powerOn true for on, false for off
+     */
+    @Override
+    public void powerReq(int serial, boolean powerOn) throws android.os.RemoteException {
+        mSapProxy.powerReq(serial, powerOn);
+    }
+
+    /**
+     * RESET_SIM_REQ from SAP 1.1 spec 5.1.14
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void resetSimReq(int serial) throws android.os.RemoteException {
+        mSapProxy.resetSimReq(serial);
+    }
+
+    /**
+     * Set callback that has response and unsolicited indication functions
+     *
+     * @param sapCallback Object containing response and unosolicited indication callbacks
+     */
+    @Override
+    public void setCallback(android.hardware.radio.sap.ISapCallback sapCallback)
+            throws android.os.RemoteException {
+        Log.e(TAG, "setCallback should never be called");
+    }
+
+    /**
+     * SET_TRANSPORT_PROTOCOL_REQ from SAP 1.1 spec 5.1.20
+     *
+     * @param serial           Id to match req-resp. Resp must include same serial.
+     * @param transferProtocol Transport Protocol
+     */
+    @Override
+    public void setTransferProtocolReq(int serial, int transferProtocol)
+            throws android.os.RemoteException {
+        mSapProxy.setTransferProtocolReq(serial, transferProtocol);
+    }
+
+    /**
+     * TRANSFER_ATR_REQ from SAP 1.1 spec 5.1.8
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void transferAtrReq(int serial) throws android.os.RemoteException {
+        mSapProxy.transferAtrReq(serial);
+    }
+
+    /**
+     * TRANSFER_CARD_READER_STATUS_REQ from SAP 1.1 spec 5.1.17
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void transferCardReaderStatusReq(int serial) throws android.os.RemoteException {
+        mSapProxy.transferCardReaderStatusReq(serial);
+    }
+
+    @Override
+    public int getInterfaceVersion() {
+        Log.e(TAG, "getInterfaceVersion should never be called");
+        return 0;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        Log.e(TAG, "getInterfaceHash should never be called");
+        return "";
+    }
+
+    @Override
+    public android.os.IBinder asBinder() {
+        Log.e(TAG, "asBinder should never be called");
+        return null;
+    }
+
+    final class SapProxyDeathRecipient implements IBinder.DeathRecipient {
         @Override
-        public void serviceDied(long cookie) {
+        public void binderDied() {
             // Deal with service going away
             Log.d(TAG, "serviceDied");
             // todo: temp hack to send delayed message so that rild is back up by then
-            // mSapHandler.sendMessage(mSapHandler.obtainMessage(EVENT_SAP_PROXY_DEAD, cookie));
             mSapServerMsgHandler.sendMessageDelayed(
-                    mSapServerMsgHandler.obtainMessage(SapServer.SAP_PROXY_DEAD, cookie),
+                    mSapServerMsgHandler.obtainMessage(SapServer.SAP_PROXY_DEAD, (long) 0),
                     SapServer.ISAP_GET_SERVICE_DELAY_MILLIS);
         }
     }
@@ -100,25 +231,25 @@
         }
 
         @Override
-        public void apduResponse(int token, int resultCode, ArrayList<Byte> apduRsp) {
+        public void apduResponse(int token, int resultCode, byte[] apduRsp) {
             Log.d(TAG, "apduResponse: token " + token);
             SapService.notifyUpdateWakeLock(mSapServiceHandler);
             SapMessage sapMessage = new SapMessage(SapMessage.ID_TRANSFER_APDU_RESP);
             sapMessage.setResultCode(resultCode);
             if (resultCode == SapMessage.RESULT_OK) {
-                sapMessage.setApduResp(arrayListToPrimitiveArray(apduRsp));
+                sapMessage.setApduResp(apduRsp);
             }
             removeOngoingReqAndSendMessage(token, sapMessage);
         }
 
         @Override
-        public void transferAtrResponse(int token, int resultCode, ArrayList<Byte> atr) {
+        public void transferAtrResponse(int token, int resultCode, byte[] atr) {
             Log.d(TAG, "transferAtrResponse: token " + token + " resultCode " + resultCode);
             SapService.notifyUpdateWakeLock(mSapServiceHandler);
             SapMessage sapMessage = new SapMessage(SapMessage.ID_TRANSFER_ATR_RESP);
             sapMessage.setResultCode(resultCode);
             if (resultCode == SapMessage.RESULT_OK) {
-                sapMessage.setAtr(arrayListToPrimitiveArray(atr));
+                sapMessage.setAtr(atr);
             }
             removeOngoingReqAndSendMessage(token, sapMessage);
         }
@@ -195,20 +326,39 @@
             sapMessage.setResultCode(resultCode);
             removeOngoingReqAndSendMessage(token, sapMessage);
         }
-    }
 
-    public static byte[] arrayListToPrimitiveArray(List<Byte> bytes) {
-        byte[] ret = new byte[bytes.size()];
-        for (int i = 0; i < ret.length; i++) {
-            ret[i] = bytes.get(i);
+        @Override
+        public String getInterfaceHash() {
+            return ISapCallback.HASH;
         }
-        return ret;
+
+        @Override
+        public int getInterfaceVersion() {
+            return ISapCallback.VERSION;
+        }
     }
 
+    @Override
     public Object getSapProxyLock() {
         return mSapProxyLock;
     }
 
+    @Override
+    public boolean isProxyValid() {
+        // Only call when synchronized with getSapProxyLock
+        return mSapProxy != null;
+    }
+
+    /**
+     * Check if AIDL is supported
+     */
+    public static boolean isAidlSupported() {
+        return SdkLevel.isAtLeastU() && ServiceManager.isDeclared(HAL_INSTANCE_NAME);
+    }
+
+    /**
+     * Obtain a valid sapProxy
+     */
     public ISap getSapProxy() {
         synchronized (mSapProxyLock) {
             if (mSapProxy != null) {
@@ -216,10 +366,11 @@
             }
 
             try {
-                mSapProxy = ISap.getService(SERVICE_NAME_RIL_BT);
+                IBinder service = ServiceManager.waitForDeclaredService(HAL_INSTANCE_NAME);
+                mSapProxy = ISap.Stub.asInterface(service);
                 if (mSapProxy != null) {
-                    mSapProxy.linkToDeath(mSapProxyDeathRecipient,
-                            mSapProxyCookie.incrementAndGet());
+                    service.linkToDeath(mSapProxyDeathRecipient,
+                            /* flags= */ 0);
                     mSapProxy.setCallback(mSapCallback);
                 } else {
                     Log.e(TAG, "getSapProxy: mSapProxy == null");
@@ -240,16 +391,17 @@
         }
     }
 
+    @Override
     public void resetSapProxy() {
         synchronized (mSapProxyLock) {
             if (DEBUG) Log.d(TAG, "resetSapProxy :" + mSapProxy);
-            try {
-                if (mSapProxy != null) {
-                    mSapProxy.unlinkToDeath(mSapProxyDeathRecipient);
-                }
-            } catch (RemoteException | RuntimeException e) {
-                Log.e(TAG, "resetSapProxy: exception: " + e);
+            if (mSapProxy == null) {
+                return;
             }
+            if (mSapProxy.asBinder() == null) {
+                Log.e(TAG, "asdf asBinder is null");
+            }
+            mSapProxy.asBinder().unlinkToDeath(mSapProxyDeathRecipient, /* flags= */ 0);
             mSapProxy = null;
         }
     }
@@ -264,25 +416,26 @@
         }
     }
 
-    /**
-     * Notify SapServer that this class is ready for shutdown.
-     */
-    void notifyShutdown() {
+    @Override
+    public void notifyShutdown() {
         if (DEBUG) {
             Log.i(TAG, "notifyShutdown()");
         }
-        // If we are already shutdown, don't bother sending a notification.
         synchronized (mSapProxyLock) {
+            // If we are already shutdown, don't bother sending a notification.
             if (mSapProxy != null) {
                 sendShutdownMessage();
             }
+            resetSapProxy();
+
+            // todo: rild should be back up since the message was sent with a delay. this is
+            // a hack.
+            getSapProxy();
         }
     }
 
-    /**
-     * Notify SapServer that the RIL socket is connected
-     */
-    void sendRilConnectMessage() {
+    @Override
+    public void sendRilConnectMessage() {
         if (mSapServerMsgHandler != null) {
             mSapServerMsgHandler.sendEmptyMessage(SapServer.SAP_MSG_RIL_CONNECT);
         }
@@ -290,6 +443,7 @@
 
     /**
      * Send reply (solicited) message from the RIL to the Sap Server Handler Thread
+     *
      * @param sapMsg The message to send
      */
     private void sendClientMessage(SapMessage sapMsg) {
@@ -308,6 +462,7 @@
 
     /**
      * Send indication (unsolicited) message from RIL to the Sap Server Handler Thread
+     *
      * @param sapMsg The message to send
      */
     private void sendRilIndMessage(SapMessage sapMsg) {
diff --git a/android/app/src/com/android/bluetooth/sap/SapRilReceiverHidl.java b/android/app/src/com/android/bluetooth/sap/SapRilReceiverHidl.java
new file mode 100644
index 0000000..30de0bb
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/sap/SapRilReceiverHidl.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.sap;
+
+import android.hardware.radio.V1_0.ISap;
+import android.hardware.radio.V1_0.ISapCallback;
+import android.os.Handler;
+import android.os.IHwBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * SapRiilReceiverHidl is the HIDL implementation of ISapRilReceiver
+ */
+public class SapRilReceiverHidl implements ISapRilReceiver {
+    private static final String TAG = "SapRilReceiver";
+    public static final boolean DEBUG = true;
+    public static final boolean VERBOSE = true;
+
+    // todo: add support for slot2 and slot3
+    private static final String SERVICE_NAME_RIL_BT = "slot1";
+
+    SapCallback mSapCallback;
+    volatile ISap mSapProxy = null;
+    final Object mSapProxyLock = new Object();
+    final AtomicLong mSapProxyCookie = new AtomicLong(0);
+    final SapProxyDeathRecipient mSapProxyDeathRecipient;
+
+    private Handler mSapServerMsgHandler = null;
+    private Handler mSapServiceHandler = null;
+
+    public static final int RIL_MAX_COMMAND_BYTES = (8 * 1024);
+    public byte[] buffer = new byte[RIL_MAX_COMMAND_BYTES];
+
+    private ArrayList<Byte> primitiveArrayToContainerArrayList(byte[] arr) {
+        ArrayList<Byte> arrayList = new ArrayList<>(arr.length);
+        for (byte b : arr) {
+            arrayList.add(b);
+        }
+        return arrayList;
+    }
+
+    /**
+     * TRANSFER_APDU_REQ from SAP 1.1 spec 5.1.6
+     *
+     * @param serial  Id to match req-resp. Resp must include same serial.
+     * @param type    APDU command type
+     * @param command CommandAPDU/CommandAPDU7816 parameter depending on type
+     */
+    @Override
+    public void apduReq(int serial, int type, byte[] command) throws RemoteException {
+        ArrayList<Byte> commandHidl = primitiveArrayToContainerArrayList(command);
+        mSapProxy.apduReq(serial, type, commandHidl);
+
+    }
+
+    /**
+     * CONNECT_REQ from SAP 1.1 spec 5.1.1
+     *
+     * @param serial          Id to match req-resp. Resp must include same serial.
+     * @param maxMsgSizeBytes MaxMsgSize to be used for SIM Access Profile connection
+     */
+    @Override
+    public void connectReq(int serial, int maxMsgSizeBytes) throws RemoteException {
+        mSapProxy.connectReq(serial, maxMsgSizeBytes);
+    }
+
+    /**
+     * DISCONNECT_REQ from SAP 1.1 spec 5.1.3
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void disconnectReq(int serial) throws RemoteException {
+        mSapProxy.disconnectReq(serial);
+    }
+
+    /**
+     * POWER_SIM_OFF_REQ and POWER_SIM_ON_REQ from SAP 1.1 spec 5.1.10 + 5.1.12
+     *
+     * @param serial  Id to match req-resp. Resp must include same serial.
+     * @param powerOn true for on, false for off
+     */
+    @Override
+    public void powerReq(int serial, boolean powerOn) throws RemoteException {
+        mSapProxy.powerReq(serial, powerOn);
+    }
+
+    /**
+     * RESET_SIM_REQ from SAP 1.1 spec 5.1.14
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void resetSimReq(int serial) throws RemoteException {
+        mSapProxy.resetSimReq(serial);
+    }
+
+    /**
+     * Set callback that has response and unsolicited indication functions
+     *
+     * @param sapCallback Object containing response and unosolicited indication callbacks
+     */
+    @Override
+    public void setCallback(android.hardware.radio.sap.ISapCallback sapCallback)
+            throws RemoteException {
+        Log.e(TAG, "setCallback should never be called");
+    }
+
+    /**
+     * SET_TRANSPORT_PROTOCOL_REQ from SAP 1.1 spec 5.1.20
+     *
+     * @param serial           Id to match req-resp. Resp must include same serial.
+     * @param transferProtocol Transport Protocol
+     */
+    @Override
+    public void setTransferProtocolReq(int serial, int transferProtocol) throws RemoteException {
+        mSapProxy.setTransferProtocolReq(serial, transferProtocol);
+    }
+
+    /**
+     * TRANSFER_ATR_REQ from SAP 1.1 spec 5.1.8
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void transferAtrReq(int serial) throws RemoteException {
+        mSapProxy.transferAtrReq(serial);
+    }
+
+    /**
+     * TRANSFER_CARD_READER_STATUS_REQ from SAP 1.1 spec 5.1.17
+     *
+     * @param serial Id to match req-resp. Resp must include same serial.
+     */
+    @Override
+    public void transferCardReaderStatusReq(int serial) throws RemoteException {
+        mSapProxy.transferCardReaderStatusReq(serial);
+    }
+
+    @Override
+    public int getInterfaceVersion() {
+        Log.e(TAG, "getInterfaceVersion should never be called");
+        return 0;
+    }
+
+    @Override
+    public String getInterfaceHash() {
+        Log.e(TAG, "getInterfaceHash should never be called");
+        return "";
+    }
+
+    @Override
+    public android.os.IBinder asBinder() {
+        Log.e(TAG, "asBinder should never be called");
+        return null;
+    }
+
+    final class SapProxyDeathRecipient implements IHwBinder.DeathRecipient {
+        @Override
+        public void serviceDied(long cookie) {
+            // Deal with service going away
+            Log.d(TAG, "serviceDied");
+            // todo: temp hack to send delayed message so that rild is back up by then
+            // mSapHandler.sendMessage(mSapHandler.obtainMessage(EVENT_SAP_PROXY_DEAD, cookie));
+
+            mSapServerMsgHandler.sendMessageDelayed(
+                    mSapServerMsgHandler.obtainMessage(SapServer.SAP_PROXY_DEAD, cookie),
+                    SapServer.ISAP_GET_SERVICE_DELAY_MILLIS);
+        }
+    }
+
+    private void sendSapMessage(SapMessage sapMessage) {
+        if (sapMessage.getMsgType() < SapMessage.ID_RIL_BASE) {
+            sendClientMessage(sapMessage);
+        } else {
+            sendRilIndMessage(sapMessage);
+        }
+    }
+
+    private void removeOngoingReqAndSendMessage(int token, SapMessage sapMessage) {
+        Integer reqType = SapMessage.sOngoingRequests.remove(token);
+        if (VERBOSE) {
+            Log.d(TAG, "removeOngoingReqAndSendMessage: token " + token + " reqType " + (
+                    reqType == null ? "null" : SapMessage.getMsgTypeName(reqType)));
+        }
+        sendSapMessage(sapMessage);
+    }
+
+    /**
+     * Convert an arrayList to a primitive array
+     *
+     * @param bytes the List to convert
+     */
+    public static byte[] arrayListToPrimitiveArray(List<Byte> bytes) {
+        byte[] ret = new byte[bytes.size()];
+        for (int i = 0; i < ret.length; i++) {
+            ret[i] = bytes.get(i);
+        }
+        return ret;
+    }
+
+    class SapCallback extends ISapCallback.Stub {
+        @Override
+        public void connectResponse(int token, int sapConnectRsp, int maxMsgSize) {
+            Log.d(TAG, "connectResponse: token " + token + " sapConnectRsp " + sapConnectRsp
+                    + " maxMsgSize " + maxMsgSize);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_CONNECT_RESP);
+            sapMessage.setConnectionStatus(sapConnectRsp);
+            if (sapConnectRsp == SapMessage.CON_STATUS_ERROR_MAX_MSG_SIZE_UNSUPPORTED) {
+                sapMessage.setMaxMsgSize(maxMsgSize);
+            }
+            sapMessage.setResultCode(SapMessage.INVALID_VALUE);
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void disconnectResponse(int token) {
+            Log.d(TAG, "disconnectResponse: token " + token);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_DISCONNECT_RESP);
+            sapMessage.setResultCode(SapMessage.INVALID_VALUE);
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void disconnectIndication(int token, int disconnectType) {
+            Log.d(TAG,
+                    "disconnectIndication: token " + token + " disconnectType " + disconnectType);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_RIL_UNSOL_DISCONNECT_IND);
+            sapMessage.setDisconnectionType(disconnectType);
+            sendSapMessage(sapMessage);
+        }
+
+        @Override
+        public void apduResponse(int token, int resultCode, ArrayList<Byte> apduRsp) {
+            Log.d(TAG, "apduResponse: token " + token);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_TRANSFER_APDU_RESP);
+            sapMessage.setResultCode(resultCode);
+            if (resultCode == SapMessage.RESULT_OK) {
+                sapMessage.setApduResp(arrayListToPrimitiveArray(apduRsp));
+            }
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void transferAtrResponse(int token, int resultCode, ArrayList<Byte> atr) {
+            Log.d(TAG, "transferAtrResponse: token " + token + " resultCode " + resultCode);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_TRANSFER_ATR_RESP);
+            sapMessage.setResultCode(resultCode);
+            if (resultCode == SapMessage.RESULT_OK) {
+                sapMessage.setAtr(arrayListToPrimitiveArray(atr));
+            }
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void powerResponse(int token, int resultCode) {
+            Log.d(TAG, "powerResponse: token " + token + " resultCode " + resultCode);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            Integer reqType = SapMessage.sOngoingRequests.remove(token);
+            if (VERBOSE) {
+                Log.d(TAG, "powerResponse: reqType " + (reqType == null ? "null"
+                        : SapMessage.getMsgTypeName(reqType)));
+            }
+            SapMessage sapMessage;
+            if (reqType == SapMessage.ID_POWER_SIM_OFF_REQ) {
+                sapMessage = new SapMessage(SapMessage.ID_POWER_SIM_OFF_RESP);
+            } else if (reqType == SapMessage.ID_POWER_SIM_ON_REQ) {
+                sapMessage = new SapMessage(SapMessage.ID_POWER_SIM_ON_RESP);
+            } else {
+                return;
+            }
+            sapMessage.setResultCode(resultCode);
+            sendSapMessage(sapMessage);
+        }
+
+        @Override
+        public void resetSimResponse(int token, int resultCode) {
+            Log.d(TAG, "resetSimResponse: token " + token + " resultCode " + resultCode);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_RESET_SIM_RESP);
+            sapMessage.setResultCode(resultCode);
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void statusIndication(int token, int status) {
+            Log.d(TAG, "statusIndication: token " + token + " status " + status);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_STATUS_IND);
+            sapMessage.setStatusChange(status);
+            sendSapMessage(sapMessage);
+        }
+
+        @Override
+        public void transferCardReaderStatusResponse(int token, int resultCode,
+                int cardReaderStatus) {
+            Log.d(TAG,
+                    "transferCardReaderStatusResponse: token " + token + " resultCode " + resultCode
+                            + " cardReaderStatus " + cardReaderStatus);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_TRANSFER_CARD_READER_STATUS_RESP);
+            sapMessage.setResultCode(resultCode);
+            if (resultCode == SapMessage.RESULT_OK) {
+                sapMessage.setCardReaderStatus(cardReaderStatus);
+            }
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+
+        @Override
+        public void errorResponse(int token) {
+            Log.d(TAG, "errorResponse: token " + token);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            // Since ERROR_RESP isn't supported by createUnsolicited(), keeping behavior same here
+            // SapMessage sapMessage = new SapMessage(SapMessage.ID_ERROR_RESP);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_RIL_UNKNOWN);
+            sendSapMessage(sapMessage);
+        }
+
+        @Override
+        public void transferProtocolResponse(int token, int resultCode) {
+            Log.d(TAG, "transferProtocolResponse: token " + token + " resultCode " + resultCode);
+            SapService.notifyUpdateWakeLock(mSapServiceHandler);
+            SapMessage sapMessage = new SapMessage(SapMessage.ID_SET_TRANSPORT_PROTOCOL_RESP);
+            sapMessage.setResultCode(resultCode);
+            removeOngoingReqAndSendMessage(token, sapMessage);
+        }
+    }
+
+    @Override
+    public Object getSapProxyLock() {
+        return mSapProxyLock;
+    }
+
+    @Override
+    public boolean isProxyValid() {
+        // Only call when synchronized with getSapProxyLock
+        return mSapProxy != null;
+    }
+
+    /**
+     * Obtain a valid sapProxy
+     */
+    public ISap getSapProxy() {
+        synchronized (mSapProxyLock) {
+            if (mSapProxy != null) {
+                return mSapProxy;
+            }
+
+            try {
+                mSapProxy = ISap.getService(SERVICE_NAME_RIL_BT);
+                if (mSapProxy != null) {
+                    mSapProxy.linkToDeath(mSapProxyDeathRecipient,
+                            mSapProxyCookie.incrementAndGet());
+                    mSapProxy.setCallback(mSapCallback);
+                } else {
+                    Log.e(TAG, "getSapProxy: mSapProxy == null");
+                }
+            } catch (RemoteException | RuntimeException e) {
+                mSapProxy = null;
+                Log.e(TAG, "getSapProxy: exception: " + e);
+            }
+
+            if (mSapProxy == null) {
+                // if service is not up, treat it like death notification to try to get service
+                // again
+                mSapServerMsgHandler.sendMessageDelayed(
+                        mSapServerMsgHandler.obtainMessage(SapServer.SAP_PROXY_DEAD,
+                                mSapProxyCookie.get()), SapServer.ISAP_GET_SERVICE_DELAY_MILLIS);
+            }
+            return mSapProxy;
+        }
+    }
+
+    @Override
+    public void resetSapProxy() {
+        synchronized (mSapProxyLock) {
+            if (DEBUG) Log.d(TAG, "resetSapProxy :" + mSapProxy);
+            try {
+                if (mSapProxy != null) {
+                    mSapProxy.unlinkToDeath(mSapProxyDeathRecipient);
+                }
+            } catch (RemoteException | RuntimeException e) {
+                Log.e(TAG, "resetSapProxy: exception: " + e);
+            }
+            mSapProxy = null;
+        }
+    }
+
+    public SapRilReceiverHidl(Handler sapServerMsgHandler, Handler sapServiceHandler) {
+        mSapServerMsgHandler = sapServerMsgHandler;
+        mSapServiceHandler = sapServiceHandler;
+        mSapCallback = new SapCallback();
+        mSapProxyDeathRecipient = new SapProxyDeathRecipient();
+        synchronized (mSapProxyLock) {
+            mSapProxy = getSapProxy();
+        }
+    }
+
+    @Override
+    public void notifyShutdown() {
+        if (DEBUG) {
+            Log.i(TAG, "notifyShutdown()");
+        }
+        synchronized (mSapProxyLock) {
+            // If we are already shutdown, don't bother sending a notification.
+            if (mSapProxy != null) {
+                sendShutdownMessage();
+            }
+            resetSapProxy();
+
+            // todo: rild should be back up since the message was sent with a delay. this is
+            // a hack.
+            getSapProxy();
+        }
+    }
+
+    @Override
+    public void sendRilConnectMessage() {
+        if (mSapServerMsgHandler != null) {
+            mSapServerMsgHandler.sendEmptyMessage(SapServer.SAP_MSG_RIL_CONNECT);
+        }
+    }
+
+    /**
+     * Send reply (solicited) message from the RIL 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);
+    }
+
+    /**
+     * Send a shutdown signal to SapServer to indicate the
+     */
+    private void sendShutdownMessage() {
+        if (mSapServerMsgHandler != null) {
+            mSapServerMsgHandler.sendEmptyMessage(SapServer.SAP_RIL_SOCK_CLOSED);
+        }
+    }
+
+    /**
+     * Send indication (unsolicited) message from RIL to the Sap Server Handler Thread
+     *
+     * @param sapMsg The message to send
+     */
+    private void sendRilIndMessage(SapMessage sapMsg) {
+        Message newMsg = mSapServerMsgHandler.obtainMessage(SapServer.SAP_MSG_RIL_IND, sapMsg);
+        mSapServerMsgHandler.sendMessage(newMsg);
+    }
+
+    AtomicLong getSapProxyCookie() {
+        return mSapProxyCookie;
+    }
+}
diff --git a/android/app/src/com/android/bluetooth/sap/SapServer.java b/android/app/src/com/android/bluetooth/sap/SapServer.java
index 9dfa22a..e8217c7 100644
--- a/android/app/src/com/android/bluetooth/sap/SapServer.java
+++ b/android/app/src/com/android/bluetooth/sap/SapServer.java
@@ -12,7 +12,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.drawable.Icon;
-import android.hardware.radio.V1_0.ISap;
 import android.os.Handler;
 import android.os.Handler.Callback;
 import android.os.HandlerThread;
@@ -67,7 +66,7 @@
     private BufferedInputStream mRfcommIn = null;
     /* References to the SapRilReceiver object */
     @VisibleForTesting
-    SapRilReceiver mRilBtReceiver = null;
+    ISapRilReceiver mRilBtReceiver = null;
     /* The message handler members */
     @VisibleForTesting
     Handler mSapHandler = null;
@@ -112,9 +111,10 @@
 
     /**
      * 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
+     * @param inStream       The socket input stream
+     * @param outStream      The socket output stream
      */
     public SapServer(Handler serviceHandler, Context context, InputStream inStream,
             OutputStream outStream) {
@@ -185,6 +185,7 @@
      * 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) {
@@ -271,8 +272,9 @@
             PendingIntent pIntentDisconnect =
                     PendingIntent.getBroadcast(mContext, type, sapDisconnectIntent, flags);
             Notification.Action actionDisconnect =
-                   new Notification.Action.Builder(Icon.createWithResource(mContext,
-                   android.R.drawable.stat_sys_data_bluetooth), button, pIntentDisconnect).build();
+                    new Notification.Action.Builder(Icon.createWithResource(mContext,
+                            android.R.drawable.stat_sys_data_bluetooth), button,
+                            pIntentDisconnect).build();
             notification =
                     new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
                             .addAction(actionDisconnect)
@@ -302,9 +304,10 @@
                     pIntentDisconnect).build();
             Notification.Action actionForceDisconnect =
                     new Notification.Action.Builder(Icon.createWithResource(mContext,
-                    android.R.drawable.stat_sys_data_bluetooth),
-                    mContext.getString(R.string.bluetooth_sap_notif_force_disconnect_button),
-                    pIntentForceDisconnect).build();
+                            android.R.drawable.stat_sys_data_bluetooth),
+                            mContext.getString(
+                                    R.string.bluetooth_sap_notif_force_disconnect_button),
+                            pIntentForceDisconnect).build();
             notification =
                     new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
                             .addAction(actionDisconnect)
@@ -351,7 +354,11 @@
             Looper sapLooper = mHandlerThread.getLooper();
             mSapHandler = new Handler(sapLooper, this);
 
-            mRilBtReceiver = new SapRilReceiver(mSapHandler, mSapServiceHandler);
+            if (SapRilReceiver.isAidlSupported()) {
+                mRilBtReceiver = new SapRilReceiver(mSapHandler, mSapServiceHandler);
+            } else {
+                mRilBtReceiver = new SapRilReceiverHidl(mSapHandler, mSapServiceHandler);
+            }
             boolean done = false;
             while (!done) {
                 if (VERBOSE) {
@@ -383,19 +390,19 @@
                                 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 */
+                                /*
+                                 * 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");
                                 }
@@ -411,20 +418,20 @@
                                     clearPendingRilResponses(msg);
                                     changeState(SAP_STATE.DISCONNECTING);
                                     sendRilThreadMessage(msg);
-                                /*cancel the timer for the hard-disconnect intent*/
+                                    /*cancel the timer for the hard-disconnect intent*/
                                     stopDisconnectTimer();
                                 }
                                 msg = null; // No message needs to be sent to RIL
                                 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 */
+                                /* Forward these to the RIL regardless of the state, and clear any
+                                 * pending resp */
                                 clearPendingRilResponses(msg);
                                 break;
                             case SapMessage.ID_SET_TRANSPORT_PROTOCOL_REQ:
-                            /* The RIL might support more protocols that specified in the SAP,
-                             * allow only the valid values. */
+                                /* The RIL might support more protocols that specified in the SAP,
+                                 * allow only the valid values. */
                                 if (mState == SAP_STATE.CONNECTED && msg.getTransportProtocol() != 0
                                         && msg.getTransportProtocol() != 1) {
                                     Log.w(TAG, "Invalid TransportProtocol received:"
@@ -438,8 +445,9 @@
                                 }
                                 // Fall through
                             default:
-                            /* Remaining cases just needs to be forwarded to the RIL unless we are
-                             * in busy state. */
+                                /* 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());
@@ -560,11 +568,11 @@
 
     /**
      * 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.
+     * - 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
      */
@@ -623,6 +631,7 @@
 
     /**
      * Send RFCOMM message to the Sap Server Handler Thread
+     *
      * @param sapMsg The message to send
      */
     @VisibleForTesting
@@ -633,7 +642,6 @@
 
     /**
      * Send a RIL message to the SapServer message handler thread
-     * @param sapMsg
      */
     @VisibleForTesting
     void sendRilThreadMessage(SapMessage sapMsg) {
@@ -643,6 +651,7 @@
 
     /**
      * 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.
      */
     @VisibleForTesting
@@ -657,7 +666,6 @@
     /**
      * Change the SAP Server state.
      * We add thread protection, as we access the state from two threads.
-     * @param newState
      */
     @VisibleForTesting
     void changeState(SAP_STATE newState) {
@@ -675,10 +683,10 @@
 
     /**
      * 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
+     * - 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) {
@@ -695,8 +703,8 @@
                 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. */
+                /* The connection to rild-bt have been established. Store the outStream handle
+                 * and send the connect request. */
                 if (mTestMode != SapMessage.INVALID_VALUE) {
                     SapMessage rilTestModeReq =
                             new SapMessage(SapMessage.ID_RIL_SIM_ACCESS_TEST_REQ);
@@ -724,17 +732,10 @@
                 startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM);
                 break;
             case SAP_PROXY_DEAD:
-                if ((long) msg.obj == mRilBtReceiver.getSapProxyCookie().get()) {
-                    mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */
-                    mRilBtReceiver.resetSapProxy();
-
-                    // todo: rild should be back up since message was sent with a delay. this is
-                    // a hack.
-                    mRilBtReceiver.getSapProxy();
-                }
+                mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */
                 break;
             default:
-            /* Message not handled */
+                /* Message not handled */
                 return false;
         }
         return true; // Message handles
@@ -808,6 +809,7 @@
      * 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
      */
     @VisibleForTesting
@@ -939,12 +941,13 @@
         switch (sapMsg.getMsgType()) {
             case SapMessage.ID_RIL_UNSOL_DISCONNECT_IND: {
                 if (mState != SAP_STATE.DISCONNECTED && mState != SAP_STATE.DISCONNECTING) {
-                /* we only send disconnect indication to the client if we are actually connected*/
+                    /* 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 */
+                    /* TODO: This was introduced to handle disconnect indication from RIL */
                     sendDisconnectInd(sapMsg.getDisconnectionType());
                 }
                 break;
@@ -960,7 +963,6 @@
 
     /**
      * This is only to be called from the handlerThread, else use sendRilThreadMessage();
-     * @param sapMsg
      */
     @VisibleForTesting
     void sendRilMessage(SapMessage sapMsg) {
@@ -968,19 +970,15 @@
             Log.i(TAG_HANDLER,
                     "sendRilMessage() - " + SapMessage.getMsgTypeName(sapMsg.getMsgType()));
         }
-
-        Log.d(TAG_HANDLER, "sendRilMessage: calling getSapProxy");
         synchronized (mRilBtReceiver.getSapProxyLock()) {
-            ISap sapProxy = mRilBtReceiver.getSapProxy();
-            if (sapProxy == null) {
+            if (!mRilBtReceiver.isProxyValid()) {
                 Log.e(TAG_HANDLER,
-                        "sendRilMessage: Unable to send message to RIL; sapProxy is null");
+                        "sendRiilMessage: Unable to send message to Ril; sapProxy is invalid");
                 sendClientMessage(new SapMessage(SapMessage.ID_ERROR_RESP));
                 return;
             }
-
             try {
-                sapMsg.send(sapProxy);
+                sapMsg.send(mRilBtReceiver);
                 if (VERBOSE) {
                     Log.d(TAG_HANDLER, "sendRilMessage: sapMsg.callISapReq called successfully");
                 }
@@ -994,6 +992,7 @@
                 mRilBtReceiver.resetSapProxy();
             }
         }
+
     }
 
     /**
diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapMessageTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapMessageTest.java
index 55cf35f..3b0b711 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/sap/SapMessageTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapMessageTest.java
@@ -42,8 +42,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.hardware.radio.V1_0.ISap;
-import android.hardware.radio.V1_0.SapTransferProtocol;
+import android.hardware.radio.sap.SapTransferProtocol;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -77,10 +76,10 @@
         int cardReaderStatus = STATUS_CARD_INSERTED;
         int statusChange = 1;
         int transportProtocol = TRANS_PROTO_T0;
-        byte[] apdu = new byte[] {0x01, 0x02};
-        byte[] apdu7816 = new byte[] {0x03, 0x04};
-        byte[] apduResp = new byte[] {0x05, 0x06};
-        byte[] atr = new byte[] {0x07, 0x08};
+        byte[] apdu = new byte[]{0x01, 0x02};
+        byte[] apdu7816 = new byte[]{0x03, 0x04};
+        byte[] apduResp = new byte[]{0x05, 0x06};
+        byte[] atr = new byte[]{0x07, 0x08};
         boolean sendToRil = true;
         boolean clearRilQueue = true;
         int testMode = TEST_MODE_ENABLE;
@@ -148,10 +147,10 @@
         int cardReaderStatus = STATUS_CARD_INSERTED;
         int statusChange = 1;
         int transportProtocol = TRANS_PROTO_T0;
-        byte[] apdu = new byte[] {0x01, 0x02};
-        byte[] apdu7816 = new byte[] {0x03, 0x04};
-        byte[] apduResp = new byte[] {0x05, 0x06};
-        byte[] atr = new byte[] {0x07, 0x08};
+        byte[] apdu = new byte[]{0x01, 0x02};
+        byte[] apdu7816 = new byte[]{0x03, 0x04};
+        byte[] apduResp = new byte[]{0x05, 0x06};
+        byte[] atr = new byte[]{0x07, 0x08};
 
         mMessage.setMsgType(msgType);
         mMessage.setMaxMsgSize(maxMsgSize);
@@ -196,10 +195,10 @@
     @Test
     public void send() throws Exception {
         int maxMsgSize = 512;
-        byte[] apdu = new byte[] {0x01, 0x02};
-        byte[] apdu7816 = new byte[] {0x03, 0x04};
+        byte[] apdu = new byte[]{0x01, 0x02};
+        byte[] apdu7816 = new byte[]{0x03, 0x04};
 
-        ISap sapProxy = mock(ISap.class);
+        ISapRilReceiver sapProxy = mock(ISapRilReceiver.class);
         mMessage.setClearRilQueue(true);
 
         mMessage.setMsgType(ID_CONNECT_REQ);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverHidlTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverHidlTest.java
new file mode 100644
index 0000000..874bd09
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverHidlTest.java
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bluetooth.sap;
+
+import static com.android.bluetooth.sap.SapMessage.CON_STATUS_OK;
+import static com.android.bluetooth.sap.SapMessage.DISC_GRACEFULL;
+import static com.android.bluetooth.sap.SapMessage.ID_CONNECT_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_DISCONNECT_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_POWER_SIM_OFF_REQ;
+import static com.android.bluetooth.sap.SapMessage.ID_POWER_SIM_OFF_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_POWER_SIM_ON_REQ;
+import static com.android.bluetooth.sap.SapMessage.ID_POWER_SIM_ON_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_RESET_SIM_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_RIL_UNKNOWN;
+import static com.android.bluetooth.sap.SapMessage.ID_RIL_UNSOL_DISCONNECT_IND;
+import static com.android.bluetooth.sap.SapMessage.ID_SET_TRANSPORT_PROTOCOL_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_STATUS_IND;
+import static com.android.bluetooth.sap.SapMessage.ID_TRANSFER_APDU_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_TRANSFER_ATR_RESP;
+import static com.android.bluetooth.sap.SapMessage.ID_TRANSFER_CARD_READER_STATUS_RESP;
+import static com.android.bluetooth.sap.SapMessage.RESULT_OK;
+import static com.android.bluetooth.sap.SapMessage.STATUS_CARD_INSERTED;
+import static com.android.bluetooth.sap.SapServer.ISAP_GET_SERVICE_DELAY_MILLIS;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RFC_REPLY;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RIL_CONNECT;
+import static com.android.bluetooth.sap.SapServer.SAP_MSG_RIL_IND;
+import static com.android.bluetooth.sap.SapServer.SAP_PROXY_DEAD;
+import static com.android.bluetooth.sap.SapServer.SAP_RIL_SOCK_CLOSED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import android.hardware.radio.V1_0.ISap;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.Spy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class SapRilReceiverHidlTest {
+
+    private static final long TIMEOUT_MS = 1_000;
+
+    private HandlerThread mHandlerThread;
+    private Handler mServerMsgHandler;
+
+    @Spy
+    private TestHandlerCallback mCallback = new TestHandlerCallback();
+
+    @Mock
+    private Handler mServiceHandler;
+
+    @Mock
+    private ISap mSapProxy;
+
+    private SapRilReceiverHidl mReceiver;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandlerThread = new HandlerThread("SapRilReceiverTest");
+        mHandlerThread.start();
+
+        mServerMsgHandler = new Handler(mHandlerThread.getLooper(), mCallback);
+        mReceiver = new SapRilReceiverHidl(mServerMsgHandler, mServiceHandler);
+        mReceiver.mSapProxy = mSapProxy;
+        mServerMsgHandler.removeMessages(SAP_PROXY_DEAD);
+    }
+
+    @After
+    public void tearDown() {
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void getSapProxyLock() {
+        assertThat(mReceiver.getSapProxyLock()).isNotNull();
+    }
+
+    @Test
+    public void resetSapProxy() throws Exception {
+        mReceiver.resetSapProxy();
+
+        assertThat(mReceiver.mSapProxy).isNull();
+        verify(mSapProxy).unlinkToDeath(any());
+    }
+
+    @Test
+    public void notifyShutdown() throws Exception {
+        mReceiver.notifyShutdown();
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_RIL_SOCK_CLOSED), any());
+    }
+
+    @Test
+    public void sendRilConnectMessage() throws Exception {
+        mReceiver.sendRilConnectMessage();
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RIL_CONNECT), any());
+    }
+
+    @Test
+    public void serviceDied() throws Exception {
+        long cookie = 1;
+        mReceiver.mSapProxyDeathRecipient.serviceDied(cookie);
+
+        verify(mCallback, timeout(ISAP_GET_SERVICE_DELAY_MILLIS + TIMEOUT_MS))
+                .receiveMessage(eq(SAP_PROXY_DEAD), argThat(
+                        arg -> (arg instanceof Long) && ((Long) arg == cookie)
+                ));
+    }
+
+    @Test
+    public void callback_connectResponse() throws Exception {
+        int token = 1;
+        int sapConnectRsp = CON_STATUS_OK;
+        int maxMsgSize = 512;
+        mReceiver.mSapCallback.connectResponse(token, sapConnectRsp, maxMsgSize);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_CONNECT_RESP
+                                && sapMsg.getConnectionStatus() == sapConnectRsp;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_disconnectResponse() throws Exception {
+        int token = 1;
+        mReceiver.mSapCallback.disconnectResponse(token);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_DISCONNECT_RESP;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_disconnectIndication() throws Exception {
+        int token = 1;
+        int disconnectType = DISC_GRACEFULL;
+        mReceiver.mSapCallback.disconnectIndication(token, disconnectType);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RIL_IND), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_RIL_UNSOL_DISCONNECT_IND
+                                && sapMsg.getDisconnectionType() == disconnectType;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_apduResponse() throws Exception {
+        int token = 1;
+        int resultCode = RESULT_OK;
+        byte[] apduRsp = new byte[]{0x03, 0x04};
+        ArrayList<Byte> apduRspList = new ArrayList<>();
+        for (byte b : apduRsp) {
+            apduRspList.add(b);
+        }
+
+        mReceiver.mSapCallback.apduResponse(token, resultCode, apduRspList);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_TRANSFER_APDU_RESP
+                                && sapMsg.getResultCode() == resultCode
+                                && Arrays.equals(sapMsg.getApduResp(), apduRsp);
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_transferAtrResponse() throws Exception {
+        int token = 1;
+        int resultCode = RESULT_OK;
+        byte[] atr = new byte[]{0x03, 0x04};
+        ArrayList<Byte> atrList = new ArrayList<>();
+        for (byte b : atr) {
+            atrList.add(b);
+        }
+
+        mReceiver.mSapCallback.transferAtrResponse(token, resultCode, atrList);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_TRANSFER_ATR_RESP
+                                && sapMsg.getResultCode() == resultCode
+                                && Arrays.equals(sapMsg.getAtr(), atr);
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_powerResponse_powerOff() throws Exception {
+        int token = 1;
+        int reqType = ID_POWER_SIM_OFF_REQ;
+        int resultCode = RESULT_OK;
+        SapMessage.sOngoingRequests.clear();
+        SapMessage.sOngoingRequests.put(token, reqType);
+
+        mReceiver.mSapCallback.powerResponse(token, resultCode);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_POWER_SIM_OFF_RESP
+                                && sapMsg.getResultCode() == resultCode;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_powerResponse_powerOn() throws Exception {
+        int token = 1;
+        int reqType = ID_POWER_SIM_ON_REQ;
+        int resultCode = RESULT_OK;
+        SapMessage.sOngoingRequests.clear();
+        SapMessage.sOngoingRequests.put(token, reqType);
+
+        mReceiver.mSapCallback.powerResponse(token, resultCode);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_POWER_SIM_ON_RESP
+                                && sapMsg.getResultCode() == resultCode;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_resetSimResponse() throws Exception {
+        int token = 1;
+        int resultCode = RESULT_OK;
+
+        mReceiver.mSapCallback.resetSimResponse(token, resultCode);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_RESET_SIM_RESP
+                                && sapMsg.getResultCode() == resultCode;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_statusIndication() throws Exception {
+        int token = 1;
+        int statusChange = 2;
+
+        mReceiver.mSapCallback.statusIndication(token, statusChange);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_STATUS_IND
+                                && sapMsg.getStatusChange() == statusChange;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_transferCardReaderStatusResponse() throws Exception {
+        int token = 1;
+        int resultCode = RESULT_OK;
+        int cardReaderStatus = STATUS_CARD_INSERTED;
+
+        mReceiver.mSapCallback.transferCardReaderStatusResponse(
+                token, resultCode, cardReaderStatus);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_TRANSFER_CARD_READER_STATUS_RESP
+                                && sapMsg.getResultCode() == resultCode;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_errorResponse() throws Exception {
+        int token = 1;
+
+        mReceiver.mSapCallback.errorResponse(token);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RIL_IND), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_RIL_UNKNOWN;
+                    }
+                }
+        ));
+    }
+
+    @Test
+    public void callback_transferProtocolResponse() throws Exception {
+        int token = 1;
+        int resultCode = RESULT_OK;
+
+        mReceiver.mSapCallback.transferProtocolResponse(token, resultCode);
+
+        verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
+                new ArgumentMatcher<Object>() {
+                    @Override
+                    public boolean matches(Object arg) {
+                        if (!(arg instanceof SapMessage)) {
+                            return false;
+                        }
+                        SapMessage sapMsg = (SapMessage) arg;
+                        return sapMsg.getMsgType() == ID_SET_TRANSPORT_PROTOCOL_RESP
+                                && sapMsg.getResultCode() == resultCode;
+                    }
+                }
+        ));
+    }
+
+    public static class TestHandlerCallback implements Handler.Callback {
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            receiveMessage(msg.what, msg.obj);
+            return true;
+        }
+
+        public void receiveMessage(int what, Object obj) {
+        }
+    }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverTest.java
index 8299daa..3d0e350 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapRilReceiverTest.java
@@ -44,14 +44,18 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.argThat;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
-import android.hardware.radio.V1_0.ISap;
+import android.hardware.radio.sap.ISap;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.IBinder;
 import android.os.Message;
 
 import androidx.test.filters.LargeTest;
@@ -66,7 +70,6 @@
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 
 @LargeTest
@@ -99,6 +102,7 @@
         mServerMsgHandler = new Handler(mHandlerThread.getLooper(), mCallback);
         mReceiver = new SapRilReceiver(mServerMsgHandler, mServiceHandler);
         mReceiver.mSapProxy = mSapProxy;
+        mServerMsgHandler.removeMessages(SAP_PROXY_DEAD);
     }
 
     @After
@@ -113,14 +117,18 @@
 
     @Test
     public void resetSapProxy() throws Exception {
+        IBinder mockSapProxyBinder = mock(IBinder.class);
+        when(mReceiver.mSapProxy.asBinder()).thenReturn(mockSapProxyBinder);
         mReceiver.resetSapProxy();
 
         assertThat(mReceiver.mSapProxy).isNull();
-        verify(mSapProxy).unlinkToDeath(any());
+        verify(mockSapProxyBinder).unlinkToDeath(any(), anyInt());
     }
 
     @Test
     public void notifyShutdown() throws Exception {
+        IBinder mockSapProxyBinder = mock(IBinder.class);
+        when(mReceiver.mSapProxy.asBinder()).thenReturn(mockSapProxyBinder);
         mReceiver.notifyShutdown();
 
         verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_RIL_SOCK_CLOSED), any());
@@ -134,13 +142,12 @@
     }
 
     @Test
-    public void serviceDied() throws Exception {
-        long cookie = 1;
-        mReceiver.mSapProxyDeathRecipient.serviceDied(cookie);
+    public void binderDied() throws Exception {
+        mReceiver.mSapProxyDeathRecipient.binderDied();
 
         verify(mCallback, timeout(ISAP_GET_SERVICE_DELAY_MILLIS + TIMEOUT_MS))
                 .receiveMessage(eq(SAP_PROXY_DEAD), argThat(
-                        arg -> (arg instanceof Long) && ((Long) arg == cookie)
+                        arg -> (arg instanceof Long) && ((Long) arg == 0)
                 ));
     }
 
@@ -211,12 +218,8 @@
         int token = 1;
         int resultCode = RESULT_OK;
         byte[] apduRsp = new byte[]{0x03, 0x04};
-        ArrayList<Byte> apduRspList = new ArrayList<>();
-        for (byte b : apduRsp) {
-            apduRspList.add(b);
-        }
 
-        mReceiver.mSapCallback.apduResponse(token, resultCode, apduRspList);
+        mReceiver.mSapCallback.apduResponse(token, resultCode, apduRsp);
 
         verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
                 new ArgumentMatcher<Object>() {
@@ -239,12 +242,8 @@
         int token = 1;
         int resultCode = RESULT_OK;
         byte[] atr = new byte[]{0x03, 0x04};
-        ArrayList<Byte> atrList = new ArrayList<>();
-        for (byte b : atr) {
-            atrList.add(b);
-        }
 
-        mReceiver.mSapCallback.transferAtrResponse(token, resultCode, atrList);
+        mReceiver.mSapCallback.transferAtrResponse(token, resultCode, atr);
 
         verify(mCallback, timeout(TIMEOUT_MS)).receiveMessage(eq(SAP_MSG_RFC_REPLY), argThat(
                 new ArgumentMatcher<Object>() {
@@ -430,6 +429,7 @@
             return true;
         }
 
-        public void receiveMessage(int what, Object obj) {}
+        public void receiveMessage(int what, Object obj) {
+        }
     }
 }
diff --git a/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java
index b3be8c2..e88c5f1 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/sap/SapServerTest.java
@@ -59,7 +59,6 @@
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.content.Intent;
-import android.hardware.radio.V1_0.ISap;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
@@ -160,11 +159,9 @@
 
     @Test
     public void onConnectRequest_whenStateIsConnecting_callsSendRilMessage() {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
-        ISap mockSapProxy = mock(ISap.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
@@ -218,26 +215,25 @@
 
     @Test
     public void sendRilMessage_success() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
-        ISap mockSapProxy = mock(ISap.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+        when(mockReceiver.isProxyValid()).thenReturn(true);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
         SapMessage msg = mock(SapMessage.class);
         mSapServer.sendRilMessage(msg);
 
-        verify(msg).send(mockSapProxy);
+        verify(msg).send(mockReceiver);
     }
 
     @Test
     public void sendRilMessage_whenSapProxyIsNull_sendsErrorClientMessage() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(null);
+        when(mockReceiver.isProxyValid()).thenReturn(false);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
@@ -250,11 +246,9 @@
 
     @Test
     public void sendRilMessage_whenIAEIsThrown_sendsErrorClientMessage() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
-        ISap mockSapProxy = mock(ISap.class);
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
@@ -269,11 +263,10 @@
     @Test
     public void sendRilMessage_whenRemoteExceptionIsThrown_sendsErrorClientMessage()
             throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
-        ISap mockSapProxy = mock(ISap.class);
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
+        when(mockReceiver.isProxyValid()).thenReturn(true);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
@@ -544,7 +537,7 @@
 
     @Test
     public void handleMessage_forRilConnectMsg_callsSendRilMessage() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
         mSapServer.mRilBtReceiver = mockReceiver;
@@ -566,11 +559,9 @@
 
     @Test
     public void handleMessage_forRilReqMsg_callsSendRilMessage() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
-        ISap mockSapProxy = mock(ISap.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         Object lock = new Object();
         when(mockReceiver.getSapProxyLock()).thenReturn(lock);
-        when(mockReceiver.getSapProxy()).thenReturn(mockSapProxy);
         mSapServer.mRilBtReceiver = mockReceiver;
         mSapServer.mSapHandler = mHandler;
 
@@ -630,9 +621,8 @@
 
     @Test
     public void handleMessage_forProxyDeadMsg_notifiesShutDown() throws Exception {
-        SapRilReceiver mockReceiver = mock(SapRilReceiver.class);
+        ISapRilReceiver mockReceiver = mock(ISapRilReceiver.class);
         AtomicLong cookie = new AtomicLong(23);
-        when(mockReceiver.getSapProxyCookie()).thenReturn(cookie);
         mSapServer.mRilBtReceiver = mockReceiver;
 
         Message message = Message.obtain();
@@ -643,7 +633,6 @@
             mSapServer.handleMessage(message);
 
             verify(mockReceiver).notifyShutdown();
-            verify(mockReceiver).resetSapProxy();
         } finally {
             message.recycle();
         }
@@ -709,7 +698,8 @@
             return true;
         }
 
-        public void receiveMessage(int what, Object obj) {}
+        public void receiveMessage(int what, Object obj) {
+        }
     }
 }