blob: 24e78e6e193f8dc288c2f82f192f126ff403128b [file] [log] [blame]
package com.google.android.connecteddevice.transport.ble
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.le.AdvertiseCallback
import android.content.Context
import android.os.ParcelUuid
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.connecteddevice.core.util.mockToBeAlive
import com.google.android.connecteddevice.transport.ConnectChallenge
import com.google.android.connecteddevice.transport.IDataReceivedListener
import com.google.android.connecteddevice.transport.IDataSendCallback
import com.google.android.connecteddevice.transport.IDeviceDisconnectedListener
import com.google.android.connecteddevice.transport.IDeviceMaxDataSizeChangedListener
import com.google.android.connecteddevice.transport.IDiscoveryCallback
import com.google.android.connecteddevice.util.ByteUtils
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.atLeastOnce
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import java.time.Duration
import java.util.UUID
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
private const val TEST_REMOTE_DEVICE_ADDRESS = "00:11:22:33:AA:BB"
private const val TEST_DEFAULT_MTU_SIZE = 23
private const val TEST_DEVICE_NAME = "TestName"
@RunWith(AndroidJUnit4::class)
class BlePeripheralProtocolTest {
private val testIdentifier = ParcelUuid(UUID.randomUUID())
private val testReconnectServiceUuid = UUID.randomUUID()
private val testReconnectDataUuid = UUID.randomUUID()
private val testAdvertiseDataCharacteristicUuid = UUID.randomUUID()
private val testWriteCharacteristicUuid = UUID.randomUUID()
private val testReadCharacteristicUuid = UUID.randomUUID()
private val testMaxReconnectAdvertisementDuration = Duration.ofMinutes(6)
private val testMessage = "TestMessage".toByteArray()
private val testChallenge =
ConnectChallenge("TestChallenge".toByteArray(), "TestSalt".toByteArray())
private val mockBlePeripheralManager = mock<BlePeripheralManager>()
private val mockDiscoveryCallback = mockToBeAlive<IDiscoveryCallback>()
private val mockDataSendCallback = mockToBeAlive<IDataSendCallback>()
private val mockSecondDataSendCallback = mockToBeAlive<IDataSendCallback>()
private val mockDisconnectedListener = mockToBeAlive<IDeviceDisconnectedListener>()
private val mockDataReceivedListener = mockToBeAlive<IDataReceivedListener>()
private val mockMaxDataSizeChangedListener = mockToBeAlive<IDeviceMaxDataSizeChangedListener>()
private lateinit var blePeripheralProtocol: BlePeripheralProtocol
private lateinit var testBluetoothDevice: BluetoothDevice
@Before
fun setUp() {
val bluetoothManager =
ApplicationProvider.getApplicationContext<Context>()
.getSystemService(BluetoothManager::class.java)
testBluetoothDevice = bluetoothManager.adapter.getRemoteDevice(TEST_REMOTE_DEVICE_ADDRESS)
blePeripheralProtocol =
BlePeripheralProtocol(
mockBlePeripheralManager,
testReconnectServiceUuid,
testReconnectDataUuid,
testAdvertiseDataCharacteristicUuid,
testWriteCharacteristicUuid,
testReadCharacteristicUuid,
testMaxReconnectAdvertisementDuration,
TEST_DEFAULT_MTU_SIZE,
directExecutor()
)
}
@Test
fun startAssociationDiscovery_startsWithIdentifier() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<BluetoothGattService>().apply {
verify(mockBlePeripheralManager).startAdvertising(capture(), any(), any(), any())
assertThat(firstValue.uuid).isEqualTo(testIdentifier.uuid)
}
}
@Test
fun startAssociationDiscovery_startedSuccessfully() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
firstValue.onStartSuccess(/* settingsInEffect= */ null)
}
verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
}
@Test
fun startAssociationDiscovery_failedToStart() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
firstValue.onStartFailure(/* errorCode= */ 0)
}
verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
}
@Test
fun startAssociationDiscovery_alreadyInConnection() {
establishConnection(testBluetoothDevice)
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
// Should only be invoked once for establishing connection.
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), any())
}
@Test
fun startAssociationDiscovery_multipleTimesSucceed() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager, atLeastOnce())
.startAdvertising(any(), any(), any(), capture())
firstValue.onStartSuccess(/* settingsInEffect= */ null)
}
blePeripheralProtocol.stopAssociationDiscovery()
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager, atLeastOnce())
.startAdvertising(any(), any(), any(), capture())
firstValue.onStartSuccess(/* settingsInEffect= */ null)
}
verify(mockBlePeripheralManager, times(2)).startAdvertising(any(), any(), any(), any())
verify(mockBlePeripheralManager).stopAdvertising(any())
verify(mockDiscoveryCallback, times(2)).onDiscoveryStartedSuccessfully()
}
@Test
fun stopAssociationDiscovery_stopSuccessfully() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
blePeripheralProtocol.stopAssociationDiscovery()
val advertiseCallback =
argumentCaptor<AdvertiseCallback>()
.apply { verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture()) }
.firstValue
verify(mockBlePeripheralManager).stopAdvertising(eq(advertiseCallback))
}
@Test
fun startConnectionDiscovery_startedSuccessfully() {
blePeripheralProtocol.startConnectionDiscovery(
testIdentifier,
testChallenge,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
firstValue.onStartSuccess(/* settingsInEffect= */ null)
}
verify(mockDiscoveryCallback).onDiscoveryStartedSuccessfully()
}
@Test
fun startConnectionDiscovery_failedToStart() {
blePeripheralProtocol.startConnectionDiscovery(
testIdentifier,
testChallenge,
mockDiscoveryCallback
)
argumentCaptor<AdvertiseCallback>().apply {
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture())
firstValue.onStartFailure(/* errorCode= */ 0)
}
verify(mockDiscoveryCallback).onDiscoveryFailedToStart()
}
@Test
fun startConnectionDiscovery_alreadyInConnection() {
establishConnection(testBluetoothDevice)
blePeripheralProtocol.startConnectionDiscovery(
testIdentifier,
testChallenge,
mockDiscoveryCallback
)
// Should only be invoked once for establishing connection.
verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), any())
}
@Test
fun stopConnectionDiscovery_stopSuccessfully() {
blePeripheralProtocol.startConnectionDiscovery(
testIdentifier,
testChallenge,
mockDiscoveryCallback
)
blePeripheralProtocol.stopConnectionDiscovery(testIdentifier)
val advertiseCallback =
argumentCaptor<AdvertiseCallback>()
.apply { verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture()) }
.firstValue
verify(mockBlePeripheralManager).stopAdvertising(eq(advertiseCallback))
}
@Test
fun sendData_oneMessage_sendSuccessfully() {
val testProtocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.sendData(testProtocolId, testMessage, mockDataSendCallback)
val writeCharacteristic =
argumentCaptor<BluetoothGattCharacteristic>()
.apply {
verify(mockBlePeripheralManager)
.notifyCharacteristicChanged(eq(testBluetoothDevice), capture(), any())
}
.firstValue
argumentCaptor<BlePeripheralManager.OnCharacteristicReadListener>().apply {
verify(mockBlePeripheralManager).addOnCharacteristicReadListener(capture())
firstValue.onCharacteristicRead(testBluetoothDevice)
}
assertThat(writeCharacteristic.uuid).isEqualTo(testWriteCharacteristicUuid)
assertThat(writeCharacteristic.value).isEqualTo(testMessage)
verify(mockDataSendCallback).onDataSentSuccessfully()
}
@Test
fun sendData_twoMessages_invokeCallbackSuccessfully() {
val testProtocolId = establishConnection(testBluetoothDevice)
val onCharacteristicReadListener =
argumentCaptor<BlePeripheralManager.OnCharacteristicReadListener>()
.apply { verify(mockBlePeripheralManager).addOnCharacteristicReadListener(capture()) }
.firstValue
blePeripheralProtocol.sendData(testProtocolId, testMessage, mockDataSendCallback)
onCharacteristicReadListener.onCharacteristicRead(testBluetoothDevice)
blePeripheralProtocol.sendData(testProtocolId, testMessage, mockSecondDataSendCallback)
onCharacteristicReadListener.onCharacteristicRead(testBluetoothDevice)
verify(mockDataSendCallback).onDataSentSuccessfully()
verify(mockSecondDataSendCallback).onDataSentSuccessfully()
}
@Test
fun sendData_failedToSend_noConnectedDevice() {
val testProtocolId = UUID.randomUUID().toString()
blePeripheralProtocol.sendData(testProtocolId, testMessage, mockDataSendCallback)
verify(mockBlePeripheralManager, never()).notifyCharacteristicChanged(any(), any(), any())
verify(mockDataSendCallback).onDataFailedToSend()
}
@Test
fun sendData_failedToSend_dataTooLarge() {
val largeTestMessage = ByteUtils.randomBytes(TEST_DEFAULT_MTU_SIZE + 1)
val protocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.sendData(protocolId, largeTestMessage, mockDataSendCallback)
verify(mockDataSendCallback).onDataFailedToSend()
}
@Test
fun disconnectDevice_cleanupBlePeripheralManager() {
val protocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.disconnectDevice(protocolId)
// First cleanup for establishing the new connection
// Second cleanup for disconnection
verify(mockBlePeripheralManager, times(2)).cleanup()
}
@Test
fun reset_stopStartedAdvertising() {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
blePeripheralProtocol.reset()
val advertiseCallback =
argumentCaptor<AdvertiseCallback>()
.apply { verify(mockBlePeripheralManager).startAdvertising(any(), any(), any(), capture()) }
.firstValue
verify(mockBlePeripheralManager).stopAdvertising(eq(advertiseCallback))
}
@Test
fun reset_cleanupBlePeripheralManager() {
blePeripheralProtocol.reset()
verify(mockBlePeripheralManager).cleanup()
}
@Test
fun getMaxWriteSize() {
val testProtocolId = UUID.randomUUID().toString()
val expectedMaxWriteSize = TEST_DEFAULT_MTU_SIZE - BlePeripheralProtocol.ATT_PROTOCOL_BYTES
assertThat(blePeripheralProtocol.getMaxWriteSize(testProtocolId))
.isEqualTo(expectedMaxWriteSize)
}
@Test
fun onMessageReceived_callbackInvoked() {
val testReadCharacteristic =
BluetoothGattCharacteristic(
testReadCharacteristicUuid,
BluetoothGattCharacteristic.PROPERTY_WRITE or
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE,
BluetoothGattCharacteristic.PERMISSION_WRITE
)
val testProtocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.registerDataReceivedListener(testProtocolId, mockDataReceivedListener)
argumentCaptor<BlePeripheralManager.OnCharacteristicWriteListener>().apply {
verify(mockBlePeripheralManager).addOnCharacteristicWriteListener(capture())
firstValue.onCharacteristicWrite(testBluetoothDevice, testReadCharacteristic, testMessage)
}
verify(mockDataReceivedListener).onDataReceived(testProtocolId, testMessage)
}
@Test
fun onDeviceDisconnected_callbackInvoked() {
val testProtocolId = establishConnection(testBluetoothDevice)
blePeripheralProtocol.registerDeviceDisconnectedListener(
testProtocolId,
mockDisconnectedListener
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
firstValue.onRemoteDeviceDisconnected(testBluetoothDevice)
}
verify(mockDisconnectedListener).onDeviceDisconnected(eq(testProtocolId))
}
@Test
fun onDeviceMaxDataSizeChanged_callbackInvoked() {
val testProtocolId = establishConnection(testBluetoothDevice)
val testMtuSize = 20
blePeripheralProtocol.registerDeviceMaxDataSizeChangedListener(
testProtocolId,
mockMaxDataSizeChangedListener
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
firstValue.onMtuSizeChanged(testMtuSize)
}
verify(mockMaxDataSizeChangedListener)
.onDeviceMaxDataSizeChanged(
eq(testProtocolId),
eq(testMtuSize - BlePeripheralProtocol.ATT_PROTOCOL_BYTES)
)
}
@Test
fun onDeviceConnected_stopsAdvertising() {
establishConnection(testBluetoothDevice)
verify(mockBlePeripheralManager).stopAdvertising(any())
}
@Test
fun getBluetoothDevice_returnNonNullDevice() {
val protocolId = establishConnection(testBluetoothDevice)
assertThat(blePeripheralProtocol.getBluetoothDeviceById(protocolId)).isNotNull()
}
@Test
fun getBluetoothDevice_returnNullWhenDisconnected() {
val protocolId = establishConnection(testBluetoothDevice)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
firstValue.onRemoteDeviceDisconnected(testBluetoothDevice)
}
assertThat(blePeripheralProtocol.getBluetoothDeviceById(protocolId)).isNull()
}
@Test
fun getBluetoothDevice_returnNullWhenProtocolIdMismatch() {
establishConnection(testBluetoothDevice)
assertThat(blePeripheralProtocol.getBluetoothDeviceById("RandomProtocolId")).isNull()
}
private fun establishConnection(bluetoothDevice: BluetoothDevice): String {
blePeripheralProtocol.startAssociationDiscovery(
TEST_DEVICE_NAME,
testIdentifier,
mockDiscoveryCallback
)
argumentCaptor<BlePeripheralManager.Callback>().apply {
verify(mockBlePeripheralManager).registerCallback(capture())
firstValue.onRemoteDeviceConnected(bluetoothDevice)
}
return argumentCaptor<String>()
.apply { verify(mockDiscoveryCallback).onDeviceConnected(capture()) }
.firstValue
}
}