@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rnhid-transport-20250411151739 → 0.0.0-test-ble-20251112111541

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 (72) hide show
  1. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +60 -20
  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 +119 -34
  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/index.js +1 -1
  34. package/lib/cjs/index.js.map +3 -3
  35. package/lib/cjs/package.json +20 -15
  36. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  37. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  38. package/lib/esm/api/bridge/NativeTransportModule.js +1 -1
  39. package/lib/esm/api/bridge/NativeTransportModule.js.map +1 -1
  40. package/lib/esm/api/bridge/mapper.js +1 -1
  41. package/lib/esm/api/bridge/mapper.js.map +3 -3
  42. package/lib/esm/api/bridge/mapper.test.js +1 -1
  43. package/lib/esm/api/bridge/mapper.test.js.map +3 -3
  44. package/lib/esm/api/bridge/types.js.map +1 -1
  45. package/lib/esm/api/transport/Errors.js +1 -1
  46. package/lib/esm/api/transport/Errors.js.map +3 -3
  47. package/lib/esm/api/transport/RNHidTransport.js +1 -1
  48. package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
  49. package/lib/esm/api/transport/RNHidTransport.test.js +1 -1
  50. package/lib/esm/api/transport/RNHidTransport.test.js.map +3 -3
  51. package/lib/esm/index.js +1 -1
  52. package/lib/esm/index.js.map +3 -3
  53. package/lib/esm/package.json +20 -15
  54. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts +1 -1
  55. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts.map +1 -1
  56. package/lib/types/api/bridge/NativeTransportModule.d.ts.map +1 -1
  57. package/lib/types/api/bridge/mapper.d.ts.map +1 -1
  58. package/lib/types/api/bridge/types.d.ts +2 -2
  59. package/lib/types/api/bridge/types.d.ts.map +1 -1
  60. package/lib/types/api/transport/Errors.d.ts +1 -1
  61. package/lib/types/api/transport/Errors.d.ts.map +1 -1
  62. package/lib/types/api/transport/NativeModuleWrapper.d.ts +1 -1
  63. package/lib/types/api/transport/NativeModuleWrapper.d.ts.map +1 -1
  64. package/lib/types/api/transport/RNHidTransport.d.ts.map +1 -1
  65. package/lib/types/index.d.ts +1 -0
  66. package/lib/types/index.d.ts.map +1 -1
  67. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  68. package/package.json +22 -17
  69. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  70. package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  71. package/android/gradlew +0 -252
  72. package/android/gradlew.bat +0 -94
@@ -17,11 +17,14 @@ import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.ACTION_U
17
17
  import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbAttachedReceiverController
18
18
  import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbDetachedReceiverController
19
19
  import com.ledger.devicesdk.shared.androidMain.transport.usb.controller.UsbPermissionReceiver
20
+ import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
21
+ import com.ledger.devicesdk.shared.api.apdu.SendApduResult
20
22
  import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
21
23
  import com.ledger.devicesdk.shared.internal.connection.InternalConnectedDevice
22
24
  import com.ledger.devicesdk.shared.internal.connection.InternalConnectionResult
23
25
  import com.ledger.devicesdk.shared.internal.event.SdkEventDispatcher
24
26
  import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
27
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
25
28
  import com.ledger.devicesdk.shared.internal.transport.TransportEvent
26
29
  import kotlinx.coroutines.CoroutineScope
27
30
  import kotlinx.coroutines.Dispatchers
@@ -31,14 +34,17 @@ import kotlinx.coroutines.flow.onEach
31
34
  import kotlinx.coroutines.launch
32
35
  import timber.log.Timber
33
36
  import kotlin.random.Random
37
+ import kotlin.time.Duration
34
38
  import kotlin.time.Duration.Companion.milliseconds
35
39
 
40
+ private val TAG = "TransportHidModule"
41
+
36
42
  class TransportHidModule(
37
43
  private val reactContext: ReactApplicationContext,
38
44
  private val coroutineScope: CoroutineScope
39
45
  ) :
40
46
  ReactContextBaseJavaModule(reactContext), LifecycleEventListener {
41
- override fun getName(): String = "RCTTransportHIDModule"
47
+ override fun getName(): String = "LDMKTransportHIDModule"
42
48
 
43
49
  private var usbPermissionReceiver: UsbPermissionReceiver? = null
44
50
  private var usbAttachedReceiverController: UsbAttachedReceiverController? = null
@@ -47,8 +53,8 @@ class TransportHidModule(
47
53
  private var eventDispatcherListeningJob: Job
48
54
  private val loggerService: LoggerService =
49
55
  LoggerService { info ->
50
- Timber.tag("RNHIDModule " + info.tag).d(info.message)
51
- sendEvent(reactContext, BridgeEvents.TransportLog(info))
56
+ Timber.tag("LDMKTransportHIDModule " + info.tag).d(info.message)
57
+ // sendEvent(reactContext, BridgeEvents.TransportLog(info))
52
58
  }
53
59
 
54
60
  private val transport: AndroidUsbTransport? by lazy {
@@ -132,12 +138,18 @@ class TransportHidModule(
132
138
  transport?.stopScan()
133
139
  }
134
140
 
135
- private var discoveryCount = 0
141
+ private var activeScanCount = 0
136
142
 
137
143
  @ReactMethod
138
144
  fun startScan(promise: Promise) {
139
- discoveryCount += 1
140
- if (discoveryCount > 1) {
145
+ loggerService.log(
146
+ buildSimpleDebugLogInfo(TAG, "[startScan] called")
147
+ )
148
+ activeScanCount += 1
149
+ if (activeScanCount > 1) {
150
+ loggerService.log(
151
+ buildSimpleDebugLogInfo(TAG, "[startScan] already scanning")
152
+ )
141
153
  promise.resolve(null)
142
154
  return
143
155
  }
@@ -155,16 +167,34 @@ class TransportHidModule(
155
167
 
156
168
  @ReactMethod
157
169
  fun stopScan(promise: Promise) {
158
- discoveryCount -= 1
159
- if (discoveryCount > 0) {
160
- promise.resolve(null)
161
- return
162
- }
163
- try {
164
- transport!!.stopScan()
165
- promise.resolve(null)
166
- } catch (e: Exception) {
167
- promise.reject(e);
170
+ loggerService.log(
171
+ buildSimpleDebugLogInfo(TAG, "[stopScan] called, activeScanCount=$activeScanCount")
172
+ )
173
+
174
+ when(activeScanCount) {
175
+ 0 -> {
176
+ loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] no active scan"))
177
+ promise.resolve(null)
178
+ }
179
+ 1 -> {
180
+ try {
181
+ transport!!.stopScan()
182
+ promise.resolve(null)
183
+ } catch (e: Exception) {
184
+ promise.reject(e);
185
+ }
186
+ activeScanCount = 0
187
+ }
188
+ else -> {
189
+ loggerService.log(
190
+ buildSimpleDebugLogInfo(
191
+ TAG,
192
+ "[stopScan] still scanning because there are active listeners"
193
+ )
194
+ )
195
+ activeScanCount -= 1
196
+ promise.resolve(null)
197
+ }
168
198
  }
169
199
  }
170
200
 
@@ -206,17 +236,27 @@ class TransportHidModule(
206
236
  }
207
237
 
208
238
  @ReactMethod
209
- fun sendApdu(sessionId: String, apduBase64: String, promise: Promise) {
239
+ fun sendApdu(
240
+ sessionId: String,
241
+ apduBase64: String,
242
+ triggersDisconnection: Boolean,
243
+ abortTimeout: Int,
244
+ promise: Promise
245
+ ) {
210
246
  try {
211
247
  val device = connectedDevices.firstOrNull() { it.id == sessionId }
212
248
  if (device == null) {
213
- promise.reject(Exception("[TransportHidModule][sendApdu] Device not found"))
249
+ promise.resolve(
250
+ SendApduResult.Failure(SendApduFailureReason.DeviceNotFound).toWritableMap()
251
+ )
214
252
  return
215
253
  }
216
254
  coroutineScope.launch {
217
255
  try {
218
256
  val apdu: ByteArray = Base64.decode(apduBase64, Base64.DEFAULT)
219
- val res = device.sendApduFn(apdu)
257
+ val abortTimeoutDuration = if (abortTimeout <= 0) Duration.INFINITE else abortTimeout.milliseconds
258
+ val res =
259
+ device.sendApduFn(apdu, triggersDisconnection, abortTimeoutDuration)
220
260
  promise.resolve(res.toWritableMap())
221
261
  } catch (e: Exception) {
222
262
  Timber.i("$e, ${e.cause}")
@@ -238,4 +278,4 @@ class TransportHidModule(
238
278
  fun removeListeners(count: Int) {
239
279
  // Nothing to do in our case, but React Native will issue a warning if this isn't implemented
240
280
  }
241
- }
281
+ }
@@ -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
  }
@@ -43,8 +43,11 @@ import kotlinx.coroutines.flow.shareIn
43
43
  import kotlinx.coroutines.isActive
44
44
  import kotlinx.coroutines.launch
45
45
  import kotlin.time.Duration
46
+ import kotlin.time.Duration.Companion.milliseconds
46
47
  import kotlin.time.Duration.Companion.seconds
47
48
 
49
+ private val TAG = "DefaultAndroidUsbTransport"
50
+
48
51
  internal class DefaultAndroidUsbTransport(
49
52
  private val application: Application,
50
53
  private val usbManager: UsbManager,
@@ -67,22 +70,16 @@ internal class DefaultAndroidUsbTransport(
67
70
  private var discoveryJob: Job? = null
68
71
 
69
72
  override fun startScan(): Flow<List<DiscoveryDevice>> {
73
+ loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] called"))
70
74
  val scanStateFlow = MutableStateFlow<List<DiscoveryDevice>>(emptyList())
71
75
  discoveryJob?.cancel()
72
76
  discoveryJob =
73
77
  scope.launch {
74
78
  while (isActive) {
75
- 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
-
79
+ loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] isActive loop"))
80
+ val usbDevices = usbManager.deviceList.values.toList().toUsbDevices()
81
+ scanStateFlow.value = usbDevices.toScannedDevices()
82
+ loggerService.log(buildSimpleDebugLogInfo(TAG, "[startScan] scannedDevices=${scanStateFlow.value}"))
86
83
  delay(scanDelay)
87
84
  }
88
85
  }
@@ -90,6 +87,7 @@ internal class DefaultAndroidUsbTransport(
90
87
  }
91
88
 
92
89
  override fun stopScan() {
90
+ loggerService.log(buildSimpleDebugLogInfo(TAG, "[stopScan] called"))
93
91
  discoveryJob?.cancel()
94
92
  discoveryJob = null
95
93
  }
@@ -97,31 +95,57 @@ internal class DefaultAndroidUsbTransport(
97
95
  override fun updateUsbState(state: UsbState) {
98
96
  when (state) {
99
97
  is UsbState.Detached -> {
100
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Detached deviceId=${state.ledgerUsbDevice.uid}"))
98
+ loggerService.log(
99
+ buildSimpleDebugLogInfo(
100
+ "AndroidUsbTransport",
101
+ "Detached deviceId=${state.ledgerUsbDevice.uid}"
102
+ )
103
+ )
101
104
  usbConnections.entries.find {
102
105
  it.value.getApduSender().dependencies.ledgerUsbDevice.uid == state.ledgerUsbDevice.uid
103
106
  }.let { item ->
104
107
  scope.launch {
105
108
  if (item == null) {
106
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No connection found"))
109
+ loggerService.log(
110
+ buildSimpleWarningLogInfo(
111
+ "AndroidUsbTransport",
112
+ "No connection found"
113
+ )
114
+ )
107
115
  return@launch
108
116
  }
109
117
  val (key, deviceConnection) = item
110
- loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Device disconnected (sessionId=${deviceConnection.sessionId})"))
111
- deviceConnection.handleDeviceDisconnected()
118
+ loggerService.log(
119
+ buildSimpleInfoLogInfo(
120
+ "AndroidUsbTransport",
121
+ "Device disconnected (sessionId=${deviceConnection.sessionId})"
122
+ )
123
+ )
112
124
  usbConnections.remove(key)
113
125
  usbConnectionsPendingReconnection.add(deviceConnection)
126
+ deviceConnection.handleDeviceDisconnected()
127
+ (deviceConnection.getApduSender() as AndroidUsbApduSender).release()
114
128
  }
115
129
  }
116
130
  }
117
131
 
118
132
  is UsbState.Attached -> {
119
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Attached deviceId=${state.ledgerUsbDevice.uid}, pendingReconnections=${usbConnectionsPendingReconnection}"))
133
+ loggerService.log(
134
+ buildSimpleDebugLogInfo(
135
+ "AndroidUsbTransport",
136
+ "Attached deviceId=${state.ledgerUsbDevice.uid}, pendingReconnections=${usbConnectionsPendingReconnection}"
137
+ )
138
+ )
120
139
  val usbDevice = usbManager.deviceList.values.firstOrNull {
121
140
  it.toLedgerUsbDevice()?.uid == state.ledgerUsbDevice.uid
122
141
  }
123
142
  if (usbDevice == null) {
124
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No UsbDevice found"))
143
+ loggerService.log(
144
+ buildSimpleWarningLogInfo(
145
+ "AndroidUsbTransport",
146
+ "No UsbDevice found"
147
+ )
148
+ )
125
149
  return
126
150
  }
127
151
  usbConnectionsPendingReconnection.firstOrNull {
@@ -138,27 +162,54 @@ internal class DefaultAndroidUsbTransport(
138
162
  )
139
163
  return@launch
140
164
  }
141
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Found matching device connection $deviceConnection"))
165
+ loggerService.log(
166
+ buildSimpleDebugLogInfo(
167
+ "AndroidUsbTransport",
168
+ "Found matching device connection $deviceConnection"
169
+ )
170
+ )
142
171
 
143
172
  val permissionResult = checkOrRequestPermission(usbDevice)
144
173
  if (permissionResult is PermissionResult.Denied) {
145
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Permission denied"))
174
+ loggerService.log(
175
+ buildSimpleDebugLogInfo(
176
+ "AndroidUsbTransport",
177
+ "Permission denied"
178
+ )
179
+ )
146
180
  return@launch
147
181
  }
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
182
+ loggerService.log(
183
+ buildSimpleInfoLogInfo(
184
+ "AndroidUsbTransport",
185
+ "Reconnecting device (sessionId=${deviceConnection.sessionId})"
160
186
  )
161
187
  )
188
+
189
+ val apduSender = AndroidUsbApduSender(
190
+ dependencies = AndroidUsbApduSender.Dependencies(
191
+ usbDevice = usbDevice,
192
+ ledgerUsbDevice = state.ledgerUsbDevice,
193
+ ),
194
+ usbManager = usbManager,
195
+ ioDispatcher = Dispatchers.IO,
196
+ framerService = FramerService(loggerService),
197
+ request = UsbRequest(),
198
+ loggerService = loggerService
199
+ )
200
+ delay(POST_CONNECTION_DELAY)
201
+
202
+ if (!usbConnectionsPendingReconnection.contains(deviceConnection)) {
203
+ /**
204
+ * We check this because maybe by the time we get here,
205
+ * the reconnection has timed out and the session has been terminated.
206
+ * Easy to reproduce for instance if the permission request requires
207
+ * a user interaction.
208
+ */
209
+ apduSender.release()
210
+ return@launch
211
+ }
212
+ deviceConnection.handleDeviceConnected(apduSender)
162
213
  usbConnectionsPendingReconnection.remove(deviceConnection)
163
214
  usbConnections[deviceConnection.sessionId] = deviceConnection
164
215
  }
@@ -195,7 +246,12 @@ internal class DefaultAndroidUsbTransport(
195
246
  device = usbDevice,
196
247
  )
197
248
 
198
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Waiting for permission result"))
249
+ loggerService.log(
250
+ buildSimpleDebugLogInfo(
251
+ "AndroidUsbTransport",
252
+ "Waiting for permission result"
253
+ )
254
+ )
199
255
 
200
256
  val result = eventsFlow.first {
201
257
  it is UsbPermissionEvent.PermissionGranted ||
@@ -237,6 +293,27 @@ internal class DefaultAndroidUsbTransport(
237
293
  return if (usbDevice == null || ledgerUsbDevice == null) {
238
294
  InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
239
295
  } else {
296
+
297
+ val existingConnection = usbConnections.firstNotNullOfOrNull {
298
+ if (it.value.getApduSender().dependencies.usbDevice == usbDevice) it.value
299
+ else if (it.key == generateSessionId(usbDevice)) it.value
300
+ else null
301
+ }
302
+
303
+ if (existingConnection != null) {
304
+ val connectedDevice =
305
+ InternalConnectedDevice(
306
+ existingConnection.sessionId,
307
+ discoveryDevice.name,
308
+ discoveryDevice.ledgerDevice,
309
+ discoveryDevice.connectivityType,
310
+ sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
311
+ existingConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
312
+ }
313
+ )
314
+ return InternalConnectionResult.Connected(device = connectedDevice, sessionId = existingConnection.sessionId)
315
+ }
316
+
240
317
  val permissionResult = checkOrRequestPermission(usbDevice)
241
318
  if (permissionResult is PermissionResult.Denied) {
242
319
  return permissionResult.connectionError
@@ -255,13 +332,15 @@ internal class DefaultAndroidUsbTransport(
255
332
  request = UsbRequest(),
256
333
  loggerService = loggerService,
257
334
  )
335
+ delay(POST_CONNECTION_DELAY)
258
336
 
259
337
  val deviceConnection = DeviceConnection(
260
338
  sessionId = sessionId,
261
339
  deviceApduSender = apduSender,
262
- isFatalSendApduFailure = { false }, // TODO: refine this
340
+ isFatalSendApduFailure = { false },
263
341
  reconnectionTimeoutDuration = 5.seconds,
264
342
  onTerminated = {
343
+ (it.getApduSender() as AndroidUsbApduSender).release()
265
344
  usbConnections.remove(sessionId)
266
345
  usbConnectionsPendingReconnection.remove(it)
267
346
  eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
@@ -276,7 +355,9 @@ internal class DefaultAndroidUsbTransport(
276
355
  discoveryDevice.name,
277
356
  discoveryDevice.ledgerDevice,
278
357
  discoveryDevice.connectivityType,
279
- sendApduFn = { apdu -> deviceConnection.requestSendApdu(apdu) },
358
+ sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
359
+ deviceConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
360
+ }
280
361
  )
281
362
 
282
363
  usbConnections[sessionId] = deviceConnection
@@ -286,12 +367,16 @@ internal class DefaultAndroidUsbTransport(
286
367
  }
287
368
 
288
369
  override suspend fun disconnect(deviceId: String) {
370
+ // The DeviceConnection is either in usbConnections or usbConnectionsPendingReconnection
289
371
  usbConnections[deviceId]?.requestCloseConnection()
372
+ usbConnectionsPendingReconnection.find { it.sessionId == deviceId }?.requestCloseConnection()
290
373
  }
291
374
 
292
375
  private fun generateSessionId(usbDevice: UsbDevice): String = "usb_${usbDevice.deviceId}"
293
376
  }
294
377
 
378
+ private val POST_CONNECTION_DELAY = 200.milliseconds
379
+
295
380
  private fun List<LedgerUsbDevice>.toScannedDevices(): List<DiscoveryDevice> =
296
381
  this.map {
297
382
  it.toScannedDevice()
@@ -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
  )