@ledgerhq/device-transport-kit-react-native-hid 0.0.0-hid-candidate-4-20250528125313 → 0.0.0-intent-queue-2-20251112131539
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 +45 -14
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +34 -11
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +3 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +93 -21
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +16 -0
- 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/index.js +1 -1
- package/lib/cjs/index.js.map +3 -3
- package/lib/cjs/package.json +2 -2
- 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/index.js +1 -1
- package/lib/esm/index.js.map +3 -3
- package/lib/esm/package.json +2 -2
- package/lib/types/api/bridge/mapper.d.ts.map +1 -1
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +9 -9
|
@@ -17,11 +17,14 @@ import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.ACTION_U
|
|
|
17
17
|
import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbAttachedReceiverController
|
|
18
18
|
import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbDetachedReceiverController
|
|
19
19
|
import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbPermissionReceiver
|
|
20
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
|
|
21
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
20
22
|
import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
|
|
21
23
|
import com.ledger.devicesdk.shared.internal.connection.InternalConnectedDevice
|
|
22
24
|
import com.ledger.devicesdk.shared.internal.connection.InternalConnectionResult
|
|
23
25
|
import com.ledger.devicesdk.shared.internal.event.SdkEventDispatcher
|
|
24
26
|
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
27
|
+
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
|
|
25
28
|
import com.ledger.devicesdk.shared.internal.transport.TransportEvent
|
|
26
29
|
import kotlinx.coroutines.CoroutineScope
|
|
27
30
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -34,6 +37,8 @@ import kotlin.random.Random
|
|
|
34
37
|
import kotlin.time.Duration
|
|
35
38
|
import kotlin.time.Duration.Companion.milliseconds
|
|
36
39
|
|
|
40
|
+
private val TAG = "TransportHidModule"
|
|
41
|
+
|
|
37
42
|
class TransportHidModule(
|
|
38
43
|
private val reactContext: ReactApplicationContext,
|
|
39
44
|
private val coroutineScope: CoroutineScope
|
|
@@ -133,12 +138,18 @@ class TransportHidModule(
|
|
|
133
138
|
transport?.stopScan()
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
private var
|
|
141
|
+
private var activeScanCount = 0
|
|
137
142
|
|
|
138
143
|
@ReactMethod
|
|
139
144
|
fun startScan(promise: Promise) {
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
loggerService.log(
|
|
146
|
+
buildSimpleDebugLogInfo(TAG, "[startScan] called")
|
|
147
|
+
)
|
|
148
|
+
activeScanCount += 1
|
|
149
|
+
if (activeScanCount > 1) {
|
|
150
|
+
loggerService.log(
|
|
151
|
+
buildSimpleDebugLogInfo(TAG, "[startScan] already scanning")
|
|
152
|
+
)
|
|
142
153
|
promise.resolve(null)
|
|
143
154
|
return
|
|
144
155
|
}
|
|
@@ -156,16 +167,34 @@ class TransportHidModule(
|
|
|
156
167
|
|
|
157
168
|
@ReactMethod
|
|
158
169
|
fun stopScan(promise: Promise) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
loggerService.log(
|
|
171
|
+
buildSimpleDebugLogInfo(TAG, "[stopScan] called, activeScanCount=$activeScanCount")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
when(activeScanCount) {
|
|
175
|
+
0 -> {
|
|
176
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] no active scan"))
|
|
177
|
+
promise.resolve(null)
|
|
178
|
+
}
|
|
179
|
+
1 -> {
|
|
180
|
+
try {
|
|
181
|
+
transport!!.stopScan()
|
|
182
|
+
promise.resolve(null)
|
|
183
|
+
} catch (e: Exception) {
|
|
184
|
+
promise.reject(e);
|
|
185
|
+
}
|
|
186
|
+
activeScanCount = 0
|
|
187
|
+
}
|
|
188
|
+
else -> {
|
|
189
|
+
loggerService.log(
|
|
190
|
+
buildSimpleDebugLogInfo(
|
|
191
|
+
TAG,
|
|
192
|
+
"[stopScan] still scanning because there are active listeners"
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
activeScanCount -= 1
|
|
196
|
+
promise.resolve(null)
|
|
197
|
+
}
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
200
|
|
|
@@ -217,7 +246,9 @@ class TransportHidModule(
|
|
|
217
246
|
try {
|
|
218
247
|
val device = connectedDevices.firstOrNull() { it.id == sessionId }
|
|
219
248
|
if (device == null) {
|
|
220
|
-
promise.
|
|
249
|
+
promise.resolve(
|
|
250
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceNotFound).toWritableMap()
|
|
251
|
+
)
|
|
221
252
|
return
|
|
222
253
|
}
|
|
223
254
|
coroutineScope.launch {
|
|
@@ -43,8 +43,11 @@ import kotlinx.coroutines.flow.shareIn
|
|
|
43
43
|
import kotlinx.coroutines.isActive
|
|
44
44
|
import kotlinx.coroutines.launch
|
|
45
45
|
import kotlin.time.Duration
|
|
46
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
46
47
|
import kotlin.time.Duration.Companion.seconds
|
|
47
48
|
|
|
49
|
+
private val TAG = "DefaultAndroidUsbTransport"
|
|
50
|
+
|
|
48
51
|
internal class DefaultAndroidUsbTransport(
|
|
49
52
|
private val application: Application,
|
|
50
53
|
private val usbManager: UsbManager,
|
|
@@ -67,22 +70,16 @@ internal class DefaultAndroidUsbTransport(
|
|
|
67
70
|
private var discoveryJob: Job? = null
|
|
68
71
|
|
|
69
72
|
override fun startScan(): Flow<List<DiscoveryDevice>> {
|
|
73
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] called"))
|
|
70
74
|
val scanStateFlow = MutableStateFlow<List<DiscoveryDevice>>(emptyList())
|
|
71
75
|
discoveryJob?.cancel()
|
|
72
76
|
discoveryJob =
|
|
73
77
|
scope.launch {
|
|
74
78
|
while (isActive) {
|
|
75
|
-
|
|
76
|
-
val
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
usbConnections.filter {
|
|
80
|
-
device == it.value.getApduSender().dependencies.usbDevice
|
|
81
|
-
}.isEmpty()
|
|
82
|
-
}.toUsbDevices()
|
|
83
|
-
|
|
84
|
-
scanStateFlow.value = devices.toScannedDevices()
|
|
85
|
-
|
|
79
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] isActive loop"))
|
|
80
|
+
val usbDevices = usbManager.deviceList.values.toList().toUsbDevices()
|
|
81
|
+
scanStateFlow.value = usbDevices.toScannedDevices()
|
|
82
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] scannedDevices=${scanStateFlow.value}"))
|
|
86
83
|
delay(scanDelay)
|
|
87
84
|
}
|
|
88
85
|
}
|
|
@@ -90,6 +87,7 @@ internal class DefaultAndroidUsbTransport(
|
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
override fun stopScan() {
|
|
90
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] called"))
|
|
93
91
|
discoveryJob?.cancel()
|
|
94
92
|
discoveryJob = null
|
|
95
93
|
}
|
|
@@ -199,6 +197,7 @@ internal class DefaultAndroidUsbTransport(
|
|
|
199
197
|
request = UsbRequest(),
|
|
200
198
|
loggerService = loggerService
|
|
201
199
|
)
|
|
200
|
+
delay(POST_CONNECTION_DELAY)
|
|
202
201
|
|
|
203
202
|
if (!usbConnectionsPendingReconnection.contains(deviceConnection)) {
|
|
204
203
|
/**
|
|
@@ -294,6 +293,27 @@ internal class DefaultAndroidUsbTransport(
|
|
|
294
293
|
return if (usbDevice == null || ledgerUsbDevice == null) {
|
|
295
294
|
InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
|
|
296
295
|
} else {
|
|
296
|
+
|
|
297
|
+
val existingConnection = usbConnections.firstNotNullOfOrNull {
|
|
298
|
+
if (it.value.getApduSender().dependencies.usbDevice == usbDevice) it.value
|
|
299
|
+
else if (it.key == generateSessionId(usbDevice)) it.value
|
|
300
|
+
else null
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (existingConnection != null) {
|
|
304
|
+
val connectedDevice =
|
|
305
|
+
InternalConnectedDevice(
|
|
306
|
+
existingConnection.sessionId,
|
|
307
|
+
discoveryDevice.name,
|
|
308
|
+
discoveryDevice.ledgerDevice,
|
|
309
|
+
discoveryDevice.connectivityType,
|
|
310
|
+
sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
|
|
311
|
+
existingConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
return InternalConnectionResult.Connected(device = connectedDevice, sessionId = existingConnection.sessionId)
|
|
315
|
+
}
|
|
316
|
+
|
|
297
317
|
val permissionResult = checkOrRequestPermission(usbDevice)
|
|
298
318
|
if (permissionResult is PermissionResult.Denied) {
|
|
299
319
|
return permissionResult.connectionError
|
|
@@ -312,6 +332,7 @@ internal class DefaultAndroidUsbTransport(
|
|
|
312
332
|
request = UsbRequest(),
|
|
313
333
|
loggerService = loggerService,
|
|
314
334
|
)
|
|
335
|
+
delay(POST_CONNECTION_DELAY)
|
|
315
336
|
|
|
316
337
|
val deviceConnection = DeviceConnection(
|
|
317
338
|
sessionId = sessionId,
|
|
@@ -354,6 +375,8 @@ internal class DefaultAndroidUsbTransport(
|
|
|
354
375
|
private fun generateSessionId(usbDevice: UsbDevice): String = "usb_${usbDevice.deviceId}"
|
|
355
376
|
}
|
|
356
377
|
|
|
378
|
+
private val POST_CONNECTION_DELAY = 200.milliseconds
|
|
379
|
+
|
|
357
380
|
private fun List<LedgerUsbDevice>.toScannedDevices(): List<DiscoveryDevice> =
|
|
358
381
|
this.map {
|
|
359
382
|
it.toScannedDevice()
|
|
@@ -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.LoggerService
|
|
6
7
|
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
|
|
7
8
|
import kotlinx.coroutines.CoroutineDispatcher
|
|
@@ -25,12 +26,31 @@ internal class DeviceConnectionStateMachine(
|
|
|
25
26
|
fun getState() = state
|
|
26
27
|
|
|
27
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 */
|
|
28
43
|
when (newState) {
|
|
29
44
|
is State.Connected -> {}
|
|
30
45
|
is State.SendingApdu -> {
|
|
31
46
|
sendApduFn(newState.requestContent.apdu, newState.requestContent.abortTimeoutDuration)
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
is State.WaitingForDisconnection -> {
|
|
50
|
+
// TODO: send getAppAndVersion
|
|
51
|
+
sendApduFn ("b0010000".fromHexStringToBytesOrThrow(), Duration.INFINITE)
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
is State.WaitingForReconnection -> {
|
|
35
55
|
startReconnectionTimeout()
|
|
36
56
|
}
|
|
@@ -75,31 +95,27 @@ internal class DeviceConnectionStateMachine(
|
|
|
75
95
|
when (event) {
|
|
76
96
|
is Event.ApduResultReceived -> {
|
|
77
97
|
when (event.result) {
|
|
78
|
-
is SendApduResult.
|
|
79
|
-
if
|
|
80
|
-
|
|
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))
|
|
81
105
|
} else {
|
|
82
106
|
pushState(State.Connected)
|
|
107
|
+
currentState.requestContent.resultCallback(event.result)
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
val apdu = event.result.apdu
|
|
89
|
-
val apduSize = apdu.size
|
|
90
|
-
val isSuccessApdu =
|
|
91
|
-
apdu.size >= 2 &&
|
|
92
|
-
apdu[apduSize - 2] == 0x90.toByte() &&
|
|
93
|
-
apdu[apduSize - 1] == 0x00.toByte()
|
|
94
|
-
|
|
95
|
-
if (isSuccessApdu && currentState.requestContent.triggersDisconnection) {
|
|
96
|
-
pushState(State.WaitingForReconnection)
|
|
110
|
+
is SendApduResult.Failure -> {
|
|
111
|
+
if (isFatalSendApduFailure(event.result)) {
|
|
112
|
+
pushState(State.Terminated)
|
|
97
113
|
} else {
|
|
98
114
|
pushState(State.Connected)
|
|
99
115
|
}
|
|
116
|
+
currentState.requestContent.resultCallback(event.result)
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
|
-
currentState.requestContent.resultCallback(event.result)
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
is Event.CloseConnectionRequested -> {
|
|
@@ -134,6 +150,42 @@ internal class DeviceConnectionStateMachine(
|
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
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
|
+
|
|
137
189
|
is State.WaitingForReconnection -> {
|
|
138
190
|
when (event) {
|
|
139
191
|
is Event.DeviceConnected -> {
|
|
@@ -240,6 +292,24 @@ internal class DeviceConnectionStateMachine(
|
|
|
240
292
|
}
|
|
241
293
|
}
|
|
242
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)
|
|
311
|
+
}
|
|
312
|
+
|
|
243
313
|
private var timeoutJob: Job? = null
|
|
244
314
|
private fun startReconnectionTimeout() {
|
|
245
315
|
// start a timeout and at the end, emit a WaitingForReconnectionTimedOut event
|
|
@@ -254,23 +324,23 @@ internal class DeviceConnectionStateMachine(
|
|
|
254
324
|
timeoutJob = null
|
|
255
325
|
}
|
|
256
326
|
|
|
257
|
-
|
|
327
|
+
fun requestSendApdu(requestContent: SendApduRequestContent) {
|
|
258
328
|
handleEvent(Event.SendApduRequested(requestContent))
|
|
259
329
|
}
|
|
260
330
|
|
|
261
|
-
|
|
331
|
+
fun requestCloseConnection() {
|
|
262
332
|
handleEvent(Event.CloseConnectionRequested)
|
|
263
333
|
}
|
|
264
334
|
|
|
265
|
-
|
|
335
|
+
fun handleApduResult(result: SendApduResult) {
|
|
266
336
|
handleEvent(Event.ApduResultReceived(result))
|
|
267
337
|
}
|
|
268
338
|
|
|
269
|
-
|
|
339
|
+
fun handleDeviceConnected() {
|
|
270
340
|
handleEvent(Event.DeviceConnected)
|
|
271
341
|
}
|
|
272
342
|
|
|
273
|
-
|
|
343
|
+
fun handleDeviceDisconnected() {
|
|
274
344
|
handleEvent(Event.DeviceDisconnected)
|
|
275
345
|
}
|
|
276
346
|
|
|
@@ -304,6 +374,8 @@ internal class DeviceConnectionStateMachine(
|
|
|
304
374
|
|
|
305
375
|
data object WaitingForReconnection : State()
|
|
306
376
|
|
|
377
|
+
data class WaitingForDisconnection(val requestContent: SendApduRequestContent, val result: SendApduResult): State()
|
|
378
|
+
|
|
307
379
|
data class WaitingForReconnectionWithQueuedApdu(val requestContent: SendApduRequestContent) :
|
|
308
380
|
State()
|
|
309
381
|
|
|
@@ -5,6 +5,21 @@ public sealed class LedgerDevice(
|
|
|
5
5
|
public val usbInfo: UsbInfo,
|
|
6
6
|
public val bleInformation: BleInformation? = null,
|
|
7
7
|
) {
|
|
8
|
+
public data object Apex :
|
|
9
|
+
LedgerDevice(
|
|
10
|
+
name = "Ledger Apex",
|
|
11
|
+
usbInfo = UsbInfo(LEDGER_USB_VENDOR_ID, "0x80", "0x0008"),
|
|
12
|
+
bleInformation =
|
|
13
|
+
BleInformation(
|
|
14
|
+
serviceUuid = "13d63400-2c97-6004-0000-4c6564676572",
|
|
15
|
+
notifyCharacteristicUuid =
|
|
16
|
+
"13d63400-2c97-6004-0001-4c6564676572",
|
|
17
|
+
writeWithResponseCharacteristicUuid =
|
|
18
|
+
"13d63400-2c97-6004-0002-4c6564676572",
|
|
19
|
+
writeWithoutResponseCharacteristicUuid =
|
|
20
|
+
"13d63400-2c97-6004-0003-4c6564676572",
|
|
21
|
+
),
|
|
22
|
+
)
|
|
8
23
|
public data object Flex :
|
|
9
24
|
LedgerDevice(
|
|
10
25
|
name = "Ledger Flex",
|
|
@@ -77,6 +92,7 @@ public sealed class LedgerDevice(
|
|
|
77
92
|
add(NanoX)
|
|
78
93
|
add(NanoSPlus)
|
|
79
94
|
add(NanoS)
|
|
95
|
+
add(Apex)
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
public fun getAllDevices(): List<LedgerDevice> {
|
|
@@ -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
|
}
|