@ledgerhq/device-transport-kit-react-native-hid 0.0.0-web-ble-29-08---20250829104351 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +1 -1
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +29 -14
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +33 -34
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +100 -31
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +142 -15
- package/lib/cjs/api/bridge/mapper.js +1 -1
- package/lib/cjs/api/bridge/mapper.js.map +2 -2
- package/lib/cjs/api/bridge/mapper.test.js +1 -1
- package/lib/cjs/api/bridge/mapper.test.js.map +2 -2
- package/lib/cjs/api/transport/Errors.js +1 -1
- package/lib/cjs/api/transport/Errors.js.map +3 -3
- package/lib/cjs/api/transport/RNHidTransport.js +1 -1
- package/lib/cjs/api/transport/RNHidTransport.js.map +2 -2
- package/lib/cjs/api/transport/RNHidTransport.test.js +1 -1
- package/lib/cjs/api/transport/RNHidTransport.test.js.map +2 -2
- package/lib/cjs/package.json +1 -1
- package/lib/esm/api/bridge/mapper.js +1 -1
- package/lib/esm/api/bridge/mapper.js.map +3 -3
- package/lib/esm/api/bridge/mapper.test.js +1 -1
- package/lib/esm/api/bridge/mapper.test.js.map +3 -3
- package/lib/esm/api/transport/Errors.js +1 -1
- package/lib/esm/api/transport/Errors.js.map +3 -3
- package/lib/esm/api/transport/RNHidTransport.js +1 -1
- package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
- package/lib/esm/api/transport/RNHidTransport.test.js +1 -1
- package/lib/esm/api/transport/RNHidTransport.test.js.map +3 -3
- package/lib/esm/package.json +1 -1
- package/lib/types/api/bridge/mapper.d.ts.map +1 -1
- package/lib/types/api/transport/Errors.d.ts +1 -1
- package/lib/types/api/transport/Errors.d.ts.map +1 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -49,7 +49,7 @@ class TransportHidModule(
|
|
|
49
49
|
private val loggerService: LoggerService =
|
|
50
50
|
LoggerService { info ->
|
|
51
51
|
Timber.tag("LDMKTransportHIDModule " + info.tag).d(info.message)
|
|
52
|
-
sendEvent(reactContext, BridgeEvents.TransportLog(info))
|
|
52
|
+
// sendEvent(reactContext, BridgeEvents.TransportLog(info))
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
private val transport: AndroidUsbTransport? by lazy {
|
|
@@ -123,9 +123,10 @@ internal class DefaultAndroidUsbTransport(
|
|
|
123
123
|
"Device disconnected (sessionId=${deviceConnection.sessionId})"
|
|
124
124
|
)
|
|
125
125
|
)
|
|
126
|
-
deviceConnection.handleDeviceDisconnected()
|
|
127
126
|
usbConnections.remove(key)
|
|
128
127
|
usbConnectionsPendingReconnection.add(deviceConnection)
|
|
128
|
+
deviceConnection.handleDeviceDisconnected()
|
|
129
|
+
(deviceConnection.getApduSender() as AndroidUsbApduSender).release()
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
@@ -186,19 +187,30 @@ internal class DefaultAndroidUsbTransport(
|
|
|
186
187
|
"Reconnecting device (sessionId=${deviceConnection.sessionId})"
|
|
187
188
|
)
|
|
188
189
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
190
|
+
|
|
191
|
+
val apduSender = AndroidUsbApduSender(
|
|
192
|
+
dependencies = AndroidUsbApduSender.Dependencies(
|
|
193
|
+
usbDevice = usbDevice,
|
|
194
|
+
ledgerUsbDevice = state.ledgerUsbDevice,
|
|
195
|
+
),
|
|
196
|
+
usbManager = usbManager,
|
|
197
|
+
ioDispatcher = Dispatchers.IO,
|
|
198
|
+
framerService = FramerService(loggerService),
|
|
199
|
+
request = UsbRequest(),
|
|
200
|
+
loggerService = loggerService
|
|
201
201
|
)
|
|
202
|
+
|
|
203
|
+
if (!usbConnectionsPendingReconnection.contains(deviceConnection)) {
|
|
204
|
+
/**
|
|
205
|
+
* We check this because maybe by the time we get here,
|
|
206
|
+
* the reconnection has timed out and the session has been terminated.
|
|
207
|
+
* Easy to reproduce for instance if the permission request requires
|
|
208
|
+
* a user interaction.
|
|
209
|
+
*/
|
|
210
|
+
apduSender.release()
|
|
211
|
+
return@launch
|
|
212
|
+
}
|
|
213
|
+
deviceConnection.handleDeviceConnected(apduSender)
|
|
202
214
|
usbConnectionsPendingReconnection.remove(deviceConnection)
|
|
203
215
|
usbConnections[deviceConnection.sessionId] = deviceConnection
|
|
204
216
|
}
|
|
@@ -304,9 +316,10 @@ internal class DefaultAndroidUsbTransport(
|
|
|
304
316
|
val deviceConnection = DeviceConnection(
|
|
305
317
|
sessionId = sessionId,
|
|
306
318
|
deviceApduSender = apduSender,
|
|
307
|
-
isFatalSendApduFailure = { false },
|
|
319
|
+
isFatalSendApduFailure = { false },
|
|
308
320
|
reconnectionTimeoutDuration = 5.seconds,
|
|
309
321
|
onTerminated = {
|
|
322
|
+
(it.getApduSender() as AndroidUsbApduSender).release()
|
|
310
323
|
usbConnections.remove(sessionId)
|
|
311
324
|
usbConnectionsPendingReconnection.remove(it)
|
|
312
325
|
eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
|
|
@@ -333,7 +346,9 @@ internal class DefaultAndroidUsbTransport(
|
|
|
333
346
|
}
|
|
334
347
|
|
|
335
348
|
override suspend fun disconnect(deviceId: String) {
|
|
349
|
+
// The DeviceConnection is either in usbConnections or usbConnectionsPendingReconnection
|
|
336
350
|
usbConnections[deviceId]?.requestCloseConnection()
|
|
351
|
+
usbConnectionsPendingReconnection.find { it.sessionId == deviceId }?.requestCloseConnection()
|
|
337
352
|
}
|
|
338
353
|
|
|
339
354
|
private fun generateSessionId(usbDevice: UsbDevice): String = "usb_${usbDevice.deviceId}"
|
|
@@ -22,7 +22,6 @@ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogIn
|
|
|
22
22
|
import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
|
|
23
23
|
import com.ledger.devicesdk.shared.internal.transport.framer.to2BytesArray
|
|
24
24
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
25
|
-
import kotlinx.coroutines.cancel
|
|
26
25
|
import kotlinx.coroutines.delay
|
|
27
26
|
import kotlinx.coroutines.launch
|
|
28
27
|
import kotlinx.coroutines.withContext
|
|
@@ -36,7 +35,7 @@ private const val DEFAULT_USB_INTERFACE = 0
|
|
|
36
35
|
|
|
37
36
|
internal class AndroidUsbApduSender(
|
|
38
37
|
override val dependencies: Dependencies,
|
|
39
|
-
|
|
38
|
+
usbManager: UsbManager,
|
|
40
39
|
private val framerService: FramerService,
|
|
41
40
|
private val request: UsbRequest,
|
|
42
41
|
private val ioDispatcher: CoroutineDispatcher,
|
|
@@ -47,49 +46,48 @@ internal class AndroidUsbApduSender(
|
|
|
47
46
|
val ledgerUsbDevice: LedgerUsbDevice,
|
|
48
47
|
)
|
|
49
48
|
|
|
49
|
+
private val usbDevice = dependencies.usbDevice
|
|
50
|
+
private val usbInterface = usbDevice.getInterface(DEFAULT_USB_INTERFACE)
|
|
51
|
+
private val androidToUsbEndpoint =
|
|
52
|
+
usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_OUT }
|
|
53
|
+
private val usbToAndroidEndpoint =
|
|
54
|
+
usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_IN }
|
|
55
|
+
private val usbConnection = usbManager.openDevice(usbDevice)
|
|
56
|
+
.apply { claimInterface(usbInterface, true) }
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
fun release() {
|
|
60
|
+
usbConnection.releaseInterface(usbInterface)
|
|
61
|
+
usbConnection.close()
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
override suspend fun send(apdu: ByteArray, abortTimeoutDuration: Duration): SendApduResult =
|
|
51
65
|
try {
|
|
52
|
-
val usbDevice = dependencies.usbDevice
|
|
53
66
|
withContext(context = ioDispatcher) {
|
|
54
|
-
val usbInterface = usbDevice.getInterface(DEFAULT_USB_INTERFACE)
|
|
55
|
-
val androidToUsbEndpoint =
|
|
56
|
-
usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_OUT }
|
|
57
|
-
val usbToAndroidEndpoint =
|
|
58
|
-
usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_IN }
|
|
59
|
-
val usbConnection = usbManager.openDevice(usbDevice)
|
|
60
|
-
.apply { claimInterface(usbInterface, true) }
|
|
61
67
|
|
|
62
68
|
val timeoutJob = launch {
|
|
63
69
|
delay(abortTimeoutDuration)
|
|
64
|
-
usbConnection.releaseInterface(usbInterface)
|
|
65
|
-
usbConnection.close()
|
|
66
70
|
throw SendApduTimeoutException
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
transmitApdu(
|
|
74
|
+
usbConnection = usbConnection,
|
|
75
|
+
androidToUsbEndpoint = androidToUsbEndpoint,
|
|
76
|
+
rawApdu = apdu,
|
|
77
|
+
)
|
|
70
78
|
|
|
71
|
-
|
|
79
|
+
val apduResponse =
|
|
80
|
+
receiveApdu(
|
|
72
81
|
usbConnection = usbConnection,
|
|
73
|
-
|
|
74
|
-
rawApdu = apdu,
|
|
82
|
+
usbToAndroidEndpoint = usbToAndroidEndpoint,
|
|
75
83
|
)
|
|
84
|
+
timeoutJob.cancel()
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
usbConnection = usbConnection,
|
|
80
|
-
usbToAndroidEndpoint = usbToAndroidEndpoint,
|
|
81
|
-
)
|
|
82
|
-
timeoutJob.cancel()
|
|
83
|
-
|
|
84
|
-
if (apduResponse.isEmpty()) {
|
|
85
|
-
return@withContext SendApduResult.Failure(reason = SendApduFailureReason.EmptyResponse)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return@withContext SendApduResult.Success(apdu = apduResponse)
|
|
89
|
-
} finally {
|
|
90
|
-
usbConnection.releaseInterface(usbInterface)
|
|
91
|
-
usbConnection.close()
|
|
86
|
+
if (apduResponse.isEmpty()) {
|
|
87
|
+
return@withContext SendApduResult.Failure(reason = SendApduFailureReason.EmptyResponse)
|
|
92
88
|
}
|
|
89
|
+
|
|
90
|
+
return@withContext SendApduResult.Success(apdu = apduResponse)
|
|
93
91
|
}
|
|
94
92
|
} catch (e: SendApduTimeoutException) {
|
|
95
93
|
loggerService.log(
|
|
@@ -132,8 +130,8 @@ internal class AndroidUsbApduSender(
|
|
|
132
130
|
private fun receiveApdu(
|
|
133
131
|
usbConnection: UsbDeviceConnection,
|
|
134
132
|
usbToAndroidEndpoint: UsbEndpoint,
|
|
135
|
-
): ByteArray
|
|
136
|
-
if (!request.initialize(usbConnection, usbToAndroidEndpoint)) {
|
|
133
|
+
): ByteArray {
|
|
134
|
+
return if (!request.initialize(usbConnection, usbToAndroidEndpoint)) {
|
|
137
135
|
request.close()
|
|
138
136
|
byteArrayOf()
|
|
139
137
|
} else {
|
|
@@ -154,6 +152,7 @@ internal class AndroidUsbApduSender(
|
|
|
154
152
|
}
|
|
155
153
|
framerService.deserialize(mtu = USB_MTU, frames)
|
|
156
154
|
}
|
|
155
|
+
}
|
|
157
156
|
|
|
158
157
|
private fun UsbInterface.firstEndpointOrThrow(predicate: (Int) -> Boolean): UsbEndpoint {
|
|
159
158
|
for (endp in 0..this.endpointCount) {
|
|
@@ -169,7 +168,7 @@ internal class AndroidUsbApduSender(
|
|
|
169
168
|
private fun generateChannelId(): ByteArray =
|
|
170
169
|
Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
|
|
171
170
|
|
|
172
|
-
private data object SendApduTimeoutException: Exception() {
|
|
171
|
+
private data object SendApduTimeoutException : Exception() {
|
|
173
172
|
private fun readResolve(): Any = SendApduTimeoutException
|
|
174
173
|
}
|
|
175
174
|
}
|
|
@@ -2,9 +2,9 @@ package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnecti
|
|
|
2
2
|
|
|
3
3
|
import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
|
|
4
4
|
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
5
|
+
import com.ledger.devicesdk.shared.api.utils.fromHexStringToBytesOrThrow
|
|
5
6
|
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
6
7
|
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
|
|
7
|
-
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
|
|
8
8
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
9
9
|
import kotlinx.coroutines.CoroutineScope
|
|
10
10
|
import kotlinx.coroutines.Job
|
|
@@ -26,12 +26,31 @@ internal class DeviceConnectionStateMachine(
|
|
|
26
26
|
fun getState() = state
|
|
27
27
|
|
|
28
28
|
private fun pushState(newState: State) {
|
|
29
|
+
|
|
30
|
+
val currentState = state
|
|
31
|
+
|
|
32
|
+
/* STATE EXIT EFFECTS */
|
|
33
|
+
if (newState != currentState) {
|
|
34
|
+
when (currentState) {
|
|
35
|
+
is State.WaitingForDisconnection -> {
|
|
36
|
+
currentState.requestContent.resultCallback(currentState.result)
|
|
37
|
+
}
|
|
38
|
+
else -> {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* STATE ENTRY EFFECTS */
|
|
29
43
|
when (newState) {
|
|
30
44
|
is State.Connected -> {}
|
|
31
45
|
is State.SendingApdu -> {
|
|
32
46
|
sendApduFn(newState.requestContent.apdu, newState.requestContent.abortTimeoutDuration)
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
is State.WaitingForDisconnection -> {
|
|
50
|
+
// TODO: send getAppAndVersion
|
|
51
|
+
sendApduFn ("b0010000".fromHexStringToBytesOrThrow(), Duration.INFINITE)
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
is State.WaitingForReconnection -> {
|
|
36
55
|
startReconnectionTimeout()
|
|
37
56
|
}
|
|
@@ -42,11 +61,16 @@ internal class DeviceConnectionStateMachine(
|
|
|
42
61
|
}
|
|
43
62
|
}
|
|
44
63
|
this.state = newState
|
|
64
|
+
loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", "-> New state: $newState"))
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
private fun handleEvent(event: Event) {
|
|
48
|
-
val
|
|
49
|
-
|
|
68
|
+
val logMessage = """
|
|
69
|
+
-> Event received: $event
|
|
70
|
+
In state: $state
|
|
71
|
+
""".trimIndent()
|
|
72
|
+
loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
|
|
73
|
+
when (val currentState = state) {
|
|
50
74
|
is State.Connected -> {
|
|
51
75
|
when (event) {
|
|
52
76
|
is Event.SendApduRequested -> {
|
|
@@ -71,31 +95,27 @@ internal class DeviceConnectionStateMachine(
|
|
|
71
95
|
when (event) {
|
|
72
96
|
is Event.ApduResultReceived -> {
|
|
73
97
|
when (event.result) {
|
|
74
|
-
is SendApduResult.
|
|
75
|
-
if
|
|
76
|
-
|
|
98
|
+
is SendApduResult.Success -> {
|
|
99
|
+
// check if last 2 bytes of APDU are [0x90,OxO0]
|
|
100
|
+
val apdu = event.result.apdu
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if (isSuccessApdu(apdu) && currentState.requestContent.triggersDisconnection) {
|
|
104
|
+
pushState(State.WaitingForDisconnection(requestContent = currentState.requestContent, result = event.result))
|
|
77
105
|
} else {
|
|
78
106
|
pushState(State.Connected)
|
|
107
|
+
currentState.requestContent.resultCallback(event.result)
|
|
79
108
|
}
|
|
80
109
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
val apdu = event.result.apdu
|
|
85
|
-
val apduSize = apdu.size
|
|
86
|
-
val isSuccessApdu =
|
|
87
|
-
apdu.size >= 2 &&
|
|
88
|
-
apdu[apduSize - 2] == 0x90.toByte() &&
|
|
89
|
-
apdu[apduSize - 1] == 0x00.toByte()
|
|
90
|
-
|
|
91
|
-
if (isSuccessApdu && currentState.requestContent.triggersDisconnection) {
|
|
92
|
-
pushState(State.WaitingForReconnection)
|
|
110
|
+
is SendApduResult.Failure -> {
|
|
111
|
+
if (isFatalSendApduFailure(event.result)) {
|
|
112
|
+
pushState(State.Terminated)
|
|
93
113
|
} else {
|
|
94
114
|
pushState(State.Connected)
|
|
95
115
|
}
|
|
116
|
+
currentState.requestContent.resultCallback(event.result)
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
|
-
currentState.requestContent.resultCallback(event.result)
|
|
99
119
|
}
|
|
100
120
|
|
|
101
121
|
is Event.CloseConnectionRequested -> {
|
|
@@ -130,6 +150,42 @@ internal class DeviceConnectionStateMachine(
|
|
|
130
150
|
}
|
|
131
151
|
}
|
|
132
152
|
|
|
153
|
+
is State.WaitingForDisconnection -> {
|
|
154
|
+
when (event) {
|
|
155
|
+
is Event.ApduResultReceived -> {
|
|
156
|
+
when (event.result) {
|
|
157
|
+
is SendApduResult.Success -> {
|
|
158
|
+
val apdu = event.result.apdu
|
|
159
|
+
if (isSendApduBusyError(apdu)) {
|
|
160
|
+
pushState(state) // Loop on same state, will trigger a new send of GetAppAndVersion (entry effect)
|
|
161
|
+
} else {
|
|
162
|
+
pushState(State.Connected)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
is SendApduResult.Failure -> {
|
|
166
|
+
pushState(State.WaitingForReconnection)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
is Event.SendApduRequested -> {
|
|
171
|
+
event.requestContent.resultCallback(
|
|
172
|
+
SendApduResult.Failure(
|
|
173
|
+
SendApduFailureReason.DeviceBusy
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
is Event.DeviceDisconnected -> {
|
|
178
|
+
pushState(State.WaitingForReconnection)
|
|
179
|
+
}
|
|
180
|
+
is Event.CloseConnectionRequested -> {
|
|
181
|
+
pushState(State.Terminated)
|
|
182
|
+
}
|
|
183
|
+
else -> {
|
|
184
|
+
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
133
189
|
is State.WaitingForReconnection -> {
|
|
134
190
|
when (event) {
|
|
135
191
|
is Event.DeviceConnected -> {
|
|
@@ -234,13 +290,24 @@ internal class DeviceConnectionStateMachine(
|
|
|
234
290
|
onError(Exception("Unhandled event: $event in state: $currentState"))
|
|
235
291
|
}
|
|
236
292
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private fun isSuccessApdu(apdu: ByteArray): Boolean {
|
|
296
|
+
val apduSize = apdu.size
|
|
297
|
+
return apduSize >= 2 &&
|
|
298
|
+
apdu[apduSize - 2] == 0x90.toByte() &&
|
|
299
|
+
apdu[apduSize - 1] == 0x00.toByte()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private fun isSendApduBusyError(apdu: ByteArray): Boolean {
|
|
303
|
+
val apduSize = apdu.size
|
|
304
|
+
return apduSize >= 2 &&
|
|
305
|
+
apdu[apduSize - 2] == 0x66.toByte() &&
|
|
306
|
+
apdu[apduSize - 1] == 0x01.toByte()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private fun sendGetAppAndVersionApdu() {
|
|
310
|
+
sendApduFn("b0010000".fromHexStringToBytesOrThrow(), Duration.INFINITE)
|
|
244
311
|
}
|
|
245
312
|
|
|
246
313
|
private var timeoutJob: Job? = null
|
|
@@ -257,23 +324,23 @@ internal class DeviceConnectionStateMachine(
|
|
|
257
324
|
timeoutJob = null
|
|
258
325
|
}
|
|
259
326
|
|
|
260
|
-
|
|
327
|
+
fun requestSendApdu(requestContent: SendApduRequestContent) {
|
|
261
328
|
handleEvent(Event.SendApduRequested(requestContent))
|
|
262
329
|
}
|
|
263
330
|
|
|
264
|
-
|
|
331
|
+
fun requestCloseConnection() {
|
|
265
332
|
handleEvent(Event.CloseConnectionRequested)
|
|
266
333
|
}
|
|
267
334
|
|
|
268
|
-
|
|
335
|
+
fun handleApduResult(result: SendApduResult) {
|
|
269
336
|
handleEvent(Event.ApduResultReceived(result))
|
|
270
337
|
}
|
|
271
338
|
|
|
272
|
-
|
|
339
|
+
fun handleDeviceConnected() {
|
|
273
340
|
handleEvent(Event.DeviceConnected)
|
|
274
341
|
}
|
|
275
342
|
|
|
276
|
-
|
|
343
|
+
fun handleDeviceDisconnected() {
|
|
277
344
|
handleEvent(Event.DeviceDisconnected)
|
|
278
345
|
}
|
|
279
346
|
|
|
@@ -307,6 +374,8 @@ internal class DeviceConnectionStateMachine(
|
|
|
307
374
|
|
|
308
375
|
data object WaitingForReconnection : State()
|
|
309
376
|
|
|
377
|
+
data class WaitingForDisconnection(val requestContent: SendApduRequestContent, val result: SendApduResult): State()
|
|
378
|
+
|
|
310
379
|
data class WaitingForReconnectionWithQueuedApdu(val requestContent: SendApduRequestContent) :
|
|
311
380
|
State()
|
|
312
381
|
|
|
@@ -2,6 +2,7 @@ package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnecti
|
|
|
2
2
|
|
|
3
3
|
import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
|
|
4
4
|
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
5
|
+
import com.ledger.devicesdk.shared.api.utils.fromHexStringToBytesOrThrow
|
|
5
6
|
import com.ledger.devicesdk.shared.internal.service.logger.LogInfo
|
|
6
7
|
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
7
8
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
@@ -12,6 +13,7 @@ import kotlin.time.Duration
|
|
|
12
13
|
import kotlin.time.Duration.Companion.seconds
|
|
13
14
|
import kotlin.test.Test
|
|
14
15
|
import org.junit.Assert.*
|
|
16
|
+
import kotlin.test.assertIs
|
|
15
17
|
import kotlin.time.Duration.Companion.milliseconds
|
|
16
18
|
|
|
17
19
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
@@ -88,7 +90,7 @@ class DeviceConnectionStateMachineTest {
|
|
|
88
90
|
stateMachine.handleApduResult(mockedFailureResult)
|
|
89
91
|
|
|
90
92
|
// Then
|
|
91
|
-
assertEquals(
|
|
93
|
+
assertEquals(mockedFailureResult, sendApduResult)
|
|
92
94
|
assertTrue(terminated)
|
|
93
95
|
assertNull(error)
|
|
94
96
|
}
|
|
@@ -134,14 +136,13 @@ class DeviceConnectionStateMachineTest {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
@Test
|
|
137
|
-
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent THEN the machine moves to WaitingForReconnection and recovers when device reconnects`() =
|
|
139
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent and triggers a disconnection THEN the machine moves to WaitingForReconnection and recovers when device reconnects`() =
|
|
138
140
|
runTest {
|
|
139
141
|
var sendApduCalled: ByteArray? = null
|
|
140
142
|
var sendApduResult: SendApduResult? = null
|
|
141
143
|
var terminated = false
|
|
142
144
|
var error: Throwable? = null
|
|
143
145
|
|
|
144
|
-
val dispatcher = StandardTestDispatcher(testScheduler)
|
|
145
146
|
val stateMachine = DeviceConnectionStateMachine(
|
|
146
147
|
sendApduFn = { apdu, _ -> sendApduCalled = apdu },
|
|
147
148
|
onTerminated = { terminated = true },
|
|
@@ -167,6 +168,10 @@ class DeviceConnectionStateMachineTest {
|
|
|
167
168
|
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
168
169
|
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
169
170
|
|
|
171
|
+
assertNull(sendApduResult)
|
|
172
|
+
// Simulate a disconnection
|
|
173
|
+
stateMachine.handleDeviceDisconnected()
|
|
174
|
+
|
|
170
175
|
// Result should have been returned
|
|
171
176
|
assertEquals(mockedSuccessApduResult, sendApduResult)
|
|
172
177
|
|
|
@@ -203,7 +208,118 @@ class DeviceConnectionStateMachineTest {
|
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
@Test
|
|
206
|
-
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent
|
|
211
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent and device does not disconnect THEN the machine moves to Connected`() =
|
|
212
|
+
runTest {
|
|
213
|
+
var sendApduCalled: ByteArray? = null
|
|
214
|
+
var sendApduResult: SendApduResult? = null
|
|
215
|
+
var terminated = false
|
|
216
|
+
var error: Throwable? = null
|
|
217
|
+
|
|
218
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
219
|
+
sendApduFn = { apdu, _ -> sendApduCalled = apdu },
|
|
220
|
+
onTerminated = { terminated = true },
|
|
221
|
+
isFatalSendApduFailure = { false },
|
|
222
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
223
|
+
onError = { error = it },
|
|
224
|
+
loggerService = FakeLoggerService(),
|
|
225
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// Request sending an APDU (with triggersDisconnection = true)
|
|
229
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
230
|
+
apdu = mockedRequestApduA,
|
|
231
|
+
triggersDisconnection = true,
|
|
232
|
+
resultCallback = { result -> sendApduResult = result },
|
|
233
|
+
abortTimeoutDuration = Duration.INFINITE
|
|
234
|
+
))
|
|
235
|
+
|
|
236
|
+
// Send APDU should have been called
|
|
237
|
+
assertArrayEquals(mockedRequestApduA, sendApduCalled)
|
|
238
|
+
|
|
239
|
+
// Simulate a successful response
|
|
240
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
241
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
242
|
+
|
|
243
|
+
assertNull(sendApduResult)
|
|
244
|
+
|
|
245
|
+
// Simulate Response from GetAppAndVersion
|
|
246
|
+
val mockedSuccessApduResultGetAppAndVersion = SendApduResult.Success(mockedGetAppAndVersionSuccessfulResponse)
|
|
247
|
+
stateMachine.handleApduResult(mockedSuccessApduResultGetAppAndVersion)
|
|
248
|
+
|
|
249
|
+
// Should be in Connected state
|
|
250
|
+
assertEquals(
|
|
251
|
+
DeviceConnectionStateMachine.State.Connected,
|
|
252
|
+
stateMachine.getState()
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
// Response should have been returned
|
|
256
|
+
assertEquals(mockedSuccessApduResult, sendApduResult)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Test
|
|
260
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent and device does not disconnect but is first busy THEN the machine retries and moves to Connected`() =
|
|
261
|
+
runTest {
|
|
262
|
+
var sendApduCalled: ByteArray? = null
|
|
263
|
+
var sendApduResult: SendApduResult? = null
|
|
264
|
+
var terminated = false
|
|
265
|
+
var error: Throwable? = null
|
|
266
|
+
|
|
267
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
268
|
+
sendApduFn = { apdu, _ -> sendApduCalled = apdu },
|
|
269
|
+
onTerminated = { terminated = true },
|
|
270
|
+
isFatalSendApduFailure = { false },
|
|
271
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
272
|
+
onError = { error = it },
|
|
273
|
+
loggerService = FakeLoggerService(),
|
|
274
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Request sending an APDU (with triggersDisconnection = true)
|
|
278
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
279
|
+
apdu = mockedRequestApduA,
|
|
280
|
+
triggersDisconnection = true,
|
|
281
|
+
resultCallback = { result -> sendApduResult = result },
|
|
282
|
+
abortTimeoutDuration = Duration.INFINITE
|
|
283
|
+
))
|
|
284
|
+
|
|
285
|
+
// Send APDU should have been called
|
|
286
|
+
assertArrayEquals(mockedRequestApduA, sendApduCalled)
|
|
287
|
+
|
|
288
|
+
// Simulate a successful response
|
|
289
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
290
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
291
|
+
|
|
292
|
+
assertNull(sendApduResult)
|
|
293
|
+
|
|
294
|
+
assertIs<DeviceConnectionStateMachine.State.WaitingForDisconnection>(
|
|
295
|
+
stateMachine.getState()
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// Simulate Busy Response from GetAppAndVersion
|
|
299
|
+
val mockedBusyApduResultGetAppAndVersion = SendApduResult.Success(mockedGetAppAndVersionBusyResponse)
|
|
300
|
+
stateMachine.handleApduResult(mockedBusyApduResultGetAppAndVersion)
|
|
301
|
+
|
|
302
|
+
assertIs<DeviceConnectionStateMachine.State.WaitingForDisconnection>(
|
|
303
|
+
stateMachine.getState()
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// Simulate Successful Response from GetAppAndVersion
|
|
307
|
+
val mockedSuccessApduResultGetAppAndVersion = SendApduResult.Success(mockedGetAppAndVersionSuccessfulResponse)
|
|
308
|
+
stateMachine.handleApduResult(mockedSuccessApduResultGetAppAndVersion)
|
|
309
|
+
|
|
310
|
+
// Should be in Connected state
|
|
311
|
+
assertEquals(
|
|
312
|
+
DeviceConnectionStateMachine.State.Connected,
|
|
313
|
+
stateMachine.getState()
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
// Response should have been returned
|
|
317
|
+
assertEquals(mockedSuccessApduResult, sendApduResult)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@Test
|
|
322
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent and triggers a disconnection THEN the machine moves to WaitingForReconnection and the next APDU is queued until reconnection`() =
|
|
207
323
|
runTest {
|
|
208
324
|
val sendApduCalled: MutableList<ByteArray> = mutableListOf()
|
|
209
325
|
var terminated = false
|
|
@@ -231,10 +347,19 @@ class DeviceConnectionStateMachineTest {
|
|
|
231
347
|
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
232
348
|
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
233
349
|
|
|
234
|
-
// sendApduFn should have been called
|
|
235
|
-
assertEquals(
|
|
350
|
+
// sendApduFn should have been called twice (for the mockedRequestApduA and an extra call for the getAppAndVersion APDU)
|
|
351
|
+
assertEquals(2, sendApduCalled.size)
|
|
352
|
+
|
|
353
|
+
// Should be in WaitingForDisconnection state
|
|
354
|
+
assertIs<DeviceConnectionStateMachine.State.WaitingForDisconnection>(
|
|
355
|
+
stateMachine.getState()
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
// Simulate disconnection
|
|
359
|
+
stateMachine.handleDeviceDisconnected()
|
|
360
|
+
|
|
361
|
+
// Should be in WaitingForReconnection state
|
|
236
362
|
|
|
237
|
-
// Should be in waiting state
|
|
238
363
|
assertEquals(
|
|
239
364
|
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
240
365
|
stateMachine.getState()
|
|
@@ -256,7 +381,7 @@ class DeviceConnectionStateMachineTest {
|
|
|
256
381
|
)
|
|
257
382
|
|
|
258
383
|
// sendApduFn should not have been called one more time
|
|
259
|
-
assertEquals(
|
|
384
|
+
assertEquals(2, sendApduCalled.size)
|
|
260
385
|
|
|
261
386
|
// Simulate reconnection
|
|
262
387
|
stateMachine.handleDeviceConnected()
|
|
@@ -267,9 +392,9 @@ class DeviceConnectionStateMachineTest {
|
|
|
267
392
|
stateMachine.getState()::class
|
|
268
393
|
)
|
|
269
394
|
|
|
270
|
-
// Send APDU should have been called a
|
|
271
|
-
assertEquals(
|
|
272
|
-
assertArrayEquals(mockedRequestApduB, sendApduCalled[
|
|
395
|
+
// Send APDU should have been called a 3rd time, and the result should have been returned
|
|
396
|
+
assertEquals(3, sendApduCalled.size)
|
|
397
|
+
assertArrayEquals(mockedRequestApduB, sendApduCalled[2])
|
|
273
398
|
|
|
274
399
|
// Simulate a successful response
|
|
275
400
|
val mockedSuccessApduResultB = SendApduResult.Success(mockedResultApduSuccessB)
|
|
@@ -721,9 +846,11 @@ class DeviceConnectionStateMachineTest {
|
|
|
721
846
|
}
|
|
722
847
|
|
|
723
848
|
val reconnectionTimeout: Duration = 5.seconds
|
|
724
|
-
val mockedRequestApduA: ByteArray =
|
|
725
|
-
val mockedRequestApduB: ByteArray =
|
|
726
|
-
val
|
|
727
|
-
val
|
|
849
|
+
val mockedRequestApduA: ByteArray = "1234".fromHexStringToBytesOrThrow()
|
|
850
|
+
val mockedRequestApduB: ByteArray = "5678".fromHexStringToBytesOrThrow()
|
|
851
|
+
val mockedGetAppAndVersionSuccessfulResponse: ByteArray = "12349000".fromHexStringToBytesOrThrow()
|
|
852
|
+
val mockedGetAppAndVersionBusyResponse: ByteArray = "12346601".fromHexStringToBytesOrThrow()
|
|
853
|
+
val mockedResultApduSuccessA: ByteArray = "56789000".fromHexStringToBytesOrThrow()
|
|
854
|
+
val mockedResultApduSuccessB: ByteArray = "abcd9000".fromHexStringToBytesOrThrow()
|
|
728
855
|
}
|
|
729
856
|
}
|