Skip to content
This repository was archived by the owner on Aug 30, 2022. It is now read-only.

Commit 2c407fb

Browse files
authored
Split ConnectGattResult.Failure into specific failure cases (#64)
1 parent ad1d9a7 commit 2c407fb

File tree

6 files changed

+65
-44
lines changed

6 files changed

+65
-44
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,15 @@ traditionally rely on [`BluetoothGattCallback`] calls with [suspension functions
3535
```kotlin
3636
sealed class ConnectGattResult {
3737
data class Success(val gatt: Gatt) : ConnectGattResult()
38-
data class Failure(val cause: Exception) : ConnectGattResult()
38+
39+
sealed class Failure : ConnectGattResult() {
40+
41+
/** Android's `BluetoothDevice.connectGatt` returned `null` (e.g. BLE unsupported). */
42+
data class Rejected(val cause: Exception) : Failure()
43+
44+
/** Connection could not be established (e.g. device is out of range). */
45+
data class Connection(val cause: Exception) : Failure()
46+
}
3947
}
4048
```
4149

core/src/main/java/device/CoroutinesDevice.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal class CoroutinesDevice(
2727
val dispatcher = newSingleThreadContext("$DISPATCHER_NAME@$device")
2828
val callback = GattCallback(dispatcher)
2929
val bluetoothGatt = device.connectGatt(context, false, callback)
30-
?: return Failure(
30+
?: return Failure.Rejected(
3131
RemoteException("`BluetoothDevice.connectGatt` returned `null` for device $device")
3232
)
3333

@@ -44,7 +44,7 @@ internal class CoroutinesDevice(
4444
Able.warn { "connectGatt() failed for $this" }
4545
callback.close(bluetoothGatt)
4646
dispatcher.close()
47-
Failure(ConnectionFailed("Failed to connect to device $device", failure))
47+
Failure.Connection(failure)
4848
}
4949
}
5050

core/src/main/java/device/Device.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,29 @@
44

55
package com.juul.able.device
66

7+
import android.bluetooth.BluetoothDevice
78
import android.content.Context
89
import com.juul.able.gatt.Gatt
910

10-
class ConnectionFailed(message: String, cause: Throwable) : IllegalStateException(message, cause)
11-
1211
sealed class ConnectGattResult {
1312
data class Success(
1413
val gatt: Gatt
1514
) : ConnectGattResult()
1615

17-
data class Failure(
18-
val cause: Exception
19-
) : ConnectGattResult()
16+
sealed class Failure : ConnectGattResult() {
17+
18+
abstract val cause: Exception
19+
20+
/** Android's [BluetoothDevice.connectGatt] returned `null` (e.g. BLE unsupported). */
21+
data class Rejected(
22+
override val cause: Exception
23+
) : Failure()
24+
25+
/** Connection could not be established (e.g. device is out of range). */
26+
data class Connection(
27+
override val cause: Exception
28+
) : Failure()
29+
}
2030
}
2131

2232
interface Device {

core/src/test/java/device/CoroutinesDeviceTest.kt

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import android.bluetooth.BluetoothGattCallback
1111
import android.bluetooth.BluetoothProfile.STATE_CONNECTED
1212
import android.bluetooth.BluetoothProfile.STATE_CONNECTING
1313
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
14+
import android.os.RemoteException
1415
import com.juul.able.device.ConnectGattResult
1516
import com.juul.able.device.ConnectGattResult.Failure
1617
import com.juul.able.device.ConnectGattResult.Success
17-
import com.juul.able.device.ConnectionFailed
1818
import com.juul.able.device.CoroutinesDevice
1919
import com.juul.able.gatt.ConnectionLost
2020
import com.juul.able.gatt.GATT_CONN_CANCEL
@@ -34,6 +34,8 @@ import kotlinx.coroutines.runBlocking
3434
import kotlinx.coroutines.yield
3535
import org.junit.Rule
3636

37+
private const val MAC_ADDRESS = "00:11:22:33:FF:EE"
38+
3739
class CoroutinesDeviceTest {
3840

3941
@get:Rule
@@ -46,7 +48,7 @@ class CoroutinesDeviceTest {
4648
val bluetoothDevice = mockk<BluetoothDevice> {
4749
bluetoothGatt = createBluetoothGatt(this@mockk)
4850
every { connectGatt(any(), false, capture(callbackSlot)) } returns bluetoothGatt
49-
every { this@mockk.toString() } returns "00:11:22:33:FF:EE"
51+
every { this@mockk.toString() } returns MAC_ADDRESS
5052
}
5153
val device = CoroutinesDevice(bluetoothDevice)
5254

@@ -59,15 +61,11 @@ class CoroutinesDeviceTest {
5961
callback.onConnectionStateChange(bluetoothGatt, GATT_CONN_CANCEL, STATE_CONNECTED)
6062
}
6163

62-
val failure = device.connectGatt(mockk()) as Failure
64+
val failure = device.connectGatt(mockk()) as Failure.Connection
6365

64-
assertEquals<Class<out Exception>>(
65-
expected = ConnectionFailed::class.java,
66-
actual = failure.cause.javaClass
67-
)
6866
assertEquals(
6967
expected = OnConnectionStateChange(GATT_CONN_CANCEL, STATE_CONNECTED),
70-
actual = (failure.cause.cause as GattStatusFailure).event
68+
actual = (failure.cause as GattStatusFailure).event
7169
)
7270
verify(exactly = 1) { bluetoothGatt.close() }
7371
}
@@ -79,7 +77,7 @@ class CoroutinesDeviceTest {
7977
val bluetoothDevice = mockk<BluetoothDevice> {
8078
bluetoothGatt = createBluetoothGatt(this@mockk)
8179
every { connectGatt(any(), false, capture(callbackSlot)) } returns bluetoothGatt
82-
every { this@mockk.toString() } returns "00:11:22:33:FF:EE"
80+
every { this@mockk.toString() } returns MAC_ADDRESS
8381
}
8482
val device = CoroutinesDevice(bluetoothDevice)
8583

@@ -108,7 +106,7 @@ class CoroutinesDeviceTest {
108106
val bluetoothDevice = mockk<BluetoothDevice> {
109107
bluetoothGatt = createBluetoothGatt(this@mockk)
110108
every { connectGatt(any(), false, capture(callbackSlot)) } returns bluetoothGatt
111-
every { this@mockk.toString() } returns "00:11:22:33:FF:EE"
109+
every { this@mockk.toString() } returns MAC_ADDRESS
112110
}
113111
val device = CoroutinesDevice(bluetoothDevice)
114112

@@ -119,15 +117,11 @@ class CoroutinesDeviceTest {
119117
callback.onConnectionStateChange(bluetoothGatt, GATT_SUCCESS, STATE_DISCONNECTED)
120118
}
121119

122-
val failure = device.connectGatt(mockk()) as Failure
120+
val failure = device.connectGatt(mockk()) as Failure.Connection
123121

124-
assertEquals<Class<out Exception>>(
125-
expected = ConnectionFailed::class.java,
126-
actual = failure.cause.javaClass
127-
)
128122
assertEquals<Class<out Throwable>>(
129123
expected = ConnectionLost::class.java,
130-
actual = failure.cause.cause!!.javaClass
124+
actual = failure.cause.javaClass
131125
)
132126
verify(exactly = 1) { bluetoothGatt.close() }
133127
}
@@ -139,7 +133,7 @@ class CoroutinesDeviceTest {
139133
val bluetoothDevice = mockk<BluetoothDevice> {
140134
bluetoothGatt = createBluetoothGatt(this@mockk)
141135
every { connectGatt(any(), false, capture(callbackSlot)) } returns bluetoothGatt
142-
every { this@mockk.toString() } returns "00:11:22:33:FF:EE"
136+
every { this@mockk.toString() } returns MAC_ADDRESS
143137
}
144138
val device = CoroutinesDevice(bluetoothDevice)
145139

@@ -153,6 +147,21 @@ class CoroutinesDeviceTest {
153147

154148
verify(exactly = 1) { bluetoothGatt.close() }
155149
}
150+
151+
@Test
152+
fun `Null return from connectGatt results in Failure Rejected`() = runBlocking {
153+
val bluetoothDevice = mockk<BluetoothDevice> {
154+
every { connectGatt(any(), false, any()) } returns null
155+
every { this@mockk.toString() } returns MAC_ADDRESS
156+
}
157+
val device = CoroutinesDevice(bluetoothDevice)
158+
val failure = device.connectGatt(mockk()) as Failure.Rejected
159+
160+
assertEquals<Class<out Throwable>>(
161+
expected = RemoteException::class.java,
162+
actual = failure.cause.javaClass
163+
)
164+
}
156165
}
157166

158167
private fun createBluetoothGatt(

throw/src/main/java/android/BluetoothDeviceOrThrow.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ package com.juul.able.throwable.android
77
import android.bluetooth.BluetoothDevice
88
import android.bluetooth.BluetoothGatt.GATT_SUCCESS
99
import android.content.Context
10+
import android.os.RemoteException
1011
import com.juul.able.android.connectGatt
1112
import com.juul.able.device.ConnectGattResult.Failure
1213
import com.juul.able.device.ConnectGattResult.Success
13-
import com.juul.able.device.ConnectionFailed
14+
import com.juul.able.gatt.ConnectionLost
1415
import com.juul.able.gatt.Gatt
16+
import com.juul.able.gatt.GattStatusFailure
1517

1618
/**
17-
* @throws ConnectionFailed if underlying [BluetoothDevice.connectGatt] returns `null` or an error (non-[GATT_SUCCESS] status) occurs during connection process.
19+
* @throws RemoteException if underlying [BluetoothDevice.connectGatt] returns `null`.
20+
* @throws GattStatusFailure if non-[GATT_SUCCESS] status occurs during connection process.
21+
* @throws ConnectionLost if disconnect is requested during connection process.
1822
*/
1923
suspend fun BluetoothDevice.connectGattOrThrow(
2024
context: Context

throw/src/test/java/android/BluetoothDeviceTest.kt

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import android.bluetooth.BluetoothGatt.GATT_FAILURE
99
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
1010
import android.content.Context
1111
import com.juul.able.android.connectGatt
12-
import com.juul.able.device.ConnectGattResult
12+
import com.juul.able.device.ConnectGattResult.Failure
1313
import com.juul.able.device.ConnectGattResult.Success
14-
import com.juul.able.device.ConnectionFailed
1514
import com.juul.able.gatt.Gatt
1615
import com.juul.able.gatt.GattStatusFailure
1716
import com.juul.able.gatt.OnConnectionStateChange
@@ -54,25 +53,16 @@ class BluetoothDeviceTest {
5453
fun `connectGattOrThrow throws result cause on failure`() {
5554
val context = mockk<Context>()
5655
val bluetoothDevice = mockk<BluetoothDevice>()
57-
val event = OnConnectionStateChange(GATT_FAILURE, STATE_DISCONNECTED)
58-
val cause = GattStatusFailure(event)
59-
val failure = ConnectionFailed("Failed to connect to device 00:11:22:33:FF:EE", cause)
60-
mockkStatic("com.juul.able.android.BluetoothDeviceKt")
61-
try {
62-
coEvery { bluetoothDevice.connectGatt(any()) } returns ConnectGattResult.Failure(failure)
56+
val cause = GattStatusFailure(OnConnectionStateChange(GATT_FAILURE, STATE_DISCONNECTED))
57+
58+
mockkStatic("com.juul.able.android.BluetoothDeviceKt") {
59+
coEvery { bluetoothDevice.connectGatt(any()) } returns Failure.Connection(cause)
6360

64-
val capturedCause = assertFailsWith<ConnectionFailed> {
61+
assertFailsWith<GattStatusFailure> {
6562
runBlocking {
6663
bluetoothDevice.connectGattOrThrow(context)
6764
}
68-
}.cause!!
69-
70-
assertEquals(
71-
expected = cause,
72-
actual = capturedCause
73-
)
74-
} finally {
75-
unmockkStatic("com.juul.able.android.BluetoothDeviceKt")
65+
}
7666
}
7767
}
7868
}

0 commit comments

Comments
 (0)