@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.
- package/README.md +1 -1
- package/android/build.gradle +101 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +1 -0
- package/android/gradlew +252 -0
- package/android/gradlew.bat +94 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/BridgeEvents.kt +42 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +241 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidPackage.kt +25 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +124 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/AndroidUsbTransport.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +298 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/UsbPermissionRequester.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +133 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbAttachedReceiverController.kt +59 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbDetachedReceiverController.kt +58 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +92 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/LedgerUsbDevice.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/ProductId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbPermissionEvent.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbState.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/VendorId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +46 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbMapper.kt +56 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +95 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +314 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +89 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +7 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
- package/lib/cjs/package.json +2 -1
- package/lib/esm/package.json +2 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- 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
|
+
)
|