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