@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rn-ble-pairing-removed-while-reconnecting-20250807094338 → 0.0.0-rn-hid-issues-20251022142715
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 +41 -14
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +58 -25
- 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 +5 -5
|
@@ -22,6 +22,7 @@ import com.ledger.devicesdk.shared.internal.connection.InternalConnectedDevice
|
|
|
22
22
|
import com.ledger.devicesdk.shared.internal.connection.InternalConnectionResult
|
|
23
23
|
import com.ledger.devicesdk.shared.internal.event.SdkEventDispatcher
|
|
24
24
|
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
25
|
+
import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
|
|
25
26
|
import com.ledger.devicesdk.shared.internal.transport.TransportEvent
|
|
26
27
|
import kotlinx.coroutines.CoroutineScope
|
|
27
28
|
import kotlinx.coroutines.Dispatchers
|
|
@@ -34,6 +35,8 @@ import kotlin.random.Random
|
|
|
34
35
|
import kotlin.time.Duration
|
|
35
36
|
import kotlin.time.Duration.Companion.milliseconds
|
|
36
37
|
|
|
38
|
+
private val TAG = "TransportHidModule"
|
|
39
|
+
|
|
37
40
|
class TransportHidModule(
|
|
38
41
|
private val reactContext: ReactApplicationContext,
|
|
39
42
|
private val coroutineScope: CoroutineScope
|
|
@@ -49,7 +52,7 @@ class TransportHidModule(
|
|
|
49
52
|
private val loggerService: LoggerService =
|
|
50
53
|
LoggerService { info ->
|
|
51
54
|
Timber.tag("LDMKTransportHIDModule " + info.tag).d(info.message)
|
|
52
|
-
sendEvent(reactContext, BridgeEvents.TransportLog(info))
|
|
55
|
+
// sendEvent(reactContext, BridgeEvents.TransportLog(info))
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
private val transport: AndroidUsbTransport? by lazy {
|
|
@@ -133,12 +136,18 @@ class TransportHidModule(
|
|
|
133
136
|
transport?.stopScan()
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
private var
|
|
139
|
+
private var activeScanCount = 0
|
|
137
140
|
|
|
138
141
|
@ReactMethod
|
|
139
142
|
fun startScan(promise: Promise) {
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
loggerService.log(
|
|
144
|
+
buildSimpleDebugLogInfo(TAG, "[startScan] called")
|
|
145
|
+
)
|
|
146
|
+
activeScanCount += 1
|
|
147
|
+
if (activeScanCount > 1) {
|
|
148
|
+
loggerService.log(
|
|
149
|
+
buildSimpleDebugLogInfo(TAG, "[startScan] already scanning")
|
|
150
|
+
)
|
|
142
151
|
promise.resolve(null)
|
|
143
152
|
return
|
|
144
153
|
}
|
|
@@ -156,16 +165,34 @@ class TransportHidModule(
|
|
|
156
165
|
|
|
157
166
|
@ReactMethod
|
|
158
167
|
fun stopScan(promise: Promise) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
loggerService.log(
|
|
169
|
+
buildSimpleDebugLogInfo(TAG, "[stopScan] called, activeScanCount=$activeScanCount")
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
when(activeScanCount) {
|
|
173
|
+
0 -> {
|
|
174
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] no active scan"))
|
|
175
|
+
promise.resolve(null)
|
|
176
|
+
}
|
|
177
|
+
1 -> {
|
|
178
|
+
try {
|
|
179
|
+
transport!!.stopScan()
|
|
180
|
+
promise.resolve(null)
|
|
181
|
+
} catch (e: Exception) {
|
|
182
|
+
promise.reject(e);
|
|
183
|
+
}
|
|
184
|
+
activeScanCount = 0
|
|
185
|
+
}
|
|
186
|
+
else -> {
|
|
187
|
+
loggerService.log(
|
|
188
|
+
buildSimpleDebugLogInfo(
|
|
189
|
+
TAG,
|
|
190
|
+
"[stopScan] still scanning because there are active listeners"
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
activeScanCount -= 1
|
|
194
|
+
promise.resolve(null)
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
}
|
|
171
198
|
|
|
@@ -45,6 +45,8 @@ import kotlinx.coroutines.launch
|
|
|
45
45
|
import kotlin.time.Duration
|
|
46
46
|
import kotlin.time.Duration.Companion.seconds
|
|
47
47
|
|
|
48
|
+
private val TAG = "DefaultAndroidUsbTransport"
|
|
49
|
+
|
|
48
50
|
internal class DefaultAndroidUsbTransport(
|
|
49
51
|
private val application: Application,
|
|
50
52
|
private val usbManager: UsbManager,
|
|
@@ -67,22 +69,16 @@ internal class DefaultAndroidUsbTransport(
|
|
|
67
69
|
private var discoveryJob: Job? = null
|
|
68
70
|
|
|
69
71
|
override fun startScan(): Flow<List<DiscoveryDevice>> {
|
|
72
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] called"))
|
|
70
73
|
val scanStateFlow = MutableStateFlow<List<DiscoveryDevice>>(emptyList())
|
|
71
74
|
discoveryJob?.cancel()
|
|
72
75
|
discoveryJob =
|
|
73
76
|
scope.launch {
|
|
74
77
|
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
|
-
|
|
78
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] isActive loop"))
|
|
79
|
+
val usbDevices = usbManager.deviceList.values.toList().toUsbDevices()
|
|
80
|
+
scanStateFlow.value = usbDevices.toScannedDevices()
|
|
81
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] scannedDevices=${scanStateFlow.value}"))
|
|
86
82
|
delay(scanDelay)
|
|
87
83
|
}
|
|
88
84
|
}
|
|
@@ -90,6 +86,7 @@ internal class DefaultAndroidUsbTransport(
|
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
override fun stopScan() {
|
|
89
|
+
loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] called"))
|
|
93
90
|
discoveryJob?.cancel()
|
|
94
91
|
discoveryJob = null
|
|
95
92
|
}
|
|
@@ -123,9 +120,10 @@ internal class DefaultAndroidUsbTransport(
|
|
|
123
120
|
"Device disconnected (sessionId=${deviceConnection.sessionId})"
|
|
124
121
|
)
|
|
125
122
|
)
|
|
126
|
-
deviceConnection.handleDeviceDisconnected()
|
|
127
123
|
usbConnections.remove(key)
|
|
128
124
|
usbConnectionsPendingReconnection.add(deviceConnection)
|
|
125
|
+
deviceConnection.handleDeviceDisconnected()
|
|
126
|
+
(deviceConnection.getApduSender() as AndroidUsbApduSender).release()
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
}
|
|
@@ -186,19 +184,30 @@ internal class DefaultAndroidUsbTransport(
|
|
|
186
184
|
"Reconnecting device (sessionId=${deviceConnection.sessionId})"
|
|
187
185
|
)
|
|
188
186
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
187
|
+
|
|
188
|
+
val apduSender = AndroidUsbApduSender(
|
|
189
|
+
dependencies = AndroidUsbApduSender.Dependencies(
|
|
190
|
+
usbDevice = usbDevice,
|
|
191
|
+
ledgerUsbDevice = state.ledgerUsbDevice,
|
|
192
|
+
),
|
|
193
|
+
usbManager = usbManager,
|
|
194
|
+
ioDispatcher = Dispatchers.IO,
|
|
195
|
+
framerService = FramerService(loggerService),
|
|
196
|
+
request = UsbRequest(),
|
|
197
|
+
loggerService = loggerService
|
|
201
198
|
)
|
|
199
|
+
|
|
200
|
+
if (!usbConnectionsPendingReconnection.contains(deviceConnection)) {
|
|
201
|
+
/**
|
|
202
|
+
* We check this because maybe by the time we get here,
|
|
203
|
+
* the reconnection has timed out and the session has been terminated.
|
|
204
|
+
* Easy to reproduce for instance if the permission request requires
|
|
205
|
+
* a user interaction.
|
|
206
|
+
*/
|
|
207
|
+
apduSender.release()
|
|
208
|
+
return@launch
|
|
209
|
+
}
|
|
210
|
+
deviceConnection.handleDeviceConnected(apduSender)
|
|
202
211
|
usbConnectionsPendingReconnection.remove(deviceConnection)
|
|
203
212
|
usbConnections[deviceConnection.sessionId] = deviceConnection
|
|
204
213
|
}
|
|
@@ -282,6 +291,27 @@ internal class DefaultAndroidUsbTransport(
|
|
|
282
291
|
return if (usbDevice == null || ledgerUsbDevice == null) {
|
|
283
292
|
InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
|
|
284
293
|
} else {
|
|
294
|
+
|
|
295
|
+
val existingConnection = usbConnections.firstNotNullOfOrNull {
|
|
296
|
+
if (it.value.getApduSender().dependencies.usbDevice == usbDevice) it.value
|
|
297
|
+
else if (it.key == generateSessionId(usbDevice)) it.value
|
|
298
|
+
else null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (existingConnection != null) {
|
|
302
|
+
val connectedDevice =
|
|
303
|
+
InternalConnectedDevice(
|
|
304
|
+
existingConnection.sessionId,
|
|
305
|
+
discoveryDevice.name,
|
|
306
|
+
discoveryDevice.ledgerDevice,
|
|
307
|
+
discoveryDevice.connectivityType,
|
|
308
|
+
sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
|
|
309
|
+
existingConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
return InternalConnectionResult.Connected(device = connectedDevice, sessionId = existingConnection.sessionId)
|
|
313
|
+
}
|
|
314
|
+
|
|
285
315
|
val permissionResult = checkOrRequestPermission(usbDevice)
|
|
286
316
|
if (permissionResult is PermissionResult.Denied) {
|
|
287
317
|
return permissionResult.connectionError
|
|
@@ -304,9 +334,10 @@ internal class DefaultAndroidUsbTransport(
|
|
|
304
334
|
val deviceConnection = DeviceConnection(
|
|
305
335
|
sessionId = sessionId,
|
|
306
336
|
deviceApduSender = apduSender,
|
|
307
|
-
isFatalSendApduFailure = { false },
|
|
337
|
+
isFatalSendApduFailure = { false },
|
|
308
338
|
reconnectionTimeoutDuration = 5.seconds,
|
|
309
339
|
onTerminated = {
|
|
340
|
+
(it.getApduSender() as AndroidUsbApduSender).release()
|
|
310
341
|
usbConnections.remove(sessionId)
|
|
311
342
|
usbConnectionsPendingReconnection.remove(it)
|
|
312
343
|
eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
|
|
@@ -333,7 +364,9 @@ internal class DefaultAndroidUsbTransport(
|
|
|
333
364
|
}
|
|
334
365
|
|
|
335
366
|
override suspend fun disconnect(deviceId: String) {
|
|
367
|
+
// The DeviceConnection is either in usbConnections or usbConnectionsPendingReconnection
|
|
336
368
|
usbConnections[deviceId]?.requestCloseConnection()
|
|
369
|
+
usbConnectionsPendingReconnection.find { it.sessionId == deviceId }?.requestCloseConnection()
|
|
337
370
|
}
|
|
338
371
|
|
|
339
372
|
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
|
|