[uwb] Integrate admin messages handling

* Add admin message handling for CS GATT client and CP GATT server.

Bug: 242272072
Test: atest ServiceUwbTests
Change-Id: I27d1c6711cb3ea1f451ea0332625a734f0b252d3
diff --git a/service/java/com/android/server/uwb/discovery/TransportClientProvider.java b/service/java/com/android/server/uwb/discovery/TransportClientProvider.java
index 73bf432..2ecc2e7 100644
--- a/service/java/com/android/server/uwb/discovery/TransportClientProvider.java
+++ b/service/java/com/android/server/uwb/discovery/TransportClientProvider.java
@@ -24,38 +24,9 @@
 public abstract class TransportClientProvider extends TransportProvider {
     private static final String TAG = TransportClientProvider.class.getSimpleName();
 
-    public enum TerminationReason {
-        /** Disconnection of the remote GATT service. */
-        REMOTE_DISCONNECTED,
-        /** remote GATT service discovery failure. */
-        SERVICE_DISCOVERY_FAILURE,
-        /** Characterstic read failure */
-        CHARACTERSTIC_READ_FAILURE,
-        /** Characterstic write failure */
-        CHARACTERSTIC_WRITE_FAILURE,
-        /** Descriptor write failure */
-        DESCRIPTOR_WRITE_FAILURE,
-    }
-
-    public @interface NotifyCharacteristicReturnValues {}
-
     /** Callback for listening to transport client events. */
     @WorkerThread
-    public interface TransportClientCallback {
-
-        /** Called when the client started processing. */
-        void onProcessingStarted();
-
-        /** Called when the client stopped processing. */
-        void onProcessingStopped();
-
-        /**
-         * Called when the client terminated the connection due to an unrecoverable errors.
-         *
-         * @param reason indicates the termination reason.
-         */
-        void onTerminated(TerminationReason reason);
-    }
+    public abstract static class TransportClientCallback implements TransportCallback {}
 
     protected TransportClientProvider(int secid) {
         super(secid);
diff --git a/service/java/com/android/server/uwb/discovery/TransportProvider.java b/service/java/com/android/server/uwb/discovery/TransportProvider.java
index 83d2c1e..97cd379 100644
--- a/service/java/com/android/server/uwb/discovery/TransportProvider.java
+++ b/service/java/com/android/server/uwb/discovery/TransportProvider.java
@@ -20,14 +20,60 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.server.uwb.discovery.info.AdminErrorMessage;
+import com.android.server.uwb.discovery.info.AdminErrorMessage.ErrorType;
+import com.android.server.uwb.discovery.info.AdminEventMessage;
+import com.android.server.uwb.discovery.info.AdminEventMessage.EventType;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage.InstructionCode;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType;
 
+import java.nio.ByteBuffer;
+
 /** Abstract class for Transport Provider */
 public abstract class TransportProvider implements Transport {
     private static final String TAG = TransportProvider.class.getSimpleName();
 
+    public enum TerminationReason {
+        /** Disconnection of the remote GATT service. */
+        REMOTE_DISCONNECTED,
+        /** remote GATT service discovery failure. */
+        SERVICE_DISCOVERY_FAILURE,
+        /** Characterstic read failure */
+        CHARACTERSTIC_READ_FAILURE,
+        /** Characterstic write failure */
+        CHARACTERSTIC_WRITE_FAILURE,
+        /** Descriptor write failure */
+        DESCRIPTOR_WRITE_FAILURE,
+        /** Remote device message error */
+        REMOTE_DEVICE_MESSAGE_ERROR,
+        /** Remote device SECID error */
+        REMOTE_DEVICE_SECID_ERROR,
+    }
+
+    /** Callback for listening to transport events. */
+    public interface TransportCallback {
+
+        /** Called when the transport started processing. */
+        void onProcessingStarted();
+
+        /** Called when the transport stopped processing. */
+        void onProcessingStopped();
+
+        /**
+         * Called when the transport terminated the connection due to an unrecoverable errors.
+         *
+         * @param reason indicates the termination reason.
+         */
+        void onTerminated(TerminationReason reason);
+    }
+
+    /**
+     * administrative SECID shall be exposed on each CS implementation at all times. It shall be
+     * marked as static.
+     */
+    public static final int ADMIN_SECID = 1;
+
     private DataReceiver mDataReceiver;
 
     /** Assigned SECID value (unsigned integer in the range 2..127, values 0 and 1 are reserved). */
@@ -39,6 +85,17 @@
      */
     private int mDestinationSecid = 2;
 
+    /** Wraps Fira Connector Message byte array and the associated SECID. */
+    public static class MessagePacket {
+        public final int secid;
+        public ByteBuffer messageBytes;
+
+        public MessagePacket(int secid, ByteBuffer messageBytes) {
+            this.secid = secid;
+            this.messageBytes = messageBytes;
+        }
+    }
+
     protected TransportProvider(@IntRange(from = 2, to = 127) int secid) {
         mSecid = secid;
     }
@@ -144,6 +201,10 @@
      * @param message FiRa connector message.
      */
     protected void onMessageReceived(int secid, FiraConnectorMessage message) {
+        if (secid == ADMIN_SECID) {
+            processAdminMessage(message);
+            return;
+        }
         if (secid != mSecid) {
             Log.w(
                     TAG,
@@ -151,10 +212,80 @@
                             + mSecid
                             + " Received:"
                             + secid);
+            sentAdminErrorMessage(ErrorType.SECID_INVALID);
             return;
         }
         if (mDataReceiver != null) {
             mDataReceiver.onDataReceived(message.payload);
         }
     }
+
+    /**
+     * Send a FiRa OOB administrative Error message to the administrative SECID on the remote
+     * device.
+     *
+     * @param errorType ErrorType of the message.
+     */
+    protected void sentAdminErrorMessage(ErrorType errorType) {
+        if (!sendMessage(ADMIN_SECID, new AdminErrorMessage(errorType))) {
+            Log.w(TAG, "sentAdminErrorMessage with ErrorType:" + errorType + " failed.");
+        }
+    }
+
+    /**
+     * Send a FiRa OOB administrative Event message to the administrative SECID on the remote
+     * device.
+     *
+     * @param eventType EventType of the message.
+     * @param additionalData additional data associated with the event.
+     */
+    protected void sentAdminEventMessage(EventType eventType, byte[] additionalData) {
+        if (!sendMessage(ADMIN_SECID, new AdminEventMessage(eventType, additionalData))) {
+            Log.w(TAG, "sentAdminEventMessage with EventType:" + eventType + " failed.");
+        }
+    }
+
+    /**
+     * Process FiRa OOB administrative message from the remote device.
+     *
+     * @param message FiRa connector message.
+     */
+    private void processAdminMessage(FiraConnectorMessage message) {
+        if (AdminErrorMessage.isAdminErrorMessage(message)) {
+            AdminErrorMessage errorMessage = AdminErrorMessage.convertToAdminErrorMessage(message);
+            Log.w(TAG, "Received AdminErrorMessage:" + errorMessage);
+            switch (errorMessage.errorType) {
+                case DATA_PACKET_LENGTH_OVERFLOW:
+                case MESSAGE_LENGTH_OVERFLOW:
+                case TOO_MANY_CONCURRENT_FRAGMENTED_MESSAGE_SESSIONS:
+                    terminateOnError(TerminationReason.REMOTE_DEVICE_MESSAGE_ERROR);
+                    break;
+                case SECID_INVALID:
+                case SECID_INVALID_FOR_RESPONSE:
+                case SECID_BUSY:
+                case SECID_PROTOCOL_ERROR:
+                case SECID_INTERNAL_ERROR:
+                    terminateOnError(TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+                    break;
+            }
+        } else if (AdminEventMessage.isAdminEventMessage(message)) {
+            AdminEventMessage eventMessage = AdminEventMessage.convertToAdminEventMessage(message);
+            Log.w(TAG, "Received AdminEventMessage:" + eventMessage);
+            switch (eventMessage.eventType) {
+                case CAPABILITIES_CHANGED:
+                    // No-op since this is only applicatble for CS with the role of GATT Server,
+                    // which isn't mandated by FiRa.
+                    break;
+            }
+        } else {
+            Log.e(TAG, "Invalid Admin FiraConnectorMessage received:" + message);
+        }
+    }
+
+    /**
+     * Terminates the transport provider.
+     *
+     * @param reason reason for the termination.
+     */
+    protected abstract void terminateOnError(TerminationReason reason);
 }
diff --git a/service/java/com/android/server/uwb/discovery/TransportServerProvider.java b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java
index a33b381..db5a3db 100644
--- a/service/java/com/android/server/uwb/discovery/TransportServerProvider.java
+++ b/service/java/com/android/server/uwb/discovery/TransportServerProvider.java
@@ -26,20 +26,13 @@
 
     /** Callback for listening to transport server events. */
     @WorkerThread
-    public interface TransportServerCallback {
-
-        /** Called when the server started processing. */
-        void onProcessingStarted();
-
-        /** Called when the server stopped processing. */
-        void onProcessingStopped();
-
+    public abstract static class TransportServerCallback implements TransportCallback {
         /**
          * Called when the server receive new capabilites from the remote device.
          *
          * @param capabilities new capabilities.
          */
-        void onCapabilitesUpdated(FiraConnectorCapabilities capabilities);
+        public abstract void onCapabilitesUpdated(FiraConnectorCapabilities capabilities);
     }
 
     protected TransportServerProvider(int secid) {
diff --git a/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java b/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java
index b95f84b..c1d8bc1 100644
--- a/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java
+++ b/service/java/com/android/server/uwb/discovery/ble/GattTransportClientProvider.java
@@ -31,8 +31,10 @@
 import androidx.annotation.WorkerThread;
 
 import com.android.server.uwb.discovery.TransportClientProvider;
-import com.android.server.uwb.discovery.TransportClientProvider.TerminationReason;
 import com.android.server.uwb.discovery.TransportClientProvider.TransportClientCallback;
+import com.android.server.uwb.discovery.TransportProvider.MessagePacket;
+import com.android.server.uwb.discovery.TransportProvider.TerminationReason;
+import com.android.server.uwb.discovery.info.AdminErrorMessage.ErrorType;
 import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
 import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage;
@@ -45,7 +47,7 @@
 import java.util.concurrent.Executor;
 
 /**
- * Class for UWB transport client provider using Bluetooth GATT.
+ * Class for FiRa CS UWB transport client provider using Bluetooth GATT.
  *
  * <p>The GATT client is responsible for the entire Service discovery procedure. Once the device
  * discovery phase passed, the client establishes the Bluetooth connection and perform GATT service
@@ -84,18 +86,6 @@
      */
     private ArrayDeque<FiraConnectorDataPacket> mIncompleteOutDataPacketQueue;
 
-    /* Wraps Fira Connector Message byte array and the associated SECID.
-     */
-    private static class MessagePacket {
-        public final int secid;
-        public ByteBuffer messageBytes;
-
-        MessagePacket(int secid, ByteBuffer messageBytes) {
-            this.secid = secid;
-            this.messageBytes = messageBytes;
-        }
-    }
-
     /* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the
      * mInControlPointCharacteristic.
      */
@@ -435,6 +425,11 @@
             Log.w(TAG, "processOutDataPacket failed due to server not ready for processing.");
             return false;
         }
+        if (bytes.length > mCapabilities.optimizedDataPacketSize) {
+            Log.w(TAG, "processOutDataPacket failed due to data packet length overflow.");
+            super.sentAdminErrorMessage(ErrorType.DATA_PACKET_LENGTH_OVERFLOW);
+            return false;
+        }
         FiraConnectorDataPacket latestDataPacket = FiraConnectorDataPacket.fromBytes(bytes);
         if (latestDataPacket == null) {
             Log.w(
@@ -449,6 +444,7 @@
                     TAG,
                     "processOutDataPacket failed due to latest FiraConnectorDataPacket's SECID"
                             + " doesn't match previous data packet.");
+            super.sentAdminErrorMessage(ErrorType.TOO_MANY_CONCURRENT_FRAGMENTED_MESSAGE_SESSIONS);
             return false;
         }
         mIncompleteOutDataPacketQueue.add(latestDataPacket);
@@ -462,6 +458,12 @@
         }
         mIncompleteOutDataPacketQueue.clear();
 
+        if (byteStream.size() > mCapabilities.maxMessageBufferSize) {
+            Log.w(TAG, "processOutDataPacket failed due to message length overflow.");
+            super.sentAdminErrorMessage(ErrorType.MESSAGE_LENGTH_OVERFLOW);
+            return false;
+        }
+
         FiraConnectorMessage message = FiraConnectorMessage.fromBytes(byteStream.toByteArray());
         if (message == null) {
             Log.w(
@@ -586,8 +588,9 @@
         }
     }
 
-    private void terminateOnError(TerminationReason reason) {
-        Log.e(TAG, "GattTransportClient terminated with reason:" + reason);
+    @Override
+    protected void terminateOnError(TerminationReason reason) {
+        Log.e(TAG, "GattTransportClientProvider terminated with reason:" + reason);
         stop();
         mTransportClientCallback.onTerminated(reason);
     }
diff --git a/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java
index 9d9bfc8..17412a6 100644
--- a/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java
+++ b/service/java/com/android/server/uwb/discovery/ble/GattTransportServerProvider.java
@@ -31,6 +31,8 @@
 
 import androidx.annotation.WorkerThread;
 
+import com.android.server.uwb.discovery.TransportProvider.MessagePacket;
+import com.android.server.uwb.discovery.TransportProvider.TerminationReason;
 import com.android.server.uwb.discovery.TransportServerProvider;
 import com.android.server.uwb.discovery.TransportServerProvider.TransportServerCallback;
 import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
@@ -43,7 +45,7 @@
 import java.util.Arrays;
 
 /**
- * Class for UWB transport server provider using Bluetooth GATT.
+ * Class for FiRa CP UWB transport server provider using Bluetooth GATT.
  *
  * <p>The GATT server simply waits for the discovery from client side. It shall also wait for at
  * least one valid update of FiRa Connector Capabilities characteristic value from the client side.
@@ -79,18 +81,6 @@
      */
     private ArrayDeque<FiraConnectorDataPacket> mIncompleteInDataPacketQueue;
 
-    /* Wraps Fira Connector Message byte array and the associated SECID.
-     */
-    private static class MessagePacket {
-        public final int secid;
-        public ByteBuffer messageBytes;
-
-        MessagePacket(int secid, ByteBuffer messageBytes) {
-            this.secid = secid;
-            this.messageBytes = messageBytes;
-        }
-    }
-
     /* Queue of Fira Connector Message wrapped as MessagePacket to be sent via the
      * mOutControlPointCharacteristic.
      */
@@ -497,4 +487,11 @@
                         BluetoothGattCharacteristic.PERMISSION_WRITE);
         mFiraCPService.addCharacteristic(mCapabilitiesCharacteristic);
     }
+
+    @Override
+    protected void terminateOnError(TerminationReason reason) {
+        Log.e(TAG, "GattTransportServerProvider terminated with reason:" + reason);
+        stop();
+        mTransportServerCallback.onTerminated(reason);
+    }
 }
diff --git a/service/tests/src/com/android/server/uwb/discovery/TransportProviderTest.java b/service/tests/src/com/android/server/uwb/discovery/TransportProviderTest.java
index f1d879f..f417def 100644
--- a/service/tests/src/com/android/server/uwb/discovery/TransportProviderTest.java
+++ b/service/tests/src/com/android/server/uwb/discovery/TransportProviderTest.java
@@ -18,6 +18,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -28,6 +30,11 @@
 
 import com.android.server.uwb.discovery.Transport.DataReceiver;
 import com.android.server.uwb.discovery.Transport.SendingDataCallback;
+import com.android.server.uwb.discovery.TransportProvider.TerminationReason;
+import com.android.server.uwb.discovery.info.AdminErrorMessage;
+import com.android.server.uwb.discovery.info.AdminErrorMessage.ErrorType;
+import com.android.server.uwb.discovery.info.AdminEventMessage;
+import com.android.server.uwb.discovery.info.AdminEventMessage.EventType;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage.InstructionCode;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType;
@@ -56,6 +63,7 @@
         public boolean sendMessageSuccess = true;
         public FiraConnectorMessage lastSendMessage;
         public int lastSendMessageSecid;
+        public TerminationReason lastTerminationReason;
 
         FakeTransportProvider() {
             super(SECID);
@@ -85,6 +93,11 @@
             mStarted = false;
             return true;
         }
+
+        @Override
+        protected void terminateOnError(TerminationReason reason) {
+            lastTerminationReason = reason;
+        }
     }
 
     @Mock DataReceiver mMockDataReceiver;
@@ -169,4 +182,56 @@
 
         verifyZeroInteractions(mMockDataReceiver);
     }
+
+    @Test
+    public void testSentAdminErrorMessage() {
+        mTransportProvider.sentAdminErrorMessage(ErrorType.DATA_PACKET_LENGTH_OVERFLOW);
+
+        assertThat(mFakeTransportProvider.lastSendMessageSecid)
+                .isEqualTo(TransportProvider.ADMIN_SECID);
+        assertThat(mFakeTransportProvider.lastSendMessage.toString())
+                .isEqualTo(new AdminErrorMessage(ErrorType.DATA_PACKET_LENGTH_OVERFLOW).toString());
+    }
+
+    @Test
+    public void testSentAdminEventMessage() {
+        mTransportProvider.sentAdminEventMessage(EventType.CAPABILITIES_CHANGED, new byte[] {});
+
+        assertThat(mFakeTransportProvider.lastSendMessageSecid)
+                .isEqualTo(TransportProvider.ADMIN_SECID);
+        assertThat(mFakeTransportProvider.lastSendMessage.toString())
+                .isEqualTo(
+                        new AdminEventMessage(EventType.CAPABILITIES_CHANGED, new byte[] {})
+                                .toString());
+    }
+
+    private void verifyAdminMessageReceive(ErrorType errorType, TerminationReason reason) {
+        mTransportProvider.registerDataReceiver(mMockDataReceiver);
+        mTransportProvider.onMessageReceived(
+                TransportProvider.ADMIN_SECID, new AdminErrorMessage(errorType));
+        verify(mMockDataReceiver, never()).onDataReceived(any());
+        assertThat(mFakeTransportProvider.lastTerminationReason).isEqualTo(reason);
+    }
+
+    @Test
+    public void testOutCharactersticNotifyAndRead_receiveAdminPacket() {
+        verifyAdminMessageReceive(
+                ErrorType.DATA_PACKET_LENGTH_OVERFLOW,
+                TerminationReason.REMOTE_DEVICE_MESSAGE_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.MESSAGE_LENGTH_OVERFLOW, TerminationReason.REMOTE_DEVICE_MESSAGE_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.TOO_MANY_CONCURRENT_FRAGMENTED_MESSAGE_SESSIONS,
+                TerminationReason.REMOTE_DEVICE_MESSAGE_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.SECID_INVALID, TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.SECID_INVALID_FOR_RESPONSE, TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.SECID_BUSY, TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.SECID_PROTOCOL_ERROR, TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+        verifyAdminMessageReceive(
+                ErrorType.SECID_INTERNAL_ERROR, TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+    }
 }
diff --git a/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportClientProviderTest.java b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportClientProviderTest.java
index b434229..9fe1a3f 100644
--- a/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportClientProviderTest.java
+++ b/service/tests/src/com/android/server/uwb/discovery/ble/GattTransportClientProviderTest.java
@@ -49,8 +49,11 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.uwb.discovery.Transport.DataReceiver;
-import com.android.server.uwb.discovery.TransportClientProvider.TerminationReason;
 import com.android.server.uwb.discovery.TransportClientProvider.TransportClientCallback;
+import com.android.server.uwb.discovery.TransportProvider;
+import com.android.server.uwb.discovery.TransportProvider.TerminationReason;
+import com.android.server.uwb.discovery.info.AdminErrorMessage;
+import com.android.server.uwb.discovery.info.AdminErrorMessage.ErrorType;
 import com.android.server.uwb.discovery.info.FiraConnectorCapabilities;
 import com.android.server.uwb.discovery.info.FiraConnectorDataPacket;
 import com.android.server.uwb.discovery.info.FiraConnectorMessage;
@@ -81,10 +84,11 @@
     private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
 
     private static final int SECID = 2;
+    private static final int SECID2 = 3;
     private static final byte[] MESSAGE_PAYLOAD1 = new byte[] {(byte) 0xF4, 0x00, 0x40};
     private static final FiraConnectorMessage MESSAGE =
             new FiraConnectorMessage(
-                    MessageType.EVENT, InstructionCode.DATA_EXCHANGE, MESSAGE_PAYLOAD1);
+                    MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, MESSAGE_PAYLOAD1);
     private static final FiraConnectorDataPacket DATA_PACKET =
             new FiraConnectorDataPacket(/*lastChainingPacket=*/ true, SECID, MESSAGE.toBytes());
     private static final byte[] DATA_PACKET_BYTES = DATA_PACKET.toBytes();
@@ -602,7 +606,7 @@
         Arrays.fill(messagePayload, (byte) 3);
         FiraConnectorMessage message =
                 new FiraConnectorMessage(
-                        MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload);
+                        MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, messagePayload);
         byte[] messageBytes = message.toBytes();
         int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1;
         byte[] packet_bytes1 =
@@ -626,6 +630,7 @@
                         .toBytes();
 
         startProcessing();
+        assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue();
         notifyAndReadOutCharacteristic(packet_bytes1);
         notifyAndReadOutCharacteristic(packet_bytes2);
         notifyAndReadOutCharacteristic(packet_bytes3);
@@ -636,4 +641,149 @@
         verify(mMockDataReceiver, times(1)).onDataReceived(captor.capture());
         assertThat(captor.getValue()).isEqualTo(message.payload);
     }
+
+    @Test
+    public void testOutCharactersticNotifyAndRead_receiveAdminPacket() {
+        byte[] packetBytes =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ true,
+                                TransportProvider.ADMIN_SECID,
+                                new AdminErrorMessage(ErrorType.SECID_INVALID).toBytes())
+                        .toBytes();
+        startProcessing();
+        notifyAndReadOutCharacteristic(packetBytes);
+
+        verify(mMockBluetoothGatt, times(1))
+                .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic)));
+        verify(mMockDataReceiver, never()).onDataReceived(any());
+        verify(mMockTransportClientCallback, times(1))
+                .onTerminated(TerminationReason.REMOTE_DEVICE_SECID_ERROR);
+        assertThat(mGattTransportClientProvider.isStarted()).isFalse();
+    }
+
+    @Test
+    public void testOutCharactersticNotifyAndRead_packetLengthOverflow() {
+        byte[] messagePayload = new byte[OPTIMIZED_DATA_PACKET_SIZE + 1];
+        Arrays.fill(messagePayload, (byte) 3);
+        byte[] messageBytes =
+                new FiraConnectorMessage(
+                                MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, messagePayload)
+                        .toBytes();
+        byte[] packet_bytes =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ false,
+                                SECID,
+                                Arrays.copyOf(messageBytes, messageBytes.length))
+                        .toBytes();
+
+        FiraConnectorDataPacket expectedInPacket =
+                new FiraConnectorDataPacket(
+                        /*lastChainingPacket=*/ true,
+                        TransportProvider.ADMIN_SECID,
+                        new AdminErrorMessage(ErrorType.DATA_PACKET_LENGTH_OVERFLOW).toBytes());
+
+        startProcessing();
+        assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue();
+        notifyAndReadOutCharacteristic(packet_bytes);
+
+        verify(mMockBluetoothGatt, times(1))
+                .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic)));
+        verify(mMockDataReceiver, never()).onDataReceived(any());
+        verify(mMockBluetoothGatt, times(1))
+                .writeCharacteristic(
+                        argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)),
+                        eq(expectedInPacket.toBytes()),
+                        eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT));
+    }
+
+    @Test
+    public void testOutCharactersticNotifyAndRead_tooManyConcurrentSessions() {
+        byte[] messagePayload = new byte[24];
+        Arrays.fill(messagePayload, (byte) 3);
+        int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1;
+        byte[] messageBytes =
+                new FiraConnectorMessage(
+                                MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, messagePayload)
+                        .toBytes();
+        byte[] packet_bytes1 =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ false,
+                                SECID,
+                                Arrays.copyOf(messageBytes, payloadSize))
+                        .toBytes();
+        byte[] packet_bytes2 =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ true,
+                                SECID2,
+                                Arrays.copyOfRange(messageBytes, payloadSize, messageBytes.length))
+                        .toBytes();
+        FiraConnectorDataPacket expectedInPacket =
+                new FiraConnectorDataPacket(
+                        /*lastChainingPacket=*/ true,
+                        TransportProvider.ADMIN_SECID,
+                        new AdminErrorMessage(
+                                        ErrorType.TOO_MANY_CONCURRENT_FRAGMENTED_MESSAGE_SESSIONS)
+                                .toBytes());
+
+        startProcessing();
+        assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue();
+        notifyAndReadOutCharacteristic(packet_bytes1);
+        notifyAndReadOutCharacteristic(packet_bytes2);
+
+        verify(mMockBluetoothGatt, times(2))
+                .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic)));
+        verify(mMockDataReceiver, never()).onDataReceived(any());
+        verify(mMockBluetoothGatt, times(1))
+                .writeCharacteristic(
+                        argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)),
+                        eq(expectedInPacket.toBytes()),
+                        eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT));
+    }
+
+    @Test
+    public void testOutCharactersticNotifyAndRead_messageLengthOverflow() {
+        int payloadSize = 200;
+        FiraConnectorCapabilities capabilities =
+                new FiraConnectorCapabilities.Builder()
+                        .setOptimizedDataPacketSize(payloadSize + 1)
+                        .setMaxMessageBufferSize(264)
+                        .build();
+        byte[] messagePayload = new byte[265];
+        Arrays.fill(messagePayload, (byte) 3);
+        byte[] messageBytes =
+                new FiraConnectorMessage(
+                                MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, messagePayload)
+                        .toBytes();
+        byte[] packet_bytes1 =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ false,
+                                SECID,
+                                Arrays.copyOf(messageBytes, payloadSize))
+                        .toBytes();
+        byte[] packet_bytes2 =
+                new FiraConnectorDataPacket(
+                                /*lastChainingPacket=*/ true,
+                                SECID,
+                                Arrays.copyOfRange(messageBytes, payloadSize, messageBytes.length))
+                        .toBytes();
+        FiraConnectorDataPacket expectedInPacket =
+                new FiraConnectorDataPacket(
+                        /*lastChainingPacket=*/ true,
+                        TransportProvider.ADMIN_SECID,
+                        new AdminErrorMessage(ErrorType.MESSAGE_LENGTH_OVERFLOW).toBytes());
+
+        startProcessing();
+        assertThat(mGattTransportClientProvider.setCapabilites(capabilities)).isTrue();
+        notifyAndReadOutCharacteristic(packet_bytes1);
+        notifyAndReadOutCharacteristic(packet_bytes2);
+
+        verify(mMockBluetoothGatt, times(2))
+                .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic)));
+        verify(mMockDataReceiver, never()).onDataReceived(any());
+        verify(mMockBluetoothGatt, times(1))
+                .writeCharacteristic(
+                        argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)),
+                        eq(expectedInPacket.toBytes()),
+                        eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT));
+    }
 }