@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rnhid-transport-20250411151739 → 0.0.0-transactionInspector-resolutionObj-20250916071327

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 (66) hide show
  1. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +15 -6
  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 +85 -23
  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/androidMain/transport/usb/utils/UsbDeviceMapper.kt +3 -0
  6. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +2 -1
  7. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +5 -4
  8. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +103 -33
  9. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +4 -0
  10. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +16 -0
  11. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +1 -1
  12. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +1 -1
  13. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +189 -46
  14. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +9 -5
  15. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  16. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  17. package/lib/cjs/api/bridge/NativeTransportModule.js +1 -1
  18. package/lib/cjs/api/bridge/NativeTransportModule.js.map +1 -1
  19. package/lib/cjs/api/bridge/mapper.js +1 -1
  20. package/lib/cjs/api/bridge/mapper.js.map +2 -2
  21. package/lib/cjs/api/bridge/mapper.test.js +1 -1
  22. package/lib/cjs/api/bridge/mapper.test.js.map +2 -2
  23. package/lib/cjs/api/bridge/types.js +1 -1
  24. package/lib/cjs/api/bridge/types.js.map +1 -1
  25. package/lib/cjs/api/transport/Errors.js +1 -1
  26. package/lib/cjs/api/transport/Errors.js.map +3 -3
  27. package/lib/cjs/api/transport/NativeModuleWrapper.js +1 -1
  28. package/lib/cjs/api/transport/NativeModuleWrapper.js.map +1 -1
  29. package/lib/cjs/api/transport/RNHidTransport.js +1 -1
  30. package/lib/cjs/api/transport/RNHidTransport.js.map +3 -3
  31. package/lib/cjs/api/transport/RNHidTransport.test.js +1 -1
  32. package/lib/cjs/api/transport/RNHidTransport.test.js.map +2 -2
  33. package/lib/cjs/package.json +19 -14
  34. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  35. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  36. package/lib/esm/api/bridge/NativeTransportModule.js +1 -1
  37. package/lib/esm/api/bridge/NativeTransportModule.js.map +1 -1
  38. package/lib/esm/api/bridge/mapper.js +1 -1
  39. package/lib/esm/api/bridge/mapper.js.map +3 -3
  40. package/lib/esm/api/bridge/mapper.test.js +1 -1
  41. package/lib/esm/api/bridge/mapper.test.js.map +3 -3
  42. package/lib/esm/api/bridge/types.js.map +1 -1
  43. package/lib/esm/api/transport/Errors.js +1 -1
  44. package/lib/esm/api/transport/Errors.js.map +3 -3
  45. package/lib/esm/api/transport/RNHidTransport.js +1 -1
  46. package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
  47. package/lib/esm/api/transport/RNHidTransport.test.js +1 -1
  48. package/lib/esm/api/transport/RNHidTransport.test.js.map +3 -3
  49. package/lib/esm/package.json +19 -14
  50. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts +1 -1
  51. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts.map +1 -1
  52. package/lib/types/api/bridge/NativeTransportModule.d.ts.map +1 -1
  53. package/lib/types/api/bridge/mapper.d.ts.map +1 -1
  54. package/lib/types/api/bridge/types.d.ts +2 -2
  55. package/lib/types/api/bridge/types.d.ts.map +1 -1
  56. package/lib/types/api/transport/Errors.d.ts +1 -1
  57. package/lib/types/api/transport/Errors.d.ts.map +1 -1
  58. package/lib/types/api/transport/NativeModuleWrapper.d.ts +1 -1
  59. package/lib/types/api/transport/NativeModuleWrapper.d.ts.map +1 -1
  60. package/lib/types/api/transport/RNHidTransport.d.ts.map +1 -1
  61. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  62. package/package.json +23 -18
  63. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  64. package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  65. package/android/gradlew +0 -252
  66. package/android/gradlew.bat +0 -94
@@ -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(
@@ -38,7 +39,7 @@ class TransportHidModule(
38
39
  private val coroutineScope: CoroutineScope
39
40
  ) :
40
41
  ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
41
- override fun getName(): String = "RCTTransportHIDModule"
42
+ override fun getName(): String = "LDMKTransportHIDModule"
42
43
 
43
44
  private var usbPermissionReceiver: UsbPermissionReceiver? = null
44
45
  private var usbAttachedReceiverController: UsbAttachedReceiverController? = null
@@ -47,8 +48,8 @@ class TransportHidModule(
47
48
  private var eventDispatcherListeningJob: Job
48
49
  private val loggerService: LoggerService =
49
50
  LoggerService { info ->
50
- Timber.tag("RNHIDModule " + info.tag).d(info.message)
51
- sendEvent(reactContext, BridgeEvents.TransportLog(info))
51
+ Timber.tag("LDMKTransportHIDModule " + info.tag).d(info.message)
52
+ // sendEvent(reactContext, BridgeEvents.TransportLog(info))
52
53
  }
53
54
 
54
55
  private val transport: AndroidUsbTransport? by lazy {
@@ -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}")
@@ -238,4 +247,4 @@ class TransportHidModule(
238
247
  fun removeListeners(count: Int) {
239
248
  // Nothing to do in our case, but React Native will issue a warning if this isn't implemented
240
249
  }
241
- }
250
+ }
@@ -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,27 +164,53 @@ 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})"))
149
- deviceConnection.handleDeviceConnected(
150
- AndroidUsbApduSender(
151
- dependencies = AndroidUsbApduSender.Dependencies(
152
- usbDevice = usbDevice,
153
- ledgerUsbDevice = state.ledgerUsbDevice,
154
- ),
155
- usbManager = usbManager,
156
- ioDispatcher = Dispatchers.IO,
157
- framerService = FramerService(loggerService),
158
- request = UsbRequest(),
159
- loggerService = loggerService
184
+ loggerService.log(
185
+ buildSimpleInfoLogInfo(
186
+ "AndroidUsbTransport",
187
+ "Reconnecting device (sessionId=${deviceConnection.sessionId})"
160
188
  )
161
189
  )
190
+
191
+ val apduSender = AndroidUsbApduSender(
192
+ dependencies = AndroidUsbApduSender.Dependencies(
193
+ usbDevice = usbDevice,
194
+ ledgerUsbDevice = state.ledgerUsbDevice,
195
+ ),
196
+ usbManager = usbManager,
197
+ ioDispatcher = Dispatchers.IO,
198
+ framerService = FramerService(loggerService),
199
+ request = UsbRequest(),
200
+ loggerService = loggerService
201
+ )
202
+
203
+ if (!usbConnectionsPendingReconnection.contains(deviceConnection)) {
204
+ /**
205
+ * We check this because maybe by the time we get here,
206
+ * the reconnection has timed out and the session has been terminated.
207
+ * Easy to reproduce for instance if the permission request requires
208
+ * a user interaction.
209
+ */
210
+ apduSender.release()
211
+ return@launch
212
+ }
213
+ deviceConnection.handleDeviceConnected(apduSender)
162
214
  usbConnectionsPendingReconnection.remove(deviceConnection)
163
215
  usbConnections[deviceConnection.sessionId] = deviceConnection
164
216
  }
@@ -195,7 +247,12 @@ internal class DefaultAndroidUsbTransport(
195
247
  device = usbDevice,
196
248
  )
197
249
 
198
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Waiting for permission result"))
250
+ loggerService.log(
251
+ buildSimpleDebugLogInfo(
252
+ "AndroidUsbTransport",
253
+ "Waiting for permission result"
254
+ )
255
+ )
199
256
 
200
257
  val result = eventsFlow.first {
201
258
  it is UsbPermissionEvent.PermissionGranted ||
@@ -259,9 +316,10 @@ internal class DefaultAndroidUsbTransport(
259
316
  val deviceConnection = DeviceConnection(
260
317
  sessionId = sessionId,
261
318
  deviceApduSender = apduSender,
262
- isFatalSendApduFailure = { false }, // TODO: refine this
319
+ isFatalSendApduFailure = { false },
263
320
  reconnectionTimeoutDuration = 5.seconds,
264
321
  onTerminated = {
322
+ (it.getApduSender() as AndroidUsbApduSender).release()
265
323
  usbConnections.remove(sessionId)
266
324
  usbConnectionsPendingReconnection.remove(it)
267
325
  eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
@@ -276,7 +334,9 @@ internal class DefaultAndroidUsbTransport(
276
334
  discoveryDevice.name,
277
335
  discoveryDevice.ledgerDevice,
278
336
  discoveryDevice.connectivityType,
279
- sendApduFn = { apdu -> deviceConnection.requestSendApdu(apdu) },
337
+ sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
338
+ deviceConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
339
+ }
280
340
  )
281
341
 
282
342
  usbConnections[sessionId] = deviceConnection
@@ -286,7 +346,9 @@ internal class DefaultAndroidUsbTransport(
286
346
  }
287
347
 
288
348
  override suspend fun disconnect(deviceId: String) {
349
+ // The DeviceConnection is either in usbConnections or usbConnectionsPendingReconnection
289
350
  usbConnections[deviceId]?.requestCloseConnection()
351
+ usbConnectionsPendingReconnection.find { it.sessionId == deviceId }?.requestCloseConnection()
290
352
  }
291
353
 
292
354
  private fun generateSessionId(usbDevice: UsbDevice): String = "usb_${usbDevice.deviceId}"
@@ -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
  }
@@ -25,6 +25,9 @@ internal fun ProductId.toLedgerDevice(): LedgerDevice? =
25
25
  this.id.isLedgerDeviceProductId(LedgerDevice.Flex) -> {
26
26
  LedgerDevice.Flex
27
27
  }
28
+ this.id.isLedgerDeviceProductId(LedgerDevice.Apex) -> {
29
+ LedgerDevice.Apex
30
+ }
28
31
  else -> {
29
32
  null
30
33
  }
@@ -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
  )