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