@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.
Files changed (32) hide show
  1. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +41 -14
  2. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +58 -25
  3. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +33 -34
  4. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +100 -31
  5. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +142 -15
  6. package/lib/cjs/api/bridge/mapper.js +1 -1
  7. package/lib/cjs/api/bridge/mapper.js.map +2 -2
  8. package/lib/cjs/api/bridge/mapper.test.js +1 -1
  9. package/lib/cjs/api/bridge/mapper.test.js.map +2 -2
  10. package/lib/cjs/api/transport/Errors.js +1 -1
  11. package/lib/cjs/api/transport/Errors.js.map +3 -3
  12. package/lib/cjs/api/transport/RNHidTransport.js +1 -1
  13. package/lib/cjs/api/transport/RNHidTransport.js.map +2 -2
  14. package/lib/cjs/api/transport/RNHidTransport.test.js +1 -1
  15. package/lib/cjs/api/transport/RNHidTransport.test.js.map +2 -2
  16. package/lib/cjs/package.json +1 -1
  17. package/lib/esm/api/bridge/mapper.js +1 -1
  18. package/lib/esm/api/bridge/mapper.js.map +3 -3
  19. package/lib/esm/api/bridge/mapper.test.js +1 -1
  20. package/lib/esm/api/bridge/mapper.test.js.map +3 -3
  21. package/lib/esm/api/transport/Errors.js +1 -1
  22. package/lib/esm/api/transport/Errors.js.map +3 -3
  23. package/lib/esm/api/transport/RNHidTransport.js +1 -1
  24. package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
  25. package/lib/esm/api/transport/RNHidTransport.test.js +1 -1
  26. package/lib/esm/api/transport/RNHidTransport.test.js.map +3 -3
  27. package/lib/esm/package.json +1 -1
  28. package/lib/types/api/bridge/mapper.d.ts.map +1 -1
  29. package/lib/types/api/transport/Errors.d.ts +1 -1
  30. package/lib/types/api/transport/Errors.d.ts.map +1 -1
  31. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  32. 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 discoveryCount = 0
139
+ private var activeScanCount = 0
137
140
 
138
141
  @ReactMethod
139
142
  fun startScan(promise: Promise) {
140
- discoveryCount += 1
141
- if (discoveryCount > 1) {
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
- discoveryCount -= 1
160
- if (discoveryCount > 0) {
161
- promise.resolve(null)
162
- return
163
- }
164
- try {
165
- transport!!.stopScan()
166
- promise.resolve(null)
167
- } catch (e: Exception) {
168
- promise.reject(e);
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
- val usbDevices = usbManager.deviceList.values.toList()
76
- val devices =
77
- usbDevices
78
- .filter { device ->
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
- deviceConnection.handleDeviceConnected(
190
- AndroidUsbApduSender(
191
- dependencies = AndroidUsbApduSender.Dependencies(
192
- usbDevice = usbDevice,
193
- ledgerUsbDevice = state.ledgerUsbDevice,
194
- ),
195
- usbManager = usbManager,
196
- ioDispatcher = Dispatchers.IO,
197
- framerService = FramerService(loggerService),
198
- request = UsbRequest(),
199
- loggerService = loggerService
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 }, // TODO: refine this
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
- private val usbManager: UsbManager,
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
- try {
73
+ transmitApdu(
74
+ usbConnection = usbConnection,
75
+ androidToUsbEndpoint = androidToUsbEndpoint,
76
+ rawApdu = apdu,
77
+ )
70
78
 
71
- transmitApdu(
79
+ val apduResponse =
80
+ receiveApdu(
72
81
  usbConnection = usbConnection,
73
- androidToUsbEndpoint = androidToUsbEndpoint,
74
- rawApdu = apdu,
82
+ usbToAndroidEndpoint = usbToAndroidEndpoint,
75
83
  )
84
+ timeoutJob.cancel()
76
85
 
77
- val apduResponse =
78
- receiveApdu(
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 currentState = state
49
- when (currentState) {
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.Failure -> {
75
- if (isFatalSendApduFailure(event.result)) {
76
- pushState(State.Terminated)
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
- is SendApduResult.Success -> {
83
- // check if last 2 bytes of APDU are [0x90,OxO0]
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
- val logMessage = """
238
- Received event:
239
- In state: $currentState
240
- -> Event: $event
241
- -> New state: $state
242
- """.trimIndent()
243
- loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
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
- public fun requestSendApdu(requestContent: SendApduRequestContent) {
327
+ fun requestSendApdu(requestContent: SendApduRequestContent) {
261
328
  handleEvent(Event.SendApduRequested(requestContent))
262
329
  }
263
330
 
264
- public fun requestCloseConnection() {
331
+ fun requestCloseConnection() {
265
332
  handleEvent(Event.CloseConnectionRequested)
266
333
  }
267
334
 
268
- public fun handleApduResult(result: SendApduResult) {
335
+ fun handleApduResult(result: SendApduResult) {
269
336
  handleEvent(Event.ApduResultReceived(result))
270
337
  }
271
338
 
272
- public fun handleDeviceConnected() {
339
+ fun handleDeviceConnected() {
273
340
  handleEvent(Event.DeviceConnected)
274
341
  }
275
342
 
276
- public fun handleDeviceDisconnected() {
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