@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rn-hid-20250221112139 → 0.0.0-rnhid-transport-20250411151739

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/README.md +1 -1
  2. package/android/build.gradle +101 -0
  3. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  4. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  5. package/android/gradle.properties +1 -0
  6. package/android/gradlew +252 -0
  7. package/android/gradlew.bat +94 -0
  8. package/android/src/main/AndroidManifest.xml +3 -0
  9. package/android/src/main/kotlin/com/ledger/androidtransporthid/BridgeEvents.kt +42 -0
  10. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +241 -0
  11. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidPackage.kt +25 -0
  12. package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +124 -0
  13. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/AndroidUsbTransport.kt +16 -0
  14. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +298 -0
  15. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/UsbPermissionRequester.kt +18 -0
  16. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +133 -0
  17. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbAttachedReceiverController.kt +59 -0
  18. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbDetachedReceiverController.kt +58 -0
  19. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +92 -0
  20. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/LedgerUsbDevice.kt +16 -0
  21. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/ProductId.kt +11 -0
  22. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbPermissionEvent.kt +14 -0
  23. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbState.kt +16 -0
  24. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/VendorId.kt +11 -0
  25. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +46 -0
  26. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbMapper.kt +56 -0
  27. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +8 -0
  28. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +13 -0
  29. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +95 -0
  30. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +314 -0
  31. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
  32. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
  33. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
  34. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
  35. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
  36. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
  37. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
  38. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
  39. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +89 -0
  40. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +7 -0
  41. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
  42. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
  43. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
  44. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
  45. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
  46. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
  47. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
  48. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
  49. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
  50. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
  51. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
  52. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
  53. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
  54. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
  55. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
  56. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
  57. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
  58. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
  59. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
  60. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
  61. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
  62. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
  63. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
  64. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
  65. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
  66. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
  67. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
  68. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
  69. package/lib/cjs/package.json +2 -1
  70. package/lib/esm/package.json +2 -1
  71. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  72. package/package.json +6 -5
@@ -0,0 +1,298 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb
7
+
8
+ import android.app.Application
9
+ import android.hardware.usb.UsbDevice
10
+ import android.hardware.usb.UsbManager
11
+ import android.hardware.usb.UsbRequest
12
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.connection.AndroidUsbApduSender
13
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
14
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbPermissionEvent
15
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbState
16
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toLedgerUsbDevice
17
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toScannedDevice
18
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toUsbDevices
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
25
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
26
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
27
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleWarningLogInfo
28
+ import com.ledger.devicesdk.shared.internal.transport.TransportEvent
29
+ import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
30
+ import kotlinx.coroutines.CoroutineDispatcher
31
+ import kotlinx.coroutines.CoroutineScope
32
+ import kotlinx.coroutines.Dispatchers
33
+ import kotlinx.coroutines.Job
34
+ import kotlinx.coroutines.SupervisorJob
35
+ import kotlinx.coroutines.delay
36
+ import kotlinx.coroutines.flow.Flow
37
+ import kotlinx.coroutines.flow.MutableSharedFlow
38
+ import kotlinx.coroutines.flow.MutableStateFlow
39
+ import kotlinx.coroutines.flow.SharingStarted
40
+ import kotlinx.coroutines.flow.first
41
+ import kotlinx.coroutines.flow.merge
42
+ import kotlinx.coroutines.flow.shareIn
43
+ import kotlinx.coroutines.isActive
44
+ import kotlinx.coroutines.launch
45
+ import kotlin.time.Duration
46
+ import kotlin.time.Duration.Companion.seconds
47
+
48
+ internal class DefaultAndroidUsbTransport(
49
+ private val application: Application,
50
+ private val usbManager: UsbManager,
51
+ private val permissionRequester: UsbPermissionRequester,
52
+ private val eventDispatcher: SdkEventDispatcher,
53
+ private val loggerService: LoggerService,
54
+ private val scanDelay: Duration,
55
+ private val coroutineDispatcher: CoroutineDispatcher,
56
+ ) : AndroidUsbTransport {
57
+ private val scope = CoroutineScope(coroutineDispatcher + SupervisorJob())
58
+ private val internalUsbEventFlow: MutableSharedFlow<UsbState> = MutableSharedFlow()
59
+ private val internalUsbPermissionEventFlow: MutableSharedFlow<UsbPermissionEvent> =
60
+ MutableSharedFlow()
61
+
62
+ private val usbConnections: MutableMap<String, DeviceConnection<AndroidUsbApduSender.Dependencies>> =
63
+ mutableMapOf()
64
+ private val usbConnectionsPendingReconnection: MutableSet<DeviceConnection<AndroidUsbApduSender.Dependencies>> =
65
+ mutableSetOf()
66
+
67
+ private var discoveryJob: Job? = null
68
+
69
+ override fun startScan(): Flow<List<DiscoveryDevice>> {
70
+ val scanStateFlow = MutableStateFlow<List<DiscoveryDevice>>(emptyList())
71
+ discoveryJob?.cancel()
72
+ discoveryJob =
73
+ scope.launch {
74
+ 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
+
86
+ delay(scanDelay)
87
+ }
88
+ }
89
+ return scanStateFlow
90
+ }
91
+
92
+ override fun stopScan() {
93
+ discoveryJob?.cancel()
94
+ discoveryJob = null
95
+ }
96
+
97
+ override fun updateUsbState(state: UsbState) {
98
+ when (state) {
99
+ is UsbState.Detached -> {
100
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Detached deviceId=${state.ledgerUsbDevice.uid}"))
101
+ usbConnections.entries.find {
102
+ it.value.getApduSender().dependencies.ledgerUsbDevice.uid == state.ledgerUsbDevice.uid
103
+ }.let { item ->
104
+ scope.launch {
105
+ if (item == null) {
106
+ loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No connection found"))
107
+ return@launch
108
+ }
109
+ val (key, deviceConnection) = item
110
+ loggerService.log(buildSimpleInfoLogInfo("AndroidUsbTransport", "Device disconnected (sessionId=${deviceConnection.sessionId})"))
111
+ deviceConnection.handleDeviceDisconnected()
112
+ usbConnections.remove(key)
113
+ usbConnectionsPendingReconnection.add(deviceConnection)
114
+ }
115
+ }
116
+ }
117
+
118
+ is UsbState.Attached -> {
119
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Attached deviceId=${state.ledgerUsbDevice.uid}, pendingReconnections=${usbConnectionsPendingReconnection}"))
120
+ val usbDevice = usbManager.deviceList.values.firstOrNull {
121
+ it.toLedgerUsbDevice()?.uid == state.ledgerUsbDevice.uid
122
+ }
123
+ if (usbDevice == null) {
124
+ loggerService.log(buildSimpleWarningLogInfo("AndroidUsbTransport", "No UsbDevice found"))
125
+ return
126
+ }
127
+ usbConnectionsPendingReconnection.firstOrNull {
128
+ it.getApduSender()
129
+ .dependencies.ledgerUsbDevice.ledgerDevice == state.ledgerUsbDevice.ledgerDevice // we just find a similar device model since there is no way to uniquely identify a device between 2 connections
130
+ }.let { deviceConnection ->
131
+ scope.launch {
132
+ if (deviceConnection == null) {
133
+ loggerService.log(
134
+ buildSimpleWarningLogInfo(
135
+ "AndroidUsbTransport",
136
+ "No pending connection found"
137
+ )
138
+ )
139
+ return@launch
140
+ }
141
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Found matching device connection $deviceConnection"))
142
+
143
+ val permissionResult = checkOrRequestPermission(usbDevice)
144
+ if (permissionResult is PermissionResult.Denied) {
145
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Permission denied"))
146
+ return@launch
147
+ }
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
160
+ )
161
+ )
162
+ usbConnectionsPendingReconnection.remove(deviceConnection)
163
+ usbConnections[deviceConnection.sessionId] = deviceConnection
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ override fun updateUsbEvent(event: UsbPermissionEvent) {
171
+ scope.launch {
172
+ internalUsbPermissionEventFlow.emit(event)
173
+ }
174
+ }
175
+
176
+ sealed class PermissionResult {
177
+ data object Granted : PermissionResult()
178
+ data class Denied(val connectionError: InternalConnectionResult.ConnectionError) :
179
+ PermissionResult()
180
+ }
181
+
182
+ private suspend fun checkOrRequestPermission(usbDevice: UsbDevice): PermissionResult {
183
+ if (usbManager.hasPermission(usbDevice)) {
184
+ return PermissionResult.Granted
185
+ }
186
+
187
+ val eventsFlow = merge(
188
+ internalUsbPermissionEventFlow,
189
+ internalUsbEventFlow,
190
+ ).shareIn(scope = scope, started = SharingStarted.Eagerly)
191
+
192
+ permissionRequester.requestPermission(
193
+ context = application,
194
+ manager = usbManager,
195
+ device = usbDevice,
196
+ )
197
+
198
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Waiting for permission result"))
199
+
200
+ val result = eventsFlow.first {
201
+ it is UsbPermissionEvent.PermissionGranted ||
202
+ it is UsbPermissionEvent.PermissionDenied ||
203
+ it is UsbState.Detached
204
+ }
205
+
206
+ loggerService.log(buildSimpleDebugLogInfo("AndroidUsbTransport", "Got permission result"))
207
+
208
+ return when (result) {
209
+ is UsbPermissionEvent -> {
210
+ return when (result) {
211
+ is UsbPermissionEvent.PermissionDenied -> {
212
+ PermissionResult.Denied(
213
+ InternalConnectionResult.ConnectionError(
214
+ error = InternalConnectionResult.Failure.PermissionNotGranted,
215
+ )
216
+ )
217
+ }
218
+
219
+ is UsbPermissionEvent.PermissionGranted -> {
220
+ PermissionResult.Granted
221
+ }
222
+ }
223
+ }
224
+
225
+ else -> {
226
+ PermissionResult.Denied(InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound))
227
+ }
228
+ }
229
+ }
230
+
231
+ override suspend fun connect(discoveryDevice: DiscoveryDevice): InternalConnectionResult {
232
+ val usbDevice: UsbDevice? =
233
+ usbManager.deviceList.values.firstOrNull { it.deviceId == discoveryDevice.uid.toInt() }
234
+
235
+ val ledgerUsbDevice = usbDevice?.toLedgerUsbDevice()
236
+
237
+ return if (usbDevice == null || ledgerUsbDevice == null) {
238
+ InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
239
+ } else {
240
+ val permissionResult = checkOrRequestPermission(usbDevice)
241
+ if (permissionResult is PermissionResult.Denied) {
242
+ return permissionResult.connectionError
243
+ }
244
+
245
+ val sessionId = generateSessionId(usbDevice)
246
+ val apduSender =
247
+ AndroidUsbApduSender(
248
+ dependencies = AndroidUsbApduSender.Dependencies(
249
+ usbDevice = usbDevice,
250
+ ledgerUsbDevice = ledgerUsbDevice,
251
+ ),
252
+ usbManager = usbManager,
253
+ ioDispatcher = Dispatchers.IO,
254
+ framerService = FramerService(loggerService),
255
+ request = UsbRequest(),
256
+ loggerService = loggerService,
257
+ )
258
+
259
+ val deviceConnection = DeviceConnection(
260
+ sessionId = sessionId,
261
+ deviceApduSender = apduSender,
262
+ isFatalSendApduFailure = { false }, // TODO: refine this
263
+ reconnectionTimeoutDuration = 5.seconds,
264
+ onTerminated = {
265
+ usbConnections.remove(sessionId)
266
+ usbConnectionsPendingReconnection.remove(it)
267
+ eventDispatcher.dispatch(TransportEvent.DeviceConnectionLost(sessionId))
268
+ },
269
+ coroutineDispatcher = coroutineDispatcher,
270
+ loggerService = loggerService,
271
+ )
272
+
273
+ val connectedDevice =
274
+ InternalConnectedDevice(
275
+ sessionId,
276
+ discoveryDevice.name,
277
+ discoveryDevice.ledgerDevice,
278
+ discoveryDevice.connectivityType,
279
+ sendApduFn = { apdu -> deviceConnection.requestSendApdu(apdu) },
280
+ )
281
+
282
+ usbConnections[sessionId] = deviceConnection
283
+
284
+ InternalConnectionResult.Connected(device = connectedDevice, sessionId = sessionId)
285
+ }
286
+ }
287
+
288
+ override suspend fun disconnect(deviceId: String) {
289
+ usbConnections[deviceId]?.requestCloseConnection()
290
+ }
291
+
292
+ private fun generateSessionId(usbDevice: UsbDevice): String = "usb_${usbDevice.deviceId}"
293
+ }
294
+
295
+ private fun List<LedgerUsbDevice>.toScannedDevices(): List<DiscoveryDevice> =
296
+ this.map {
297
+ it.toScannedDevice()
298
+ }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2023 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb
7
+
8
+ import android.content.Context
9
+ import android.hardware.usb.UsbManager
10
+ import android.hardware.usb.UsbDevice as AndroidUsbDevice
11
+
12
+ internal fun interface UsbPermissionRequester {
13
+ fun requestPermission(
14
+ context: Context,
15
+ manager: UsbManager,
16
+ device: AndroidUsbDevice,
17
+ )
18
+ }
@@ -0,0 +1,133 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.connection
7
+
8
+ import android.hardware.usb.UsbConstants
9
+ import android.hardware.usb.UsbDevice
10
+ import android.hardware.usb.UsbDeviceConnection
11
+ import android.hardware.usb.UsbEndpoint
12
+ import android.hardware.usb.UsbInterface
13
+ import android.hardware.usb.UsbManager
14
+ import android.hardware.usb.UsbRequest
15
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
16
+ import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
17
+ 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
+ import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
22
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
23
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
24
+ import com.ledger.devicesdk.shared.internal.transport.framer.FramerService
25
+ import com.ledger.devicesdk.shared.internal.transport.framer.to2BytesArray
26
+ import java.nio.ByteBuffer
27
+ import kotlin.random.Random
28
+ import kotlinx.coroutines.CoroutineDispatcher
29
+ import kotlinx.coroutines.withContext
30
+ import timber.log.Timber
31
+
32
+ private const val USB_TIMEOUT = 500
33
+
34
+ private const val DEFAULT_USB_INTERFACE = 0
35
+
36
+ internal class AndroidUsbApduSender(
37
+ override val dependencies: Dependencies,
38
+ private val usbManager: UsbManager,
39
+ private val framerService: FramerService,
40
+ private val request: UsbRequest,
41
+ private val ioDispatcher: CoroutineDispatcher,
42
+ private val loggerService: LoggerService,
43
+ ) : DeviceApduSender<AndroidUsbApduSender.Dependencies> {
44
+
45
+ data class Dependencies(
46
+ val usbDevice: UsbDevice,
47
+ val ledgerUsbDevice: LedgerUsbDevice,
48
+ )
49
+
50
+ override suspend fun send(apdu: ByteArray): SendApduResult =
51
+ try {
52
+ val usbDevice = dependencies.usbDevice
53
+ 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) }
58
+
59
+ transmitApdu(
60
+ usbConnection = usbConnection,
61
+ androidToUsbEndpoint = androidToUsbEndpoint,
62
+ rawApdu = apdu,
63
+ )
64
+ val apduResponse =
65
+ receiveApdu(
66
+ usbConnection = usbConnection,
67
+ usbToAndroidEndpoint = usbToAndroidEndpoint,
68
+ )
69
+
70
+ usbConnection.releaseInterface(usbInterface)
71
+ usbConnection.close()
72
+
73
+ SendApduResult.Success(apdu = apduResponse)
74
+ }
75
+ } catch (e: NoSuchElementException) {
76
+ SendApduResult.Failure(reason = SendApduFailureReason.NoUsbEndpointFound)
77
+ } catch (e: Exception) {
78
+ loggerService.log(buildSimpleErrorLogInfo("AndroidUsbApduSender", "error in send: $e"))
79
+ SendApduResult.Failure(reason = SendApduFailureReason.Unknown)
80
+ }
81
+
82
+ private fun transmitApdu(
83
+ usbConnection: UsbDeviceConnection,
84
+ androidToUsbEndpoint: UsbEndpoint,
85
+ rawApdu: ByteArray,
86
+ ) {
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
+ }
92
+ }
93
+
94
+ private fun receiveApdu(
95
+ usbConnection: UsbDeviceConnection,
96
+ usbToAndroidEndpoint: UsbEndpoint,
97
+ ): ByteArray =
98
+ if (!request.initialize(usbConnection, usbToAndroidEndpoint)) {
99
+ request.close()
100
+ byteArrayOf()
101
+ } else {
102
+ val frames = framerService.createApduFrames(mtu = USB_MTU, isUsbTransport = true){
103
+ val buffer = ByteArray(USB_MTU)
104
+ val responseBuffer = ByteBuffer.allocate(USB_MTU)
105
+
106
+ val queuingResult = request.queue(responseBuffer)
107
+ if (!queuingResult) {
108
+ request.close()
109
+ byteArrayOf()
110
+ }
111
+ else{
112
+ usbConnection.requestWait()
113
+ responseBuffer.rewind()
114
+ responseBuffer.get(buffer, 0, responseBuffer.remaining())
115
+ buffer
116
+ }
117
+ }
118
+ framerService.deserialize(mtu = USB_MTU, frames)
119
+ }
120
+
121
+ private fun UsbInterface.firstEndpointOrThrow(predicate: (Int) -> Boolean): UsbEndpoint {
122
+ for (endp in 0..this.endpointCount) {
123
+ val endpoint = this.getEndpoint(endp)
124
+ val endpointDirection = endpoint.direction
125
+ if (predicate(endpointDirection)) {
126
+ return endpoint
127
+ }
128
+ }
129
+ throw NoSuchElementException("No endpoint matching the predicate")
130
+ }
131
+
132
+ private fun generateChannelId(): ByteArray = Random.nextInt(0, until = Int.MAX_VALUE).to2BytesArray()
133
+ }
@@ -0,0 +1,59 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2023 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.controller
7
+
8
+ import android.content.BroadcastReceiver
9
+ import android.content.Context
10
+ import android.content.Intent
11
+ import android.content.IntentFilter
12
+ import android.hardware.usb.UsbDevice
13
+ import android.hardware.usb.UsbManager
14
+ import androidx.core.content.ContextCompat
15
+ import com.ledger.devicesdk.shared.internal.utils.Controller
16
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.AndroidUsbTransport
17
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
18
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbState
19
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.getAndroidUsbDevice
20
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toLedgerUsbDevice
21
+ import timber.log.Timber
22
+
23
+ internal class UsbAttachedReceiverController(
24
+ private val context: Context,
25
+ private val androidUsbTransport: AndroidUsbTransport,
26
+ ) : BroadcastReceiver(),
27
+ Controller {
28
+ override fun start() {
29
+ Timber.i("start UsbAttachedReceiverController")
30
+ ContextCompat.registerReceiver(
31
+ context,
32
+ this,
33
+ IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED),
34
+ ContextCompat.RECEIVER_NOT_EXPORTED,
35
+ )
36
+ }
37
+
38
+ override fun stop() {
39
+ Timber.i("stop UsbAttachedReceiverController")
40
+ context.unregisterReceiver(this)
41
+ }
42
+
43
+ override fun onReceive(
44
+ context: Context,
45
+ intent: Intent,
46
+ ) {
47
+ Timber.i("UsbAttachedReceiverController:onReceive")
48
+ val androidUsbDevice: UsbDevice = intent.getAndroidUsbDevice()
49
+ val ledgerUsbDevice: LedgerUsbDevice? = androidUsbDevice.toLedgerUsbDevice()
50
+ if (ledgerUsbDevice != null) {
51
+ androidUsbTransport.updateUsbState(
52
+ state = UsbState.Attached(
53
+ ledgerUsbDevice = ledgerUsbDevice,
54
+ )
55
+ )
56
+ }
57
+
58
+ }
59
+ }
@@ -0,0 +1,58 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2023 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.controller
7
+
8
+ import android.content.BroadcastReceiver
9
+ import android.content.Context
10
+ import android.content.Intent
11
+ import android.content.IntentFilter
12
+ import android.hardware.usb.UsbDevice
13
+ import android.hardware.usb.UsbManager
14
+ import androidx.core.content.ContextCompat
15
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.AndroidUsbTransport
16
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
17
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbState
18
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.getAndroidUsbDevice
19
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toLedgerUsbDevice
20
+ import com.ledger.devicesdk.shared.internal.utils.Controller
21
+ import timber.log.Timber
22
+
23
+ internal class UsbDetachedReceiverController(
24
+ private val context: Context,
25
+ private val androidUsbTransport: AndroidUsbTransport,
26
+ ) : BroadcastReceiver(),
27
+ Controller {
28
+ override fun start() {
29
+ Timber.i("start UsbDetachedReceiverController")
30
+ ContextCompat.registerReceiver(
31
+ context,
32
+ this,
33
+ IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED),
34
+ ContextCompat.RECEIVER_NOT_EXPORTED,
35
+ )
36
+ }
37
+
38
+ override fun stop() {
39
+ Timber.i("stop UsbDetachedReceiverController")
40
+ context.unregisterReceiver(this)
41
+ }
42
+
43
+ override fun onReceive(
44
+ context: Context,
45
+ intent: Intent,
46
+ ) {
47
+ Timber.i("UsbDetachedReceiverController:onReceive")
48
+ val usbDevice: UsbDevice = intent.getAndroidUsbDevice()
49
+ val ledgerUsbDevice: LedgerUsbDevice? = usbDevice.toLedgerUsbDevice()
50
+ if (ledgerUsbDevice != null) {
51
+ androidUsbTransport.updateUsbState(
52
+ state = UsbState.Detached(
53
+ ledgerUsbDevice = ledgerUsbDevice,
54
+ )
55
+ )
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.controller
7
+
8
+ import android.content.BroadcastReceiver
9
+ import android.content.Context
10
+ import android.content.Intent
11
+ import android.content.IntentFilter
12
+ import android.hardware.usb.UsbManager
13
+ import android.os.Build
14
+ import androidx.core.content.ContextCompat
15
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.AndroidUsbTransport
16
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.UsbPermissionEvent
17
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.utils.toLedgerUsbDevice
18
+ import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
19
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
20
+ import com.ledger.devicesdk.shared.internal.utils.Controller
21
+ import timber.log.Timber
22
+
23
+ internal const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"
24
+
25
+ internal class UsbPermissionReceiver(
26
+ private val context: Context,
27
+ private val androidUsbTransport: AndroidUsbTransport,
28
+ private val usbManager: UsbManager,
29
+ private val loggerService: LoggerService,
30
+ ) : BroadcastReceiver(),
31
+ Controller {
32
+ override fun start() {
33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
34
+ ContextCompat.registerReceiver(
35
+ context,
36
+ this,
37
+ IntentFilter(ACTION_USB_PERMISSION),
38
+ ContextCompat.RECEIVER_NOT_EXPORTED,
39
+ )
40
+ } else {
41
+ ContextCompat.registerReceiver(
42
+ context,
43
+ this,
44
+ IntentFilter(ACTION_USB_PERMISSION),
45
+ null,
46
+ null,
47
+ ContextCompat.RECEIVER_NOT_EXPORTED,
48
+ )
49
+ }
50
+ }
51
+
52
+ override fun stop() {
53
+ context.unregisterReceiver(this)
54
+ }
55
+
56
+ override fun onReceive(
57
+ context: Context,
58
+ intent: Intent,
59
+ ) {
60
+ Timber.i("UsbPermissionReceiver:onReceive")
61
+ if (ACTION_USB_PERMISSION == intent.action) {
62
+ synchronized(this) {
63
+ val androidUsbDevice = usbManager.deviceList.values.firstOrNull {
64
+ usbManager.hasPermission(it) && it.toLedgerUsbDevice() != null
65
+ }
66
+ val ledgerUsbDevice = androidUsbDevice?.toLedgerUsbDevice()
67
+ if (ledgerUsbDevice != null) {
68
+ loggerService.log(
69
+ buildSimpleDebugLogInfo(
70
+ "UsbPermissionReceiver:onReceive",
71
+ "permission granted"
72
+ )
73
+ )
74
+ androidUsbTransport.updateUsbEvent(
75
+ UsbPermissionEvent.PermissionGranted(ledgerUsbDevice = ledgerUsbDevice)
76
+ )
77
+ } else {
78
+ loggerService.log(
79
+ buildSimpleDebugLogInfo(
80
+ "UsbPermissionReceiver:onReceive",
81
+ "permission denied"
82
+ )
83
+ )
84
+ androidUsbTransport.updateUsbEvent(
85
+ UsbPermissionEvent.PermissionDenied
86
+ )
87
+ }
88
+
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2023 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.model
7
+
8
+ import com.ledger.devicesdk.shared.api.device.LedgerDevice
9
+
10
+ internal class LedgerUsbDevice(
11
+ val uid: String,
12
+ val name: String,
13
+ val vendorId: VendorId,
14
+ val productId: ProductId,
15
+ val ledgerDevice: LedgerDevice,
16
+ )
@@ -0,0 +1,11 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.model
7
+
8
+ @JvmInline
9
+ public value class ProductId(
10
+ public val id: Int,
11
+ )