@ledgerhq/device-transport-kit-react-native-hid 0.0.0-fix-rn-ble-20250502082216 → 0.0.0-hid-candidate-20250523130730

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 (55) hide show
  1. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +11 -2
  2. package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +2 -0
  3. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +84 -14
  4. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +70 -29
  5. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +2 -1
  6. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +5 -4
  7. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +4 -3
  8. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +4 -0
  9. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +1 -1
  10. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +1 -1
  11. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +47 -31
  12. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +9 -5
  13. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  14. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  15. package/lib/cjs/api/bridge/mapper.js +1 -1
  16. package/lib/cjs/api/bridge/mapper.js.map +2 -2
  17. package/lib/cjs/api/bridge/mapper.test.js +1 -1
  18. package/lib/cjs/api/bridge/mapper.test.js.map +2 -2
  19. package/lib/cjs/api/bridge/types.js +1 -1
  20. package/lib/cjs/api/bridge/types.js.map +1 -1
  21. package/lib/cjs/api/transport/Errors.js +1 -1
  22. package/lib/cjs/api/transport/Errors.js.map +3 -3
  23. package/lib/cjs/api/transport/NativeModuleWrapper.js +1 -1
  24. package/lib/cjs/api/transport/NativeModuleWrapper.js.map +1 -1
  25. package/lib/cjs/api/transport/RNHidTransport.js +1 -1
  26. package/lib/cjs/api/transport/RNHidTransport.js.map +3 -3
  27. package/lib/cjs/api/transport/RNHidTransport.test.js +1 -1
  28. package/lib/cjs/api/transport/RNHidTransport.test.js.map +2 -2
  29. package/lib/cjs/package.json +10 -10
  30. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  31. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  32. package/lib/esm/api/bridge/mapper.js +1 -1
  33. package/lib/esm/api/bridge/mapper.js.map +3 -3
  34. package/lib/esm/api/bridge/mapper.test.js +1 -1
  35. package/lib/esm/api/bridge/mapper.test.js.map +3 -3
  36. package/lib/esm/api/bridge/types.js.map +1 -1
  37. package/lib/esm/api/transport/Errors.js +1 -1
  38. package/lib/esm/api/transport/Errors.js.map +3 -3
  39. package/lib/esm/api/transport/RNHidTransport.js +1 -1
  40. package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
  41. package/lib/esm/api/transport/RNHidTransport.test.js +1 -1
  42. package/lib/esm/api/transport/RNHidTransport.test.js.map +3 -3
  43. package/lib/esm/package.json +10 -10
  44. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts +1 -1
  45. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts.map +1 -1
  46. package/lib/types/api/bridge/mapper.d.ts.map +1 -1
  47. package/lib/types/api/bridge/types.d.ts +2 -2
  48. package/lib/types/api/bridge/types.d.ts.map +1 -1
  49. package/lib/types/api/transport/Errors.d.ts +1 -1
  50. package/lib/types/api/transport/Errors.d.ts.map +1 -1
  51. package/lib/types/api/transport/NativeModuleWrapper.d.ts +1 -1
  52. package/lib/types/api/transport/NativeModuleWrapper.d.ts.map +1 -1
  53. package/lib/types/api/transport/RNHidTransport.d.ts.map +1 -1
  54. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  55. package/package.json +12 -12
@@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.onEach
31
31
  import kotlinx.coroutines.launch
32
32
  import timber.log.Timber
33
33
  import kotlin.random.Random
34
+ import kotlin.time.Duration
34
35
  import kotlin.time.Duration.Companion.milliseconds
35
36
 
36
37
  class TransportHidModule(
@@ -206,7 +207,13 @@ class TransportHidModule(
206
207
  }
207
208
 
208
209
  @ReactMethod
209
- fun sendApdu(sessionId: String, apduBase64: String, promise: Promise) {
210
+ fun sendApdu(
211
+ sessionId: String,
212
+ apduBase64: String,
213
+ triggersDisconnection: Boolean,
214
+ abortTimeout: Int,
215
+ promise: Promise
216
+ ) {
210
217
  try {
211
218
  val device = connectedDevices.firstOrNull() { it.id == sessionId }
212
219
  if (device == null) {
@@ -216,7 +223,9 @@ class TransportHidModule(
216
223
  coroutineScope.launch {
217
224
  try {
218
225
  val apdu: ByteArray = Base64.decode(apduBase64, Base64.DEFAULT)
219
- val res = device.sendApduFn(apdu)
226
+ val abortTimeoutDuration = if (abortTimeout <= 0) Duration.INFINITE else abortTimeout.milliseconds
227
+ val res =
228
+ device.sendApduFn(apdu, triggersDisconnection, abortTimeoutDuration)
220
229
  promise.resolve(res.toWritableMap())
221
230
  } catch (e: Exception) {
222
231
  Timber.i("$e, ${e.cause}")
@@ -103,6 +103,8 @@ internal fun SendApduResult.toWritableMap(): WritableMap {
103
103
  SendApduFailureReason.NoUsbEndpointFound -> "NoUsbEndpointFound"
104
104
  SendApduFailureReason.DeviceDisconnected -> "DeviceDisconnected"
105
105
  SendApduFailureReason.Unknown -> "Unknown"
106
+ SendApduFailureReason.AbortTimeout -> "SendApduTimeout"
107
+ SendApduFailureReason.EmptyResponse -> "EmptyResponse"
106
108
  })
107
109
  }
108
110
  }
@@ -97,31 +97,57 @@ internal class DefaultAndroidUsbTransport(
97
97
  override fun updateUsbState(state: UsbState) {
98
98
  when (state) {
99
99
  is UsbState.Detached -> {
100
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Detached deviceId=${state.ledgerUsbDevice.uid}"))
100
+ loggerService.log(
101
+ buildSimpleDebugLogInfo(
102
+ "AndroidUsbTransport",
103
+ "Detached deviceId=${state.ledgerUsbDevice.uid}"
104
+ )
105
+ )
101
106
  usbConnections.entries.find {
102
107
  it.value.getApduSender().dependencies.ledgerUsbDevice.uid == state.ledgerUsbDevice.uid
103
108
  }.let { item ->
104
109
  scope.launch {
105
110
  if (item == null) {
106
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No connection found"))
111
+ loggerService.log(
112
+ buildSimpleWarningLogInfo(
113
+ "AndroidUsbTransport",
114
+ "No connection found"
115
+ )
116
+ )
107
117
  return@launch
108
118
  }
109
119
  val (key, deviceConnection) = item
110
- loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Device disconnected (sessionId=${deviceConnection.sessionId})"))
111
- deviceConnection.handleDeviceDisconnected()
120
+ loggerService.log(
121
+ buildSimpleInfoLogInfo(
122
+ "AndroidUsbTransport",
123
+ "Device disconnected (sessionId=${deviceConnection.sessionId})"
124
+ )
125
+ )
112
126
  usbConnections.remove(key)
113
127
  usbConnectionsPendingReconnection.add(deviceConnection)
128
+ deviceConnection.handleDeviceDisconnected()
129
+ (deviceConnection.getApduSender() as AndroidUsbApduSender).release()
114
130
  }
115
131
  }
116
132
  }
117
133
 
118
134
  is UsbState.Attached -> {
119
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Attached deviceId=${state.ledgerUsbDevice.uid}, pendingReconnections=${usbConnectionsPendingReconnection}"))
135
+ loggerService.log(
136
+ buildSimpleDebugLogInfo(
137
+ "AndroidUsbTransport",
138
+ "Attached deviceId=${state.ledgerUsbDevice.uid}, pendingReconnections=${usbConnectionsPendingReconnection}"
139
+ )
140
+ )
120
141
  val usbDevice = usbManager.deviceList.values.firstOrNull {
121
142
  it.toLedgerUsbDevice()?.uid == state.ledgerUsbDevice.uid
122
143
  }
123
144
  if (usbDevice == null) {
124
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No UsbDevice found"))
145
+ loggerService.log(
146
+ buildSimpleWarningLogInfo(
147
+ "AndroidUsbTransport",
148
+ "No UsbDevice found"
149
+ )
150
+ )
125
151
  return
126
152
  }
127
153
  usbConnectionsPendingReconnection.firstOrNull {
@@ -138,14 +164,29 @@ internal class DefaultAndroidUsbTransport(
138
164
  )
139
165
  return@launch
140
166
  }
141
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Found matching device connection $deviceConnection"))
167
+ loggerService.log(
168
+ buildSimpleDebugLogInfo(
169
+ "AndroidUsbTransport",
170
+ "Found matching device connection $deviceConnection"
171
+ )
172
+ )
142
173
 
143
174
  val permissionResult = checkOrRequestPermission(usbDevice)
144
175
  if (permissionResult is PermissionResult.Denied) {
145
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Permission denied"))
176
+ loggerService.log(
177
+ buildSimpleDebugLogInfo(
178
+ "AndroidUsbTransport",
179
+ "Permission denied"
180
+ )
181
+ )
146
182
  return@launch
147
183
  }
148
- loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Reconnecting device (sessionId=${deviceConnection.sessionId})"))
184
+ loggerService.log(
185
+ buildSimpleInfoLogInfo(
186
+ "AndroidUsbTransport",
187
+ "Reconnecting device (sessionId=${deviceConnection.sessionId})"
188
+ )
189
+ )
149
190
  deviceConnection.handleDeviceConnected(
150
191
  AndroidUsbApduSender(
151
192
  dependencies = AndroidUsbApduSender.Dependencies(
@@ -195,7 +236,12 @@ internal class DefaultAndroidUsbTransport(
195
236
  device = usbDevice,
196
237
  )
197
238
 
198
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Waiting for permission result"))
239
+ loggerService.log(
240
+ buildSimpleDebugLogInfo(
241
+ "AndroidUsbTransport",
242
+ "Waiting for permission result"
243
+ )
244
+ )
199
245
 
200
246
  val result = eventsFlow.first {
201
247
  it is UsbPermissionEvent.PermissionGranted ||
@@ -229,12 +275,33 @@ internal class DefaultAndroidUsbTransport(
229
275
  }
230
276
 
231
277
  override suspend fun connect(discoveryDevice: DiscoveryDevice): InternalConnectionResult {
232
- val usbDevice: UsbDevice? =
233
- usbManager.deviceList.values.firstOrNull { it.deviceId == discoveryDevice.uid.toInt() }
278
+
279
+ loggerService.log(
280
+ buildSimpleDebugLogInfo(
281
+ "AndroidUsbTransport",
282
+ "[connect] discoveryDevice=$discoveryDevice"
283
+ )
284
+ )
285
+
286
+ val usbDevices = usbManager.deviceList.values
287
+
288
+ var usbDevice: UsbDevice? =
289
+ usbDevices.firstOrNull { it.deviceId == discoveryDevice.uid.toInt() }
290
+
291
+ if (usbDevice == null) {
292
+ // This is useful for LL during the OS update
293
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "[connect] No device found with matching id, looking for device with matching model"))
294
+ usbDevice =
295
+ usbDevices.firstOrNull { it.toLedgerUsbDevice()?.ledgerDevice == discoveryDevice.ledgerDevice }
296
+ } else {
297
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "[connect] Found device with matching id"))
298
+ }
234
299
 
235
300
  val ledgerUsbDevice = usbDevice?.toLedgerUsbDevice()
236
301
 
237
302
  return if (usbDevice == null || ledgerUsbDevice == null) {
303
+ loggerService.log(
304
+ buildSimpleDebugLogInfo("AndroidUsbTransport", "[connect] No device found"))
238
305
  InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
239
306
  } else {
240
307
  val permissionResult = checkOrRequestPermission(usbDevice)
@@ -259,9 +326,10 @@ internal class DefaultAndroidUsbTransport(
259
326
  val deviceConnection = DeviceConnection(
260
327
  sessionId = sessionId,
261
328
  deviceApduSender = apduSender,
262
- isFatalSendApduFailure = { false }, // TODO: refine this
329
+ isFatalSendApduFailure = { false },
263
330
  reconnectionTimeoutDuration = 5.seconds,
264
331
  onTerminated = {
332
+ (it.getApduSender() as AndroidUsbApduSender).release()
265
333
  usbConnections.remove(sessionId)
266
334
  usbConnectionsPendingReconnection.remove(it)
267
335
  eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
@@ -276,7 +344,9 @@ internal class DefaultAndroidUsbTransport(
276
344
  discoveryDevice.name,
277
345
  discoveryDevice.ledgerDevice,
278
346
  discoveryDevice.connectivityType,
279
- sendApduFn = { apdu -> deviceConnection.requestSendApdu(apdu) },
347
+ sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
348
+ deviceConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
349
+ }
280
350
  )
281
351
 
282
352
  usbConnections[sessionId] = deviceConnection
@@ -13,21 +13,21 @@ import android.hardware.usb.UsbInterface
13
13
  import android.hardware.usb.UsbManager
14
14
  import android.hardware.usb.UsbRequest
15
15
  import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
16
+ import com.ledger.devicesdk.shared.androidMainInternal.transport.USB_MTU
17
+ import com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection.DeviceApduSender
16
18
  import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
17
19
  import com.ledger.devicesdk.shared.api.apdu.SendApduResult
18
- import com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection.DeviceApduSender
19
- import com.ledger.devicesdk.shared.api.utils.toHexadecimalString
20
- import com.ledger.devicesdk.shared.androidMainInternal.transport.USB_MTU
21
20
  import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
22
21
  import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
23
- import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
24
22
  import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
25
23
  import com.ledger.devicesdk.shared.internal.transport.framer.to2BytesArray
26
- import java.nio.ByteBuffer
27
- import kotlin.random.Random
28
24
  import kotlinx.coroutines.CoroutineDispatcher
25
+ import kotlinx.coroutines.delay
26
+ import kotlinx.coroutines.launch
29
27
  import kotlinx.coroutines.withContext
30
- import timber.log.Timber
28
+ import java.nio.ByteBuffer
29
+ import kotlin.random.Random
30
+ import kotlin.time.Duration
31
31
 
32
32
  private const val USB_TIMEOUT = 500
33
33
 
@@ -35,44 +35,75 @@ private const val DEFAULT_USB_INTERFACE = 0
35
35
 
36
36
  internal class AndroidUsbApduSender(
37
37
  override val dependencies: Dependencies,
38
- private val usbManager: UsbManager,
38
+ usbManager: UsbManager,
39
39
  private val framerService: FramerService,
40
40
  private val request: UsbRequest,
41
41
  private val ioDispatcher: CoroutineDispatcher,
42
42
  private val loggerService: LoggerService,
43
43
  ) : DeviceApduSender<AndroidUsbApduSender.Dependencies> {
44
-
45
44
  data class Dependencies(
46
45
  val usbDevice: UsbDevice,
47
46
  val ledgerUsbDevice: LedgerUsbDevice,
48
47
  )
49
48
 
50
- override suspend fun send(apdu: ByteArray): SendApduResult =
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
+
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 = usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_OUT }
56
- val usbToAndroidEndpoint = usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_IN }
57
- val usbConnection = usbManager.openDevice(usbDevice).apply { claimInterface(usbInterface, true) }
67
+
68
+ val timeoutJob = launch {
69
+ delay(abortTimeoutDuration)
70
+ throw SendApduTimeoutException
71
+ }
58
72
 
59
73
  transmitApdu(
60
74
  usbConnection = usbConnection,
61
75
  androidToUsbEndpoint = androidToUsbEndpoint,
62
76
  rawApdu = apdu,
63
77
  )
78
+
64
79
  val apduResponse =
65
80
  receiveApdu(
66
81
  usbConnection = usbConnection,
67
82
  usbToAndroidEndpoint = usbToAndroidEndpoint,
68
83
  )
84
+ timeoutJob.cancel()
69
85
 
70
- usbConnection.releaseInterface(usbInterface)
71
- usbConnection.close()
86
+ if (apduResponse.isEmpty()) {
87
+ return@withContext SendApduResult.Failure(reason = SendApduFailureReason.EmptyResponse)
88
+ }
72
89
 
73
- SendApduResult.Success(apdu = apduResponse)
90
+ return@withContext SendApduResult.Success(apdu = apduResponse)
74
91
  }
92
+ } catch (e: SendApduTimeoutException) {
93
+ loggerService.log(
94
+ buildSimpleErrorLogInfo(
95
+ "AndroidUsbApduSender",
96
+ "timeout in send: $e"
97
+ )
98
+ )
99
+ SendApduResult.Failure(reason = SendApduFailureReason.AbortTimeout)
75
100
  } catch (e: NoSuchElementException) {
101
+ loggerService.log(
102
+ buildSimpleErrorLogInfo(
103
+ "AndroidUsbApduSender",
104
+ "no endpoint found: $e"
105
+ )
106
+ )
76
107
  SendApduResult.Failure(reason = SendApduFailureReason.NoUsbEndpointFound)
77
108
  } catch (e: Exception) {
78
109
  loggerService.log(buildSimpleErrorLogInfo("AndroidUsbApduSender", "error in send: $e"))
@@ -84,22 +115,27 @@ internal class AndroidUsbApduSender(
84
115
  androidToUsbEndpoint: UsbEndpoint,
85
116
  rawApdu: ByteArray,
86
117
  ) {
87
- framerService.serialize(mtu = USB_MTU, channelId = generateChannelId(), rawApdu = rawApdu).forEach { apduFrame ->
88
- val buffer = apduFrame.toByteArray()
89
- Timber.i("APDU sent = ${buffer.toHexadecimalString()}")
90
- usbConnection.bulkTransfer(androidToUsbEndpoint, buffer, apduFrame.size(), USB_TIMEOUT)
91
- }
118
+ framerService.serialize(mtu = USB_MTU, channelId = generateChannelId(), rawApdu = rawApdu)
119
+ .forEach { apduFrame ->
120
+ val buffer = apduFrame.toByteArray()
121
+ usbConnection.bulkTransfer(
122
+ androidToUsbEndpoint,
123
+ buffer,
124
+ apduFrame.size(),
125
+ USB_TIMEOUT
126
+ )
127
+ }
92
128
  }
93
129
 
94
130
  private fun receiveApdu(
95
131
  usbConnection: UsbDeviceConnection,
96
132
  usbToAndroidEndpoint: UsbEndpoint,
97
- ): ByteArray =
98
- if (!request.initialize(usbConnection, usbToAndroidEndpoint)) {
133
+ ): ByteArray {
134
+ return if (!request.initialize(usbConnection, usbToAndroidEndpoint)) {
99
135
  request.close()
100
136
  byteArrayOf()
101
137
  } else {
102
- val frames = framerService.createApduFrames(mtu = USB_MTU, isUsbTransport = true){
138
+ val frames = framerService.createApduFrames(mtu = USB_MTU, isUsbTransport = true) {
103
139
  val buffer = ByteArray(USB_MTU)
104
140
  val responseBuffer = ByteBuffer.allocate(USB_MTU)
105
141
 
@@ -107,8 +143,7 @@ internal class AndroidUsbApduSender(
107
143
  if (!queuingResult) {
108
144
  request.close()
109
145
  byteArrayOf()
110
- }
111
- else{
146
+ } else {
112
147
  usbConnection.requestWait()
113
148
  responseBuffer.rewind()
114
149
  responseBuffer.get(buffer, 0, responseBuffer.remaining())
@@ -117,6 +152,7 @@ internal class AndroidUsbApduSender(
117
152
  }
118
153
  framerService.deserialize(mtu = USB_MTU, frames)
119
154
  }
155
+ }
120
156
 
121
157
  private fun UsbInterface.firstEndpointOrThrow(predicate: (Int) -> Boolean): UsbEndpoint {
122
158
  for (endp in 0..this.endpointCount) {
@@ -129,5 +165,10 @@ internal class AndroidUsbApduSender(
129
165
  throw NoSuchElementException("No endpoint matching the predicate")
130
166
  }
131
167
 
132
- private fun generateChannelId(): ByteArray = Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
168
+ private fun generateChannelId(): ByteArray =
169
+ Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
170
+
171
+ private data object SendApduTimeoutException : Exception() {
172
+ private fun readResolve(): Any = SendApduTimeoutException
173
+ }
133
174
  }
@@ -6,8 +6,9 @@
6
6
  package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
7
7
 
8
8
  import com.ledger.devicesdk.shared.api.apdu.SendApduResult
9
+ import kotlin.time.Duration
9
10
 
10
11
  internal interface DeviceApduSender<Dependencies> {
11
- suspend fun send(apdu: ByteArray): SendApduResult
12
+ suspend fun send(apdu: ByteArray, abortTimeoutDuration: Duration): SendApduResult
12
13
  val dependencies: Dependencies
13
14
  }
@@ -24,9 +24,9 @@ internal class DeviceConnection<Dependencies>(
24
24
 
25
25
  init {
26
26
  stateMachine = DeviceConnectionStateMachine(
27
- sendApduFn = {
27
+ sendApduFn = { apdu, abortTimeoutDuration ->
28
28
  coroutineScope.launch {
29
- val res = deviceApduSender.send(it)
29
+ val res = deviceApduSender.send(apdu, abortTimeoutDuration)
30
30
  handleApduResult(res)
31
31
  }
32
32
  },
@@ -66,12 +66,13 @@ internal class DeviceConnection<Dependencies>(
66
66
  stateMachine.handleDeviceDisconnected()
67
67
  }
68
68
 
69
- public suspend fun requestSendApdu(apdu: ByteArray): SendApduResult =
69
+ public suspend fun requestSendApdu(apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration): SendApduResult =
70
70
  suspendCoroutine { cont ->
71
71
  stateMachine.requestSendApdu(
72
72
  DeviceConnectionStateMachine.SendApduRequestContent(
73
73
  apdu = apdu,
74
- triggersDisconnection = apduTriggersDisconnection(apdu),
74
+ triggersDisconnection = apduTriggersDisconnection(apdu) || triggersDisconnection,
75
+ abortTimeoutDuration = abortTimeoutDuration,
75
76
  resultCallback = cont::resume
76
77
  )
77
78
  )
@@ -12,7 +12,7 @@ import kotlinx.coroutines.launch
12
12
  import kotlin.time.Duration
13
13
 
14
14
  internal class DeviceConnectionStateMachine(
15
- private val sendApduFn: (apdu: ByteArray) -> Unit,
15
+ private val sendApduFn: (apdu: ByteArray, abortTimeoutDuration: Duration) -> Unit,
16
16
  private val onTerminated: () -> Unit,
17
17
  private val isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
18
18
  private val reconnectionTimeoutDuration: Duration,
@@ -29,7 +29,7 @@ internal class DeviceConnectionStateMachine(
29
29
  when (newState) {
30
30
  is State.Connected -> {}
31
31
  is State.SendingApdu -> {
32
- sendApduFn(newState.requestContent.apdu)
32
+ sendApduFn(newState.requestContent.apdu, newState.requestContent.abortTimeoutDuration)
33
33
  }
34
34
 
35
35
  is State.WaitingForReconnection -> {
@@ -240,7 +240,7 @@ internal class DeviceConnectionStateMachine(
240
240
  -> Event: $event
241
241
  -> New state: $state
242
242
  """.trimIndent()
243
- loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
243
+ // loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
244
244
  }
245
245
 
246
246
  private var timeoutJob: Job? = null
@@ -280,6 +280,7 @@ internal class DeviceConnectionStateMachine(
280
280
  data class SendApduRequestContent(
281
281
  val apdu: ByteArray,
282
282
  val triggersDisconnection: Boolean,
283
+ val abortTimeoutDuration: Duration,
283
284
  val resultCallback: (SendApduResult) -> Unit
284
285
  )
285
286
 
@@ -44,4 +44,8 @@ public sealed class SendApduFailureReason {
44
44
  public data object DeviceDisconnected : SendApduFailureReason()
45
45
 
46
46
  public data object Unknown : SendApduFailureReason()
47
+
48
+ public data object AbortTimeout : SendApduFailureReason()
49
+
50
+ public data object EmptyResponse : SendApduFailureReason()
47
51
  }
@@ -8,7 +8,7 @@ package com.ledger.devicesdk.shared.api.discovery
8
8
  import com.ledger.devicesdk.shared.api.device.LedgerDevice
9
9
  import kotlinx.datetime.Clock
10
10
 
11
- public class DiscoveryDevice(
11
+ public data class DiscoveryDevice(
12
12
  public val uid: String,
13
13
  public val name: String,
14
14
  public val ledgerDevice: LedgerDevice,
@@ -9,5 +9,5 @@ internal data class InternalConnectedDevice(
9
9
  val name: String,
10
10
  val ledgerDevice: LedgerDevice,
11
11
  val connectivity: ConnectivityType,
12
- val sendApduFn: suspend (ByteArray) -> SendApduResult,
12
+ val sendApduFn: suspend (apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: kotlin.time.Duration) -> SendApduResult,
13
13
  )