@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rn-hid-20250221115747 → 0.0.0-rn-hid-sync-onboarding-behavior-20250516092329

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 (59) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +3 -1
  3. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +14 -5
  4. package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +2 -0
  5. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +75 -32
  6. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +72 -32
  7. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +1 -1
  8. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +27 -36
  9. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +1 -1
  10. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +2 -1
  11. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +11 -11
  12. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +8 -4
  13. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +4 -0
  14. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +64 -49
  15. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +4 -3
  16. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +1 -1
  17. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +1 -1
  18. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
  19. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
  20. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  21. package/lib/cjs/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  22. package/lib/cjs/api/bridge/NativeTransportModule.js +1 -1
  23. package/lib/cjs/api/bridge/NativeTransportModule.js.map +1 -1
  24. package/lib/cjs/api/bridge/mapper.js +1 -1
  25. package/lib/cjs/api/bridge/mapper.js.map +2 -2
  26. package/lib/cjs/api/bridge/types.js +1 -1
  27. package/lib/cjs/api/bridge/types.js.map +1 -1
  28. package/lib/cjs/api/transport/NativeModuleWrapper.js +1 -1
  29. package/lib/cjs/api/transport/NativeModuleWrapper.js.map +1 -1
  30. package/lib/cjs/api/transport/RNHidTransport.js +1 -1
  31. package/lib/cjs/api/transport/RNHidTransport.js.map +3 -3
  32. package/lib/cjs/package.json +17 -12
  33. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js +1 -1
  34. package/lib/esm/api/bridge/DefaultNativeModuleWrapper.js.map +3 -3
  35. package/lib/esm/api/bridge/NativeTransportModule.js +1 -1
  36. package/lib/esm/api/bridge/NativeTransportModule.js.map +1 -1
  37. package/lib/esm/api/bridge/mapper.js +1 -1
  38. package/lib/esm/api/bridge/mapper.js.map +3 -3
  39. package/lib/esm/api/bridge/types.js.map +1 -1
  40. package/lib/esm/api/transport/RNHidTransport.js +1 -1
  41. package/lib/esm/api/transport/RNHidTransport.js.map +3 -3
  42. package/lib/esm/package.json +17 -12
  43. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts +1 -1
  44. package/lib/types/api/bridge/DefaultNativeModuleWrapper.d.ts.map +1 -1
  45. package/lib/types/api/bridge/NativeTransportModule.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/NativeModuleWrapper.d.ts +1 -1
  50. package/lib/types/api/transport/NativeModuleWrapper.d.ts.map +1 -1
  51. package/lib/types/api/transport/RNHidTransport.d.ts.map +1 -1
  52. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  53. package/package.json +19 -14
  54. package/android/.settings/org.eclipse.buildship.core.prefs +0 -13
  55. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  56. package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  57. package/android/gradlew +0 -252
  58. package/android/gradlew.bat +0 -94
  59. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/DeviceAction.kt +0 -23
package/README.md CHANGED
@@ -35,7 +35,7 @@ The transport itself is only compatible with Android devices but there is no add
35
35
 
36
36
  ### Pre-requisites
37
37
 
38
- To use this transport, ensure you have the Device Magement Kit installed in your project.
38
+ To use this transport, ensure you have the Device Management Kit installed in your project.
39
39
 
40
40
  #### AndroidManifest.xml
41
41
 
@@ -93,7 +93,9 @@ dependencies {
93
93
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_stdlib_version"
94
94
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
95
95
  implementation "org.jetbrains.kotlinx:kotlinx-datetime:$kotlinx_datetime_version"
96
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_test_version"
97
96
  implementation "com.jakewharton.timber:timber:$timber_version"
98
97
  implementation "com.ditchoom:buffer:$buffer_version"
98
+ testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
99
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_test_version"
100
+ testImplementation "junit:junit:4.13.2"
99
101
  }
@@ -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,7 +48,7 @@ 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
+ Timber.tag("LDMKTransportHIDModule " + info.tag).d(info.message)
51
52
  sendEvent(reactContext, BridgeEvents.TransportLog(info))
52
53
  }
53
54
 
@@ -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
  }
@@ -9,13 +9,6 @@ import android.app.Application
9
9
  import android.hardware.usb.UsbDevice
10
10
  import android.hardware.usb.UsbManager
11
11
  import android.hardware.usb.UsbRequest
12
- import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
13
- import com.ledger.devicesdk.shared.internal.connection.InternalConnectedDevice
14
- import com.ledger.devicesdk.shared.internal.connection.InternalConnectionResult
15
- import com.ledger.devicesdk.shared.internal.event.SdkEventDispatcher
16
- import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
17
- import com.ledger.devicesdk.shared.internal.transport.TransportEvent
18
- import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
19
12
  import com.ledger.devicesdk.shared.androidMain.transport.usb.connection.AndroidUsbApduSender
20
13
  import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
21
14
  import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbPermissionEvent
@@ -24,11 +17,16 @@ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toLedgerUsbDe
24
17
  import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toScannedDevice
25
18
  import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toUsbDevices
26
19
  import com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection.DeviceConnection
20
+ import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
21
+ import com.ledger.devicesdk.shared.internal.connection.InternalConnectedDevice
22
+ import com.ledger.devicesdk.shared.internal.connection.InternalConnectionResult
23
+ import com.ledger.devicesdk.shared.internal.event.SdkEventDispatcher
24
+ import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
27
25
  import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
28
- import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
29
26
  import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
30
27
  import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleWarningLogInfo
31
- import kotlin.time.Duration
28
+ import com.ledger.devicesdk.shared.internal.transport.TransportEvent
29
+ import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
32
30
  import kotlinx.coroutines.CoroutineDispatcher
33
31
  import kotlinx.coroutines.CoroutineScope
34
32
  import kotlinx.coroutines.Dispatchers
@@ -41,11 +39,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
41
39
  import kotlinx.coroutines.flow.SharingStarted
42
40
  import kotlinx.coroutines.flow.first
43
41
  import kotlinx.coroutines.flow.merge
44
- import kotlinx.coroutines.flow.onStart
45
42
  import kotlinx.coroutines.flow.shareIn
46
43
  import kotlinx.coroutines.isActive
47
44
  import kotlinx.coroutines.launch
48
- import kotlin.math.log
45
+ import kotlin.time.Duration
49
46
  import kotlin.time.Duration.Companion.seconds
50
47
 
51
48
  internal class DefaultAndroidUsbTransport(
@@ -55,25 +52,23 @@ internal class DefaultAndroidUsbTransport(
55
52
  private val eventDispatcher: SdkEventDispatcher,
56
53
  private val loggerService: LoggerService,
57
54
  private val scanDelay: Duration,
58
- coroutineDispatcher: CoroutineDispatcher,
55
+ private val coroutineDispatcher: CoroutineDispatcher,
59
56
  ) : AndroidUsbTransport {
60
57
  private val scope = CoroutineScope(coroutineDispatcher + SupervisorJob())
61
58
  private val internalUsbEventFlow: MutableSharedFlow<UsbState> = MutableSharedFlow()
62
59
  private val internalUsbPermissionEventFlow: MutableSharedFlow<UsbPermissionEvent> =
63
60
  MutableSharedFlow()
64
61
 
65
- @Suppress("BackingPropertyName")
66
- private var _scanStateFlow: MutableStateFlow<List<DiscoveryDevice>> =
67
- MutableStateFlow(emptyList())
68
- private var discoveryJob: Job? = null
69
62
  private val usbConnections: MutableMap<String, DeviceConnection<AndroidUsbApduSender.Dependencies>> =
70
63
  mutableMapOf()
71
64
  private val usbConnectionsPendingReconnection: MutableSet<DeviceConnection<AndroidUsbApduSender.Dependencies>> =
72
65
  mutableSetOf()
73
66
 
67
+ private var discoveryJob: Job? = null
68
+
74
69
  override fun startScan(): Flow<List<DiscoveryDevice>> {
70
+ val scanStateFlow = MutableStateFlow<List<DiscoveryDevice>>(emptyList())
75
71
  discoveryJob?.cancel()
76
- _scanStateFlow.value = emptyList()
77
72
  discoveryJob =
78
73
  scope.launch {
79
74
  while (isActive) {
@@ -86,12 +81,12 @@ internal class DefaultAndroidUsbTransport(
86
81
  }.isEmpty()
87
82
  }.toUsbDevices()
88
83
 
89
- _scanStateFlow.value = devices.toScannedDevices()
84
+ scanStateFlow.value = devices.toScannedDevices()
90
85
 
91
86
  delay(scanDelay)
92
87
  }
93
88
  }
94
- return _scanStateFlow
89
+ return scanStateFlow
95
90
  }
96
91
 
97
92
  override fun stopScan() {
@@ -102,17 +97,33 @@ internal class DefaultAndroidUsbTransport(
102
97
  override fun updateUsbState(state: UsbState) {
103
98
  when (state) {
104
99
  is UsbState.Detached -> {
105
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Detached deviceId=${state.ledgerUsbDevice.uid}"))
100
+ loggerService.log(
101
+ buildSimpleDebugLogInfo(
102
+ "AndroidUsbTransport",
103
+ "Detached deviceId=${state.ledgerUsbDevice.uid}"
104
+ )
105
+ )
106
106
  usbConnections.entries.find {
107
107
  it.value.getApduSender().dependencies.ledgerUsbDevice.uid == state.ledgerUsbDevice.uid
108
108
  }.let { item ->
109
109
  scope.launch {
110
110
  if (item == null) {
111
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No connection found"))
111
+ loggerService.log(
112
+ buildSimpleWarningLogInfo(
113
+ "AndroidUsbTransport",
114
+ "No connection found"
115
+ )
116
+ )
112
117
  return@launch
113
118
  }
114
119
  val (key, deviceConnection) = item
115
- loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Device disconnected (sessionId=${deviceConnection.sessionId})"))
120
+ // (deviceConnection.getApduSender() as AndroidUsbApduSender).clear()
121
+ loggerService.log(
122
+ buildSimpleInfoLogInfo(
123
+ "AndroidUsbTransport",
124
+ "Device disconnected (sessionId=${deviceConnection.sessionId})"
125
+ )
126
+ )
116
127
  deviceConnection.handleDeviceDisconnected()
117
128
  usbConnections.remove(key)
118
129
  usbConnectionsPendingReconnection.add(deviceConnection)
@@ -121,12 +132,22 @@ internal class DefaultAndroidUsbTransport(
121
132
  }
122
133
 
123
134
  is UsbState.Attached -> {
124
- 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
+ )
125
141
  val usbDevice = usbManager.deviceList.values.firstOrNull {
126
142
  it.toLedgerUsbDevice()?.uid == state.ledgerUsbDevice.uid
127
143
  }
128
144
  if (usbDevice == null) {
129
- loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No UsbDevice found"))
145
+ loggerService.log(
146
+ buildSimpleWarningLogInfo(
147
+ "AndroidUsbTransport",
148
+ "No UsbDevice found"
149
+ )
150
+ )
130
151
  return
131
152
  }
132
153
  usbConnectionsPendingReconnection.firstOrNull {
@@ -143,15 +164,30 @@ internal class DefaultAndroidUsbTransport(
143
164
  )
144
165
  return@launch
145
166
  }
146
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Found matching device connection $deviceConnection"))
167
+ loggerService.log(
168
+ buildSimpleDebugLogInfo(
169
+ "AndroidUsbTransport",
170
+ "Found matching device connection $deviceConnection"
171
+ )
172
+ )
147
173
 
148
174
  val permissionResult = checkOrRequestPermission(usbDevice)
149
175
  if (permissionResult is PermissionResult.Denied) {
150
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Permission denied"))
176
+ loggerService.log(
177
+ buildSimpleDebugLogInfo(
178
+ "AndroidUsbTransport",
179
+ "Permission denied"
180
+ )
181
+ )
151
182
  return@launch
152
183
  }
153
- loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Reconnecting device (sessionId=${deviceConnection.sessionId})"))
154
- deviceConnection.setApduSender(
184
+ loggerService.log(
185
+ buildSimpleInfoLogInfo(
186
+ "AndroidUsbTransport",
187
+ "Reconnecting device (sessionId=${deviceConnection.sessionId})"
188
+ )
189
+ )
190
+ deviceConnection.handleDeviceConnected(
155
191
  AndroidUsbApduSender(
156
192
  dependencies = AndroidUsbApduSender.Dependencies(
157
193
  usbDevice = usbDevice,
@@ -164,7 +200,6 @@ internal class DefaultAndroidUsbTransport(
164
200
  loggerService = loggerService
165
201
  )
166
202
  )
167
- deviceConnection.handleDeviceConnected()
168
203
  usbConnectionsPendingReconnection.remove(deviceConnection)
169
204
  usbConnections[deviceConnection.sessionId] = deviceConnection
170
205
  }
@@ -201,7 +236,12 @@ internal class DefaultAndroidUsbTransport(
201
236
  device = usbDevice,
202
237
  )
203
238
 
204
- loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Waiting for permission result"))
239
+ loggerService.log(
240
+ buildSimpleDebugLogInfo(
241
+ "AndroidUsbTransport",
242
+ "Waiting for permission result"
243
+ )
244
+ )
205
245
 
206
246
  val result = eventsFlow.first {
207
247
  it is UsbPermissionEvent.PermissionGranted ||
@@ -268,11 +308,12 @@ internal class DefaultAndroidUsbTransport(
268
308
  isFatalSendApduFailure = { false }, // TODO: refine this
269
309
  reconnectionTimeoutDuration = 5.seconds,
270
310
  onTerminated = {
311
+ // (it.getApduSender() as AndroidUsbApduSender).clear()
271
312
  usbConnections.remove(sessionId)
272
313
  usbConnectionsPendingReconnection.remove(it)
273
314
  eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
274
315
  },
275
- coroutineScope = scope,
316
+ coroutineDispatcher = coroutineDispatcher,
276
317
  loggerService = loggerService,
277
318
  )
278
319
 
@@ -282,7 +323,9 @@ internal class DefaultAndroidUsbTransport(
282
323
  discoveryDevice.name,
283
324
  discoveryDevice.ledgerDevice,
284
325
  discoveryDevice.connectivityType,
285
- sendApduFn = { apdu -> deviceConnection.requestSendApdu(apdu) },
326
+ sendApduFn = { apdu: ByteArray, triggersDisconnection: Boolean, abortTimeoutDuration: Duration ->
327
+ deviceConnection.requestSendApdu(apdu, triggersDisconnection, abortTimeoutDuration)
328
+ }
286
329
  )
287
330
 
288
331
  usbConnections[sessionId] = deviceConnection
@@ -13,21 +13,24 @@ 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
20
  import com.ledger.devicesdk.shared.api.utils.toHexadecimalString
20
- import com.ledger.devicesdk.shared.androidMainInternal.transport.USB_MTU
21
21
  import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
22
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
22
23
  import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
23
- import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
24
24
  import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
25
25
  import com.ledger.devicesdk.shared.internal.transport.framer.to2BytesArray
26
- import java.nio.ByteBuffer
27
- import kotlin.random.Random
28
26
  import kotlinx.coroutines.CoroutineDispatcher
27
+ import kotlinx.coroutines.TimeoutCancellationException
29
28
  import kotlinx.coroutines.withContext
29
+ import kotlinx.coroutines.withTimeout
30
30
  import timber.log.Timber
31
+ import java.nio.ByteBuffer
32
+ import kotlin.random.Random
33
+ import kotlin.time.Duration
31
34
 
32
35
  private const val USB_TIMEOUT = 500
33
36
 
@@ -41,38 +44,60 @@ internal class AndroidUsbApduSender(
41
44
  private val ioDispatcher: CoroutineDispatcher,
42
45
  private val loggerService: LoggerService,
43
46
  ) : DeviceApduSender<AndroidUsbApduSender.Dependencies> {
44
-
45
47
  data class Dependencies(
46
48
  val usbDevice: UsbDevice,
47
49
  val ledgerUsbDevice: LedgerUsbDevice,
48
50
  )
49
51
 
50
- override suspend fun send(apdu: ByteArray): SendApduResult =
52
+ override suspend fun send(apdu: ByteArray, abortTimeoutDuration: Duration): SendApduResult =
51
53
  try {
52
54
  val usbDevice = dependencies.usbDevice
53
55
  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) }
56
+ withTimeout(abortTimeoutDuration) {
57
+ val usbInterface = usbDevice.getInterface(DEFAULT_USB_INTERFACE)
58
+ val androidToUsbEndpoint =
59
+ usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_OUT }
60
+ val usbToAndroidEndpoint =
61
+ usbInterface.firstEndpointOrThrow { it == UsbConstants.USB_DIR_IN }
62
+ val usbConnection = usbManager.openDevice(usbDevice)
63
+ .apply { claimInterface(usbInterface, true) }
58
64
 
59
- transmitApdu(
60
- usbConnection = usbConnection,
61
- androidToUsbEndpoint = androidToUsbEndpoint,
62
- rawApdu = apdu,
63
- )
64
- val apduResponse =
65
- receiveApdu(
65
+ transmitApdu(
66
66
  usbConnection = usbConnection,
67
- usbToAndroidEndpoint = usbToAndroidEndpoint,
67
+ androidToUsbEndpoint = androidToUsbEndpoint,
68
+ rawApdu = apdu,
68
69
  )
70
+ val apduResponse =
71
+ receiveApdu(
72
+ usbConnection = usbConnection,
73
+ usbToAndroidEndpoint = usbToAndroidEndpoint,
74
+ )
69
75
 
70
- usbConnection.releaseInterface(usbInterface)
71
- usbConnection.close()
76
+ if (apduResponse.isEmpty()) {
77
+ return@withTimeout SendApduResult.Failure(reason = SendApduFailureReason.EmptyResponse)
78
+ }
72
79
 
73
- SendApduResult.Success(apdu = apduResponse)
80
+ usbConnection.releaseInterface(usbInterface)
81
+ usbConnection.close()
82
+
83
+ return@withTimeout SendApduResult.Success(apdu = apduResponse)
84
+ }
74
85
  }
86
+ } catch (e: TimeoutCancellationException) {
87
+ loggerService.log(
88
+ buildSimpleErrorLogInfo(
89
+ "AndroidUsbApduSender",
90
+ "timeout in send: $e"
91
+ )
92
+ )
93
+ SendApduResult.Failure(reason = SendApduFailureReason.AbortTimeout)
75
94
  } catch (e: NoSuchElementException) {
95
+ loggerService.log(
96
+ buildSimpleErrorLogInfo(
97
+ "AndroidUsbApduSender",
98
+ "no endpoint found: $e"
99
+ )
100
+ )
76
101
  SendApduResult.Failure(reason = SendApduFailureReason.NoUsbEndpointFound)
77
102
  } catch (e: Exception) {
78
103
  loggerService.log(buildSimpleErrorLogInfo("AndroidUsbApduSender", "error in send: $e"))
@@ -84,11 +109,17 @@ internal class AndroidUsbApduSender(
84
109
  androidToUsbEndpoint: UsbEndpoint,
85
110
  rawApdu: ByteArray,
86
111
  ) {
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
- }
112
+ framerService.serialize(mtu = USB_MTU, channelId = generateChannelId(), rawApdu = rawApdu)
113
+ .forEach { apduFrame ->
114
+ val buffer = apduFrame.toByteArray()
115
+ Timber.i("APDU sent = ${buffer.toHexadecimalString()}")
116
+ usbConnection.bulkTransfer(
117
+ androidToUsbEndpoint,
118
+ buffer,
119
+ apduFrame.size(),
120
+ USB_TIMEOUT
121
+ )
122
+ }
92
123
  }
93
124
 
94
125
  private fun receiveApdu(
@@ -99,7 +130,7 @@ internal class AndroidUsbApduSender(
99
130
  request.close()
100
131
  byteArrayOf()
101
132
  } else {
102
- val frames = framerService.createApduFrames(mtu = USB_MTU, isUsbTransport = true){
133
+ val frames = framerService.createApduFrames(mtu = USB_MTU, isUsbTransport = true) {
103
134
  val buffer = ByteArray(USB_MTU)
104
135
  val responseBuffer = ByteBuffer.allocate(USB_MTU)
105
136
 
@@ -107,11 +138,19 @@ internal class AndroidUsbApduSender(
107
138
  if (!queuingResult) {
108
139
  request.close()
109
140
  byteArrayOf()
110
- }
111
- else{
141
+ } else {
112
142
  usbConnection.requestWait()
113
143
  responseBuffer.rewind()
114
- responseBuffer.get(buffer, 0, responseBuffer.remaining())
144
+ val remaining = responseBuffer.remaining()
145
+ responseBuffer.get(buffer, 0, remaining)
146
+ loggerService.log(
147
+ buildSimpleDebugLogInfo(
148
+ "AndroidUsbApduSender",
149
+ "APDU frame received = ${
150
+ buffer.copyOfRange(0, remaining).toHexadecimalString()
151
+ }"
152
+ )
153
+ )
115
154
  buffer
116
155
  }
117
156
  }
@@ -129,5 +168,6 @@ internal class AndroidUsbApduSender(
129
168
  throw NoSuchElementException("No endpoint matching the predicate")
130
169
  }
131
170
 
132
- private fun generateChannelId(): ByteArray = Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
171
+ private fun generateChannelId(): ByteArray =
172
+ Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
133
173
  }
@@ -42,7 +42,7 @@ internal class UsbPermissionReceiver(
42
42
  context,
43
43
  this,
44
44
  IntentFilter(ACTION_USB_PERMISSION),
45
- ACTION_USB_PERMISSION,
45
+ null,
46
46
  null,
47
47
  ContextCompat.RECEIVER_NOT_EXPORTED,
48
48
  )
@@ -5,51 +5,42 @@
5
5
 
6
6
  package com.ledger.devicesdk.shared.androidMain.transport.usb.utils
7
7
 
8
- import com.ledger.devicesdk.shared.api.device.LedgerDevice
9
8
  import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
9
+ import com.ledger.devicesdk.shared.api.device.LedgerDevice
10
10
 
11
11
  internal fun ProductId.toLedgerDevice(): LedgerDevice? =
12
- when {
13
- this.id.isLedgerDeviceProductId(LedgerDevice.NanoS)
14
- -> {
15
- LedgerDevice.NanoS
16
- }
17
-
18
- this.id.isLedgerDeviceProductId(LedgerDevice.NanoSPlus)
19
- -> {
20
- LedgerDevice.NanoSPlus
12
+ when {
13
+ this.id.isLedgerDeviceProductId(LedgerDevice.NanoS) -> {
14
+ LedgerDevice.NanoS
15
+ }
16
+ this.id.isLedgerDeviceProductId(LedgerDevice.NanoSPlus) -> {
17
+ LedgerDevice.NanoSPlus
18
+ }
19
+ this.id.isLedgerDeviceProductId(LedgerDevice.NanoX) -> {
20
+ LedgerDevice.NanoX
21
+ }
22
+ this.id.isLedgerDeviceProductId(LedgerDevice.Stax) -> {
23
+ LedgerDevice.Stax
24
+ }
25
+ this.id.isLedgerDeviceProductId(LedgerDevice.Flex) -> {
26
+ LedgerDevice.Flex
27
+ }
28
+ else -> {
29
+ null
30
+ }
21
31
  }
22
32
 
23
- this.id.isLedgerDeviceProductId(LedgerDevice.NanoX)
24
- -> {
25
- LedgerDevice.NanoX
26
- }
27
-
28
- this.id.isLedgerDeviceProductId(LedgerDevice.Stax)
29
- -> {
30
- LedgerDevice.Stax
31
- }
32
-
33
- this.id.isLedgerDeviceProductId(LedgerDevice.Flex)
34
- -> {
35
- LedgerDevice.Flex
36
- }
37
-
38
- else -> {
39
- null
40
- }
41
- }
42
-
43
33
  private fun Int.isLedgerDeviceProductId(device: LedgerDevice): Boolean {
44
34
  val productId = device.usbInfo.productIdMask.sdkHexToInt()
35
+ val bootloaderProductId = device.usbInfo.bootloaderProductId.sdkHexToInt()
45
36
  val shiftedId = this shr 8
46
- return shiftedId == productId
37
+ return shiftedId == productId || this == bootloaderProductId
47
38
  }
48
39
 
49
40
  @OptIn(ExperimentalStdlibApi::class)
50
41
  public fun String.sdkHexToInt(withPrefix: Boolean = true): Int =
51
- if (withPrefix) {
52
- this.substring(2).hexToInt()
53
- } else {
54
- this.hexToInt()
55
- }
42
+ if (withPrefix) {
43
+ this.substring(2).hexToInt()
44
+ } else {
45
+ this.hexToInt()
46
+ }
@@ -5,4 +5,4 @@
5
5
 
6
6
  package com.ledger.devicesdk.shared.androidMainInternal.transport
7
7
 
8
- internal val USB_MTU: Int = 64
8
+ internal const val USB_MTU: Int = 64
@@ -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
  }