blob: dd1c90502d637847500c0f9c9db012bf6ed3bebe [file] [log] [blame]
/*
* Copyright (C) 2019 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.car.trust;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Handler;
import androidx.test.runner.AndroidJUnit4;
import com.android.car.BLEStreamProtos.BLEMessageProto.BLEMessage;
import com.android.car.BLEStreamProtos.BLEOperationProto.OperationType;
import com.android.car.protobuf.InvalidProtocolBufferException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
/**
* Unit test for the {@link BleMessageStreamV1}.
*
* <p>Run:
* {@code atest CarServiceUnitTest:BleMessageStreamV1Test}
*/
@RunWith(AndroidJUnit4.class)
public class BleMessageStreamV1Test {
private static final String ADDRESS_MOCK = "00:11:22:33:AA:BB";
// The UUID values here are arbitrary.
private static final UUID WRITE_UUID = UUID.fromString("9a138a69-7c29-400f-9e71-fc29516f9f8b");
private static final UUID READ_UUID = UUID.fromString("3e344860-e688-4cce-8411-16161b61ad57");
private BleMessageStreamV1 mBleMessageStream;
private BluetoothDevice mBluetoothDevice;
@Mock BlePeripheralManager mBlePeripheralManager;
@Mock BleMessageStreamCallback mCallbackMock;
@Mock Handler mHandlerMock;
@Mock BluetoothGattCharacteristic mWriteCharacteristicMock;
@Mock BluetoothGattCharacteristic mReadCharacteristicMock;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// Mock so that handler will run anything that is posted to it.
when(mHandlerMock.post(any(Runnable.class))).thenAnswer(invocation -> {
invocation.<Runnable>getArgument(0).run();
return null;
});
// Ensure the mock characteristics return valid UUIDs.
when(mWriteCharacteristicMock.getUuid()).thenReturn(WRITE_UUID);
when(mReadCharacteristicMock.getUuid()).thenReturn(READ_UUID);
mBluetoothDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(ADDRESS_MOCK);
mBleMessageStream = new BleMessageStreamV1(
mHandlerMock, mBlePeripheralManager, mBluetoothDevice, mWriteCharacteristicMock,
mReadCharacteristicMock);
mBleMessageStream.registerCallback(mCallbackMock);
}
@Test
public void writeMessage_noChunkingRequired_sendsCorrectMessage() {
// Ensure that there is enough space to fit the message.
mBleMessageStream.setMaxWriteSize(512);
byte[] message = "message".getBytes();
boolean isPayloadEncrypted = true;
OperationType operationType = OperationType.CLIENT_MESSAGE;
mBleMessageStream.writeMessage(message, operationType, isPayloadEncrypted);
BLEMessage expectedMessage = BLEMessageV1Factory.makeBLEMessage(
message, operationType, isPayloadEncrypted);
// Verify that the message was written.
verify(mWriteCharacteristicMock).setValue(expectedMessage.toByteArray());
// Verify that there is also a notification of the characteristic change.
verify(mBlePeripheralManager).notifyCharacteristicChanged(mBluetoothDevice,
mWriteCharacteristicMock, false);
}
@Test
public void writeMessage_chunkingRequired_sendsCorrectMessage()
throws InvalidProtocolBufferException, IOException {
boolean isPayloadEncrypted = true;
OperationType operationType = OperationType.CLIENT_MESSAGE;
int maxSize = 20;
int requiredWrites = 9;
byte[] message = makeMessage(requiredWrites * maxSize);
int headerSize = BLEMessageV1Factory.getProtoHeaderSize(
operationType, message.length, isPayloadEncrypted);
// Make sure the payload can't fit into one chunk.
mBleMessageStream.setMaxWriteSize(maxSize + headerSize);
mBleMessageStream.writeMessage(message, operationType, isPayloadEncrypted);
// Each part of the message requires an ACK except the last one.
int numOfAcks = requiredWrites - 1;
for (int i = 0; i < numOfAcks; i++) {
mBleMessageStream.onCharacteristicWrite(
mBluetoothDevice,
mReadCharacteristicMock,
BLEMessageV1Factory.makeAcknowledgementMessage().toByteArray());
}
// Each ACK should trigger a canceling of the retry runnable.
verify(mHandlerMock, times(numOfAcks)).removeCallbacks(any(Runnable.class));
ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
verify(mWriteCharacteristicMock, times(requiredWrites)).setValue(
messageCaptor.capture());
verify(mBlePeripheralManager, times(requiredWrites))
.notifyCharacteristicChanged(mBluetoothDevice,
mWriteCharacteristicMock, false);
List<byte[]> writtenBytes = messageCaptor.getAllValues();
ByteArrayOutputStream reassembledMessageStream = new ByteArrayOutputStream();
// Verify the packet numbers.
for (int i = 0; i < writtenBytes.size(); i++) {
BLEMessage bleMessage = BLEMessage.parseFrom(writtenBytes.get(i));
assertThat(bleMessage.getPacketNumber()).isEqualTo(i + 1);
assertThat(bleMessage.getTotalPackets()).isEqualTo(writtenBytes.size());
assertThat(bleMessage.getIsPayloadEncrypted()).isTrue();
reassembledMessageStream.write(bleMessage.getPayload().toByteArray());
}
// Verify the reassembled message.
assertThat(reassembledMessageStream.toByteArray()).isEqualTo(message);
}
@Test
public void writeMessage_chunkingRequired_retriesSamePayloadIfNoAckReceived()
throws InvalidProtocolBufferException, IOException {
// Execute delayed runnables immediately to simulate an ACK timeout.
mockHandlerToExecuteDelayedRunnablesImmediately();
boolean isPayloadEncrypted = true;
OperationType operationType = OperationType.CLIENT_MESSAGE;
int maxSize = 20;
int requiredWrites = 9;
byte[] message = makeMessage(requiredWrites * maxSize);
int headerSize = BLEMessageV1Factory.getProtoHeaderSize(
operationType, message.length, isPayloadEncrypted);
// Make sure the payload can't fit into one chunk.
mBleMessageStream.setMaxWriteSize(maxSize + headerSize);
mBleMessageStream.writeMessage(message, operationType, isPayloadEncrypted);
ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
// Because there is no ACK, the write should be retried up to the limit.
verify(mWriteCharacteristicMock, times(BleMessageStreamV1.BLE_MESSAGE_RETRY_LIMIT))
.setValue(messageCaptor.capture());
verify(mBlePeripheralManager, times(BleMessageStreamV1.BLE_MESSAGE_RETRY_LIMIT))
.notifyCharacteristicChanged(mBluetoothDevice, mWriteCharacteristicMock, false);
List<byte[]> writtenBytes = messageCaptor.getAllValues();
List<byte[]> writtenPayloads = new ArrayList<>();
for (int i = 0; i < writtenBytes.size(); i++) {
BLEMessage bleMessage = BLEMessage.parseFrom(writtenBytes.get(i));
// The same packet should be written.
assertThat(bleMessage.getPacketNumber()).isEqualTo(1);
assertThat(bleMessage.getTotalPackets()).isEqualTo(requiredWrites);
assertThat(bleMessage.getIsPayloadEncrypted()).isTrue();
writtenPayloads.add(bleMessage.getPayload().toByteArray());
}
// Verify that the same payload is being written.
for (byte[] payload : writtenPayloads) {
assertThat(payload).isEqualTo(writtenPayloads.get(0));
}
}
@Test
public void writeMessage_chunkingRequired_notifiesCallbackIfNoAck()
throws InvalidProtocolBufferException, IOException {
// Execute delayed runnables immediately to simulate an ACK timeout.
mockHandlerToExecuteDelayedRunnablesImmediately();
boolean isPayloadEncrypted = true;
OperationType operationType = OperationType.CLIENT_MESSAGE;
int maxSize = 20;
int requiredWrites = 9;
byte[] message = makeMessage(requiredWrites * maxSize);
int headerSize = BLEMessageV1Factory.getProtoHeaderSize(
operationType, message.length, isPayloadEncrypted);
// Make sure the payload can't fit into one chunk.
mBleMessageStream.setMaxWriteSize(maxSize + headerSize);
mBleMessageStream.writeMessage(message, operationType, isPayloadEncrypted);
// Because there is no ACK, the write should be retried up to the limit.
verify(mWriteCharacteristicMock, times(BleMessageStreamV1.BLE_MESSAGE_RETRY_LIMIT))
.setValue(any(byte[].class));
verify(mBlePeripheralManager, times(BleMessageStreamV1.BLE_MESSAGE_RETRY_LIMIT))
.notifyCharacteristicChanged(mBluetoothDevice, mWriteCharacteristicMock, false);
// But the callback should only be notified once.
verify(mCallbackMock).onWriteMessageError();
}
@Test
public void processClientMessage_noChunking_notifiesCallbackForCompleteMessage() {
byte[] payload = "message".getBytes();
// This client message is a complete message, meaning it is part 1 of 1.
BLEMessage clientMessage = BLEMessageV1Factory.makeBLEMessage(
payload, OperationType.CLIENT_MESSAGE, /* isPayloadEncrypted= */ true);
mBleMessageStream.onCharacteristicWrite(
mBluetoothDevice,
mReadCharacteristicMock,
clientMessage.toByteArray());
// The callback should be notified with only the payload.
verify(mCallbackMock).onMessageReceived(payload, READ_UUID);
// And the callback should only be notified once.
verify(mCallbackMock).onMessageReceived(any(byte[].class), any(UUID.class));
}
@Test
public void processClientMessage_chunkingRequired_notifiesCallbackForCompleteMessage() {
// The length here is arbitrary.
int payloadLength = 1024;
byte[] payload = makeMessage(payloadLength);
// Ensure the max size is smaller than the payload length.
int maxSize = 50;
List<BLEMessage> clientMessages = BLEMessageV1Factory.makeBLEMessages(
payload, OperationType.CLIENT_MESSAGE, maxSize, /* isPayloadEncrypted= */ true);
for (BLEMessage message : clientMessages) {
mBleMessageStream.onCharacteristicWrite(
mBluetoothDevice,
mReadCharacteristicMock,
message.toByteArray());
}
// The callback should be notified with only the payload.
verify(mCallbackMock).onMessageReceived(payload, READ_UUID);
// And the callback should only be notified once.
verify(mCallbackMock).onMessageReceived(any(byte[].class), any(UUID.class));
}
@Test
public void processClientMessage_chunkingRequired_sendsACKForEachMessagePart() {
// The length here is arbitrary.
int payloadLength = 1024;
byte[] payload = makeMessage(payloadLength);
// Ensure the max size is smaller than the payload length.
int maxSize = 50;
List<BLEMessage> clientMessages = BLEMessageV1Factory.makeBLEMessages(
payload, OperationType.CLIENT_MESSAGE, maxSize, /* isPayloadEncrypted= */ true);
for (BLEMessage message : clientMessages) {
mBleMessageStream.onCharacteristicWrite(
mBluetoothDevice,
mReadCharacteristicMock,
message.toByteArray());
}
ArgumentCaptor<byte[]> messageCaptor = ArgumentCaptor.forClass(byte[].class);
// An ACK should be sent for each message received, except the last one.
verify(mWriteCharacteristicMock, times(clientMessages.size() - 1))
.setValue(messageCaptor.capture());
verify(mBlePeripheralManager, times(clientMessages.size() - 1))
.notifyCharacteristicChanged(mBluetoothDevice, mWriteCharacteristicMock, false);
List<byte[]> writtenValues = messageCaptor.getAllValues();
byte[] ackMessageBytes = BLEMessageV1Factory.makeAcknowledgementMessage().toByteArray();
// Now verify that each byte was an ACK message.
for (byte[] writtenValue : writtenValues) {
assertThat(writtenValue).isEqualTo(ackMessageBytes);
}
}
private void mockHandlerToExecuteDelayedRunnablesImmediately() {
when(mHandlerMock.postDelayed(any(Runnable.class), anyLong())).thenAnswer(invocation -> {
invocation.<Runnable>getArgument(0).run();
return null;
});
}
/** Returns a random message of the specified length. */
private byte[] makeMessage(int length) {
byte[] message = new byte[length];
new Random().nextBytes(message);
return message;
}
}