| /* |
| * 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.server.uwb.discovery.ble; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.argThat; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| import static org.mockito.Mockito.verifyZeroInteractions; |
| import static org.mockito.Mockito.when; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothGatt; |
| import android.bluetooth.BluetoothGattCallback; |
| import android.bluetooth.BluetoothGattCharacteristic; |
| import android.bluetooth.BluetoothGattDescriptor; |
| import android.bluetooth.BluetoothGattService; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.BluetoothStatusCodes; |
| import android.bluetooth.le.ScanResult; |
| import android.content.AttributionSource; |
| import android.content.Context; |
| import android.uwb.UwbTestUtils; |
| |
| import androidx.test.filters.SmallTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.server.uwb.discovery.Transport.DataReceiver; |
| 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; |
| import com.android.server.uwb.discovery.info.FiraConnectorMessage.InstructionCode; |
| import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType; |
| import com.android.server.uwb.discovery.info.SecureComponentInfo; |
| import com.android.server.uwb.discovery.info.TransportClientInfo; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.ArgumentMatcher; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.mockito.invocation.InvocationOnMock; |
| import org.mockito.stubbing.Answer; |
| |
| import java.util.Arrays; |
| import java.util.concurrent.Executor; |
| |
| /** Unit test for {@link GattTransportClientProvider} */ |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class GattTransportClientProviderTest { |
| private static final String TAG = GattTransportClientProviderTest.class.getSimpleName(); |
| |
| 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.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(); |
| private static final int OPTIMIZED_DATA_PACKET_SIZE = 21; |
| private static final FiraConnectorCapabilities CAPABILITIES = |
| new FiraConnectorCapabilities.Builder() |
| .setOptimizedDataPacketSize(OPTIMIZED_DATA_PACKET_SIZE) |
| .setMaxMessageBufferSize(265) |
| .addSecureComponentInfo( |
| new SecureComponentInfo( |
| /*static_indication=*/ true, |
| SECID, |
| SecureComponentInfo.SecureComponentType.ESE_NONREMOVABLE, |
| SecureComponentInfo.SecureComponentProtocolType |
| .FIRA_OOB_ADMINISTRATIVE_PROTOCOL)) |
| .build(); |
| private static final BluetoothGattCharacteristic IN_CHARACTERSTIC = |
| new BluetoothGattCharacteristic( |
| UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid(), |
| BluetoothGattCharacteristic.PROPERTY_WRITE, |
| BluetoothGattCharacteristic.PERMISSION_WRITE); |
| private static final BluetoothGattCharacteristic CAPABILITIES_CHARACTERSTIC = |
| new BluetoothGattCharacteristic( |
| UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid(), |
| BluetoothGattCharacteristic.PROPERTY_WRITE, |
| BluetoothGattCharacteristic.PERMISSION_WRITE); |
| private BluetoothGattCharacteristic mOutCharacterstic = |
| new BluetoothGattCharacteristic( |
| UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid(), |
| BluetoothGattCharacteristic.PROPERTY_READ |
| | BluetoothGattCharacteristic.PROPERTY_NOTIFY, |
| BluetoothGattCharacteristic.PERMISSION_READ); |
| private BluetoothGattDescriptor mCccdDescriptor = |
| new BluetoothGattDescriptor( |
| UuidConstants.CCCD_UUID.getUuid(), BluetoothGattDescriptor.PERMISSION_READ); |
| |
| @Mock AttributionSource mMockAttributionSource; |
| @Mock Context mMockContext; |
| @Mock TransportClientCallback mMockTransportClientCallback; |
| // @Mock TransportClientInfo mMockTransportClientInfo; |
| @Mock ScanResult mMockScanResult; |
| @Mock BluetoothDevice mMockBluetoothDevice; |
| @Mock BluetoothGatt mMockBluetoothGatt; |
| @Mock BluetoothGattService mMockBluetoothGattService; |
| @Mock DataReceiver mMockDataReceiver; |
| |
| private TransportClientInfo mTransportClientInfo; |
| private GattTransportClientProvider mGattTransportClientProvider; |
| private BluetoothGattCallback mBluetoothGattCallback; |
| private FiraConnectorCapabilities mDefaultCapabilities = |
| new FiraConnectorCapabilities.Builder().build(); |
| |
| // For matching the package name of a PackageInfo |
| private static class CharacteristicMatcher |
| implements ArgumentMatcher<BluetoothGattCharacteristic> { |
| private final BluetoothGattCharacteristic mCharacteristic; |
| |
| CharacteristicMatcher(BluetoothGattCharacteristic characteristic) { |
| mCharacteristic = characteristic; |
| } |
| |
| @Override |
| public boolean matches(BluetoothGattCharacteristic characteristic) { |
| return characteristic.getUuid().equals(mCharacteristic.getUuid()); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format( |
| "BluetoothGattCharacteristic with uuid %s'", mCharacteristic.getUuid()); |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| MockitoAnnotations.initMocks(this); |
| when(mMockContext.createContext(any())).thenReturn(mMockContext); |
| when(mMockBluetoothGatt.connect()).thenReturn(true); |
| when(mMockScanResult.getDevice()).thenReturn(mMockBluetoothDevice); |
| mTransportClientInfo = new TransportClientInfo(mMockScanResult); |
| |
| mGattTransportClientProvider = |
| new GattTransportClientProvider( |
| mMockAttributionSource, |
| mMockContext, |
| EXECUTOR, |
| SECID, |
| mTransportClientInfo, |
| mMockTransportClientCallback); |
| mGattTransportClientProvider.registerDataReceiver(mMockDataReceiver); |
| assertThat(mOutCharacterstic.addDescriptor(mCccdDescriptor)).isTrue(); |
| } |
| |
| @Test |
| public void testStartAndStop() { |
| when(mMockBluetoothDevice.connectGatt(eq(mMockContext), anyBoolean(), any(), anyInt())) |
| .thenReturn(mMockBluetoothGatt); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| assertThat(mGattTransportClientProvider.isStarted()).isTrue(); |
| assertThat(mGattTransportClientProvider.stop()).isTrue(); |
| verify(mMockBluetoothGatt, times(1)).disconnect(); |
| assertThat(mGattTransportClientProvider.isStarted()).isFalse(); |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockBluetoothGatt, times(1)).connect(); |
| } |
| |
| @Test |
| public void testStart_successAndRejectRestart() { |
| when(mMockBluetoothDevice.connectGatt(eq(mMockContext), anyBoolean(), any(), anyInt())) |
| .thenReturn(mMockBluetoothGatt); |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| assertThat(mGattTransportClientProvider.start()).isFalse(); |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| verify(mMockBluetoothGatt, never()).connect(); |
| } |
| |
| @Test |
| public void testStop_successAndRejectRestop() { |
| when(mMockBluetoothDevice.connectGatt(eq(mMockContext), anyBoolean(), any(), anyInt())) |
| .thenReturn(mMockBluetoothGatt); |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| assertThat(mGattTransportClientProvider.stop()).isTrue(); |
| verify(mMockBluetoothGatt, times(1)).disconnect(); |
| assertThat(mGattTransportClientProvider.stop()).isFalse(); |
| verify(mMockBluetoothGatt, times(1)).disconnect(); |
| } |
| |
| private void setupGattConnect() { |
| Answer notifyConnectionStateChange = |
| new Answer() { |
| public BluetoothGatt answer(InvocationOnMock invocation) { |
| mBluetoothGattCallback = |
| (BluetoothGattCallback) invocation.getArgument(/*index=*/ 2); |
| EXECUTOR.execute( |
| () -> |
| mBluetoothGattCallback.onConnectionStateChange( |
| mMockBluetoothGatt, |
| BluetoothGatt.GATT_SUCCESS, |
| BluetoothProfile.STATE_CONNECTED)); |
| return mMockBluetoothGatt; |
| } |
| }; |
| doAnswer(notifyConnectionStateChange) |
| .when(mMockBluetoothDevice) |
| .connectGatt(eq(mMockContext), anyBoolean(), any(), anyInt()); |
| } |
| |
| private void setupGattServicesDiscover() { |
| when(mMockBluetoothGatt.getService(UuidConstants.FIRA_CP_PARCEL_UUID.getUuid())) |
| .thenReturn(mMockBluetoothGattService); |
| when(mMockBluetoothGattService.getCharacteristic( |
| UuidConstants.CP_IN_CONTROL_POINT_UUID.getUuid())) |
| .thenReturn(IN_CHARACTERSTIC); |
| when(mMockBluetoothGattService.getCharacteristic( |
| UuidConstants.CP_OUT_CONTROL_POINT_UUID.getUuid())) |
| .thenReturn(mOutCharacterstic); |
| when(mMockBluetoothGattService.getCharacteristic( |
| UuidConstants.CP_FIRA_CONNECTOR_CAPABILITIES_UUID.getUuid())) |
| .thenReturn(CAPABILITIES_CHARACTERSTIC); |
| Answer notifyGattServicesDiscovered = |
| new Answer() { |
| public Boolean answer(InvocationOnMock invocation) { |
| EXECUTOR.execute( |
| () -> |
| mBluetoothGattCallback.onServicesDiscovered( |
| (BluetoothGatt) invocation.getMock(), |
| BluetoothGatt.GATT_SUCCESS)); |
| return true; |
| } |
| }; |
| doAnswer(notifyGattServicesDiscovered).when(mMockBluetoothGatt).discoverServices(); |
| } |
| |
| private void setupGattWriteCharacteristic(BluetoothGattCharacteristic gattCharacteristic) { |
| Answer notifyGattCharacteristicWrite = |
| new Answer() { |
| public Integer answer(InvocationOnMock invocation) { |
| EXECUTOR.execute( |
| () -> |
| mBluetoothGattCallback.onCharacteristicWrite( |
| (BluetoothGatt) invocation.getMock(), |
| (BluetoothGattCharacteristic) |
| invocation.getArgument(/*index=*/ 0), |
| BluetoothGatt.GATT_SUCCESS)); |
| return BluetoothStatusCodes.SUCCESS; |
| } |
| }; |
| doAnswer(notifyGattCharacteristicWrite) |
| .when(mMockBluetoothGatt) |
| .writeCharacteristic( |
| argThat(new CharacteristicMatcher(gattCharacteristic)), any(), anyInt()); |
| } |
| |
| private void setupGattWriteDescriptor() { |
| Answer notifyGattDescriptorWrite = |
| new Answer() { |
| public Integer answer(InvocationOnMock invocation) { |
| EXECUTOR.execute( |
| () -> |
| mBluetoothGattCallback.onDescriptorWrite( |
| (BluetoothGatt) invocation.getMock(), |
| (BluetoothGattDescriptor) |
| invocation.getArgument(/*index=*/ 0), |
| BluetoothGatt.GATT_SUCCESS)); |
| return BluetoothStatusCodes.SUCCESS; |
| } |
| }; |
| doAnswer(notifyGattDescriptorWrite).when(mMockBluetoothGatt).writeDescriptor(any(), any()); |
| } |
| |
| private void startProcessing() { |
| setupGattConnect(); |
| setupGattServicesDiscover(); |
| setupGattWriteCharacteristic(CAPABILITIES_CHARACTERSTIC); |
| setupGattWriteDescriptor(); |
| setupGattWriteCharacteristic(IN_CHARACTERSTIC); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockTransportClientCallback, times(1)).onProcessingStarted(); |
| } |
| |
| @Test |
| public void testStartProcessing_serviceNotDiscovered() { |
| setupGattConnect(); |
| when(mMockBluetoothGatt.discoverServices()).thenReturn(true); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| verify(mMockBluetoothGatt, times(1)).discoverServices(); |
| verifyZeroInteractions(mMockTransportClientCallback); |
| } |
| |
| @Test |
| public void testStartProcessing_capabilitiesNotWritten() { |
| setupGattConnect(); |
| setupGattServicesDiscover(); |
| when(mMockBluetoothGatt.writeCharacteristic(any(), any(), anyInt())) |
| .thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| verify(mMockBluetoothGatt, times(1)).discoverServices(); |
| verify(mMockBluetoothGatt, times(1)) |
| .getService(UuidConstants.FIRA_CP_PARCEL_UUID.getUuid()); |
| verify(mMockBluetoothGattService, times(3)).getCharacteristic(any()); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeCharacteristic( |
| any(), |
| eq(mDefaultCapabilities.toBytes()), |
| eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)); |
| verify(mMockTransportClientCallback, never()).onProcessingStarted(); |
| verify(mMockTransportClientCallback, times(1)) |
| .onTerminated(TerminationReason.CHARACTERSTIC_WRITE_FAILURE); |
| } |
| |
| @Test |
| public void testStartProcessing_notificationNotEnabled() { |
| setupGattConnect(); |
| setupGattServicesDiscover(); |
| setupGattWriteCharacteristic(CAPABILITIES_CHARACTERSTIC); |
| when(mMockBluetoothGatt.writeDescriptor(any(), any())) |
| .thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| verify(mMockBluetoothGatt, times(1)).discoverServices(); |
| verify(mMockBluetoothGatt, times(1)) |
| .getService(UuidConstants.FIRA_CP_PARCEL_UUID.getUuid()); |
| verify(mMockBluetoothGattService, times(3)).getCharacteristic(any()); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeCharacteristic( |
| any(), |
| eq(mDefaultCapabilities.toBytes()), |
| eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeDescriptor(any(), eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); |
| verify(mMockTransportClientCallback, never()).onProcessingStarted(); |
| verify(mMockTransportClientCallback, times(1)) |
| .onTerminated(TerminationReason.DESCRIPTOR_WRITE_FAILURE); |
| } |
| |
| @Test |
| public void testStartProcessing_succeed() { |
| startProcessing(); |
| |
| verify(mMockBluetoothDevice, times(1)).connectGatt(any(), anyBoolean(), any(), anyInt()); |
| verify(mMockBluetoothGatt, times(1)).discoverServices(); |
| verify(mMockBluetoothGatt, times(1)) |
| .getService(UuidConstants.FIRA_CP_PARCEL_UUID.getUuid()); |
| verify(mMockBluetoothGattService, times(3)).getCharacteristic(any()); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeCharacteristic( |
| any(), |
| eq(mDefaultCapabilities.toBytes()), |
| eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeDescriptor(any(), eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)); |
| verify(mMockTransportClientCallback, times(1)).onProcessingStarted(); |
| } |
| |
| @Test |
| public void testSetCapabilities_emptyCapabilities() { |
| assertThat(mGattTransportClientProvider.setCapabilites(null)).isFalse(); |
| } |
| |
| @Test |
| public void testSetCapabilities_clientNotStarted() { |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isFalse(); |
| } |
| |
| @Test |
| public void testSetCapabilities_serviceNotDiscovered() { |
| setupGattConnect(); |
| when(mMockBluetoothGatt.discoverServices()).thenReturn(true); |
| |
| assertThat(mGattTransportClientProvider.start()).isTrue(); |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isFalse(); |
| } |
| |
| @Test |
| public void testSetCapabilities_charactersticWriteFailed() { |
| startProcessing(); |
| |
| doReturn(BluetoothStatusCodes.ERROR_UNKNOWN) |
| .when(mMockBluetoothGatt) |
| .writeCharacteristic(any(), any(), anyInt()); |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isFalse(); |
| } |
| |
| @Test |
| public void testSetCapabilities_succeed() { |
| startProcessing(); |
| |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue(); |
| } |
| |
| @Test |
| public void testOnMtuChanged_failed() { |
| startProcessing(); |
| |
| mBluetoothGattCallback.onMtuChanged( |
| mMockBluetoothGatt, /*mtu=*/ 40, BluetoothStatusCodes.ERROR_UNKNOWN); |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue(); |
| } |
| |
| @Test |
| public void testOnMtuChanged_succeed() { |
| startProcessing(); |
| |
| mBluetoothGattCallback.onMtuChanged( |
| mMockBluetoothGatt, /*mtu=*/ 40, BluetoothStatusCodes.ERROR_UNKNOWN); |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue(); |
| } |
| |
| @Test |
| public void testSendMessage_processingNotStarted() { |
| assertThat(mGattTransportClientProvider.sendMessage(SECID, MESSAGE)).isFalse(); |
| } |
| |
| @Test |
| public void testSendMessage_messageLengthGreaterThanCapabilitites() { |
| startProcessing(); |
| |
| byte[] bytes = new byte[270]; |
| Arrays.fill(bytes, (byte) 1); |
| FiraConnectorMessage message = |
| new FiraConnectorMessage(MessageType.EVENT, InstructionCode.DATA_EXCHANGE, bytes); |
| |
| // Capabilities set the max message length to 265, so a message of length 270 exceeded the |
| // limit. |
| assertThat(mGattTransportClientProvider.sendMessage(SECID, message)).isFalse(); |
| } |
| |
| @Test |
| public void testSendMessage_charactersticWriteFailed() { |
| startProcessing(); |
| |
| doReturn(BluetoothStatusCodes.ERROR_UNKNOWN) |
| .when(mMockBluetoothGatt) |
| .writeCharacteristic( |
| argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)), any(), anyInt()); |
| |
| assertThat(mGattTransportClientProvider.sendMessage(SECID, MESSAGE)).isFalse(); |
| verify(mMockTransportClientCallback, times(1)) |
| .onTerminated(TerminationReason.CHARACTERSTIC_WRITE_FAILURE); |
| } |
| |
| @Test |
| public void testSendMessage_succeedOnePacket() { |
| startProcessing(); |
| |
| assertThat(mGattTransportClientProvider.sendMessage(SECID, MESSAGE)).isTrue(); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeCharacteristic( |
| argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)), any(), anyInt()); |
| verify(mMockBluetoothGatt, times(1)) |
| .writeCharacteristic( |
| argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)), |
| eq(DATA_PACKET_BYTES), |
| eq(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)); |
| } |
| |
| @Test |
| public void testSendMessage_succeedThreePacket() { |
| byte[] messagePayload = new byte[51]; |
| Arrays.fill(messagePayload, (byte) 3); |
| FiraConnectorMessage message = |
| new FiraConnectorMessage( |
| MessageType.EVENT, InstructionCode.DATA_EXCHANGE, messagePayload); |
| byte[] messageBytes = message.toBytes(); |
| int payloadSize = mDefaultCapabilities.optimizedDataPacketSize - 1; |
| byte[] packet_bytes1 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ false, |
| SECID, |
| Arrays.copyOf(messageBytes, payloadSize)) |
| .toBytes(); |
| byte[] packet_bytes2 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ false, |
| SECID, |
| Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize)) |
| .toBytes(); |
| byte[] packet_bytes3 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ true, |
| SECID, |
| Arrays.copyOfRange( |
| messageBytes, 2 * payloadSize, messageBytes.length)) |
| .toBytes(); |
| |
| startProcessing(); |
| |
| assertThat(mGattTransportClientProvider.sendMessage(SECID, message)).isTrue(); |
| |
| ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); |
| verify(mMockBluetoothGatt, times(3)) |
| .writeCharacteristic( |
| argThat(new CharacteristicMatcher(IN_CHARACTERSTIC)), |
| captor.capture(), |
| anyInt()); |
| assertThat(captor.getAllValues().get(0)).isEqualTo(packet_bytes1); |
| assertThat(captor.getAllValues().get(1)).isEqualTo(packet_bytes2); |
| assertThat(captor.getAllValues().get(2)).isEqualTo(packet_bytes3); |
| } |
| |
| private void notifyAndReadOutCharacteristic(byte[] bytes) { |
| Answer notifyGattCharacteristicRead = |
| new Answer() { |
| public Boolean answer(InvocationOnMock invocation) { |
| EXECUTOR.execute( |
| () -> |
| mBluetoothGattCallback.onCharacteristicRead( |
| (BluetoothGatt) invocation.getMock(), |
| (BluetoothGattCharacteristic) |
| invocation.getArgument(/*index=*/ 0), |
| bytes, |
| BluetoothGatt.GATT_SUCCESS)); |
| return true; |
| } |
| }; |
| doAnswer(notifyGattCharacteristicRead) |
| .when(mMockBluetoothGatt) |
| .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic))); |
| |
| mBluetoothGattCallback.onCharacteristicChanged( |
| mMockBluetoothGatt, mOutCharacterstic, bytes); |
| } |
| |
| @Test |
| public void testOutCharactersticNotifyAndRead_readFailed() { |
| startProcessing(); |
| when(mMockBluetoothGatt.readCharacteristic(any())).thenReturn(false); |
| |
| mBluetoothGattCallback.onCharacteristicChanged( |
| mMockBluetoothGatt, mOutCharacterstic, DATA_PACKET_BYTES); |
| |
| verify(mMockBluetoothGatt, times(1)) |
| .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic))); |
| verify(mMockTransportClientCallback, times(1)) |
| .onTerminated(TerminationReason.CHARACTERSTIC_READ_FAILURE); |
| } |
| |
| @Test |
| public void testOutCharactersticNotifyAndRead_emptyPacket() { |
| startProcessing(); |
| notifyAndReadOutCharacteristic(new byte[] {}); |
| |
| verify(mMockBluetoothGatt, times(1)) |
| .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic))); |
| verifyNoMoreInteractions(mMockTransportClientCallback); |
| } |
| |
| @Test |
| public void testOutCharactersticNotifyAndRead_succeedOnePacket() { |
| startProcessing(); |
| notifyAndReadOutCharacteristic(DATA_PACKET_BYTES); |
| |
| verify(mMockBluetoothGatt, times(1)) |
| .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic))); |
| ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); |
| verify(mMockDataReceiver, times(1)).onDataReceived(captor.capture()); |
| assertThat(captor.getValue()).isEqualTo(MESSAGE.payload); |
| } |
| |
| @Test |
| public void testOutCharactersticNotifyAndRead_succeedThreePackets() { |
| byte[] messagePayload = new byte[51]; |
| Arrays.fill(messagePayload, (byte) 3); |
| FiraConnectorMessage message = |
| new FiraConnectorMessage( |
| MessageType.COMMAND, InstructionCode.DATA_EXCHANGE, messagePayload); |
| byte[] messageBytes = message.toBytes(); |
| int payloadSize = OPTIMIZED_DATA_PACKET_SIZE - 1; |
| byte[] packet_bytes1 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ false, |
| SECID, |
| Arrays.copyOf(messageBytes, payloadSize)) |
| .toBytes(); |
| byte[] packet_bytes2 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ false, |
| SECID, |
| Arrays.copyOfRange(messageBytes, payloadSize, 2 * payloadSize)) |
| .toBytes(); |
| byte[] packet_bytes3 = |
| new FiraConnectorDataPacket( |
| /*lastChainingPacket=*/ true, |
| SECID, |
| Arrays.copyOfRange( |
| messageBytes, 2 * payloadSize, messageBytes.length)) |
| .toBytes(); |
| |
| startProcessing(); |
| assertThat(mGattTransportClientProvider.setCapabilites(CAPABILITIES)).isTrue(); |
| notifyAndReadOutCharacteristic(packet_bytes1); |
| notifyAndReadOutCharacteristic(packet_bytes2); |
| notifyAndReadOutCharacteristic(packet_bytes3); |
| |
| verify(mMockBluetoothGatt, times(3)) |
| .readCharacteristic(argThat(new CharacteristicMatcher(mOutCharacterstic))); |
| ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class); |
| 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)); |
| } |
| } |