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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +101 -0
  3. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  4. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  5. package/android/gradle.properties +1 -0
  6. package/android/gradlew +252 -0
  7. package/android/gradlew.bat +94 -0
  8. package/android/src/main/AndroidManifest.xml +3 -0
  9. package/android/src/main/kotlin/com/ledger/androidtransporthid/BridgeEvents.kt +42 -0
  10. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +241 -0
  11. package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidPackage.kt +25 -0
  12. package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +124 -0
  13. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/AndroidUsbTransport.kt +16 -0
  14. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +298 -0
  15. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/UsbPermissionRequester.kt +18 -0
  16. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +133 -0
  17. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbAttachedReceiverController.kt +59 -0
  18. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbDetachedReceiverController.kt +58 -0
  19. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +92 -0
  20. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/LedgerUsbDevice.kt +16 -0
  21. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/ProductId.kt +11 -0
  22. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbPermissionEvent.kt +14 -0
  23. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbState.kt +16 -0
  24. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/VendorId.kt +11 -0
  25. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +46 -0
  26. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbMapper.kt +56 -0
  27. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +8 -0
  28. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +13 -0
  29. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +95 -0
  30. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +314 -0
  31. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
  32. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
  33. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
  34. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
  35. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
  36. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
  37. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
  38. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
  39. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +89 -0
  40. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +7 -0
  41. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
  42. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
  43. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
  44. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
  45. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
  46. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
  47. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
  48. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
  49. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
  50. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
  51. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
  52. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
  53. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
  54. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
  55. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
  56. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
  57. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
  58. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
  59. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
  60. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
  61. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
  62. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
  63. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
  64. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
  65. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
  66. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
  67. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
  68. package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
  69. package/lib/cjs/package.json +2 -1
  70. package/lib/esm/package.json +2 -1
  71. package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
  72. package/package.json +6 -5
@@ -0,0 +1,14 @@
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
+ internal sealed class UsbPermissionEvent {
9
+ data class PermissionGranted(
10
+ val ledgerUsbDevice: LedgerUsbDevice,
11
+ ) : UsbPermissionEvent()
12
+
13
+ data object PermissionDenied : UsbPermissionEvent()
14
+ }
@@ -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
+ internal sealed class UsbState {
9
+ data class Detached(
10
+ val ledgerUsbDevice: LedgerUsbDevice,
11
+ ) : UsbState()
12
+
13
+ data class Attached(
14
+ val ledgerUsbDevice: LedgerUsbDevice,
15
+ ): UsbState()
16
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.model
7
+
8
+ @JvmInline
9
+ internal value class VendorId(
10
+ val id: Int,
11
+ )
@@ -0,0 +1,46 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.utils
7
+
8
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
9
+ import com.ledger.devicesdk.shared.api.device.LedgerDevice
10
+
11
+ internal fun ProductId.toLedgerDevice(): LedgerDevice? =
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
+ }
31
+ }
32
+
33
+ private fun Int.isLedgerDeviceProductId(device: LedgerDevice): Boolean {
34
+ val productId = device.usbInfo.productIdMask.sdkHexToInt()
35
+ val bootloaderProductId = device.usbInfo.bootloaderProductId.sdkHexToInt()
36
+ val shiftedId = this shr 8
37
+ return shiftedId == productId || this == bootloaderProductId
38
+ }
39
+
40
+ @OptIn(ExperimentalStdlibApi::class)
41
+ public fun String.sdkHexToInt(withPrefix: Boolean = true): Int =
42
+ if (withPrefix) {
43
+ this.substring(2).hexToInt()
44
+ } else {
45
+ this.hexToInt()
46
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2023 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.utils
7
+
8
+ import android.content.Intent
9
+ import android.hardware.usb.UsbManager
10
+ import android.os.Build
11
+ import com.ledger.devicesdk.shared.api.device.LedgerDevice
12
+ import com.ledger.devicesdk.shared.api.discovery.ConnectivityType
13
+ import com.ledger.devicesdk.shared.api.discovery.DiscoveryDevice
14
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.VendorId
15
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
16
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.LedgerUsbDevice
17
+
18
+ internal fun LedgerUsbDevice.toScannedDevice() =
19
+ DiscoveryDevice(
20
+ uid = this.uid,
21
+ name = this.name,
22
+ ledgerDevice = this.ledgerDevice,
23
+ connectivityType = ConnectivityType.Usb,
24
+ )
25
+
26
+ internal fun List<android.hardware.usb.UsbDevice>.toUsbDevices(): List<LedgerUsbDevice> = mapNotNull { it.toLedgerUsbDevice() }
27
+
28
+ internal fun android.hardware.usb.UsbDevice.toLedgerUsbDevice(): LedgerUsbDevice? {
29
+ val productId = ProductId(this.productId)
30
+ val vendorId = VendorId(this.vendorId)
31
+
32
+ val ledgerDevice = productId.toLedgerDevice()
33
+ return if (vendorId.id == LedgerDevice.LEDGER_USB_VENDOR_ID.toProductIdInt() && ledgerDevice != null) {
34
+ return LedgerUsbDevice(
35
+ uid = this.deviceId.toString(),
36
+ name = ledgerDevice.name,
37
+ vendorId = vendorId,
38
+ productId = productId,
39
+ ledgerDevice = ledgerDevice,
40
+ )
41
+ } else {
42
+ null
43
+ }
44
+ }
45
+
46
+ private fun String.toProductIdInt(): Int = this.substring(2).toInt(16)
47
+
48
+ internal fun Intent.getAndroidUsbDevice(): android.hardware.usb.UsbDevice {
49
+ val device =
50
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
51
+ getParcelableExtra(UsbManager.EXTRA_DEVICE, android.hardware.usb.UsbDevice::class.java)
52
+ } else {
53
+ getParcelableExtra(UsbManager.EXTRA_DEVICE)
54
+ }
55
+ return checkNotNull(device)
56
+ }
@@ -0,0 +1,8 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2025 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMainInternal.transport
7
+
8
+ internal const val USB_MTU: Int = 64
@@ -0,0 +1,13 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2025 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
7
+
8
+ import com.ledger.devicesdk.shared.api.apdu.SendApduResult
9
+
10
+ internal interface DeviceApduSender<Dependencies> {
11
+ suspend fun send(apdu: ByteArray): SendApduResult
12
+ val dependencies: Dependencies
13
+ }
@@ -0,0 +1,95 @@
1
+ package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
2
+
3
+ import com.ledger.devicesdk.shared.api.apdu.SendApduResult
4
+ import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
5
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleErrorLogInfo
6
+ import kotlinx.coroutines.CoroutineDispatcher
7
+ import kotlinx.coroutines.CoroutineScope
8
+ import kotlinx.coroutines.launch
9
+ import kotlin.coroutines.resume
10
+ import kotlin.coroutines.suspendCoroutine
11
+ import kotlin.time.Duration
12
+
13
+ internal class DeviceConnection<Dependencies>(
14
+ val sessionId: String,
15
+ private var deviceApduSender: DeviceApduSender<Dependencies>,
16
+ isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
17
+ reconnectionTimeoutDuration: Duration,
18
+ coroutineDispatcher: CoroutineDispatcher,
19
+ private val onTerminated: (DeviceConnection<Dependencies>) -> Unit,
20
+ private val loggerService: LoggerService,
21
+ ) {
22
+ private val stateMachine: DeviceConnectionStateMachine
23
+ private val coroutineScope = CoroutineScope(coroutineDispatcher)
24
+
25
+ init {
26
+ stateMachine = DeviceConnectionStateMachine(
27
+ sendApduFn = {
28
+ coroutineScope.launch {
29
+ val res = deviceApduSender.send(it)
30
+ handleApduResult(res)
31
+ }
32
+ },
33
+ onTerminated = {
34
+ onTerminated(this)
35
+ },
36
+ isFatalSendApduFailure = isFatalSendApduFailure,
37
+ reconnectionTimeoutDuration = reconnectionTimeoutDuration,
38
+ coroutineDispatcher = coroutineDispatcher,
39
+ onError = {
40
+ loggerService.log(
41
+ buildSimpleErrorLogInfo(
42
+ "DeviceConnection",
43
+ "State machine error $it"
44
+ )
45
+ )
46
+ },
47
+ loggerService = loggerService,
48
+ )
49
+ }
50
+
51
+
52
+ private fun handleApduResult(result: SendApduResult) {
53
+ stateMachine.handleApduResult(result)
54
+ }
55
+
56
+ public fun getApduSender(): DeviceApduSender<Dependencies> {
57
+ return deviceApduSender
58
+ }
59
+
60
+ public fun handleDeviceConnected(apduSender: DeviceApduSender<Dependencies>) {
61
+ deviceApduSender = apduSender
62
+ stateMachine.handleDeviceConnected()
63
+ }
64
+
65
+ public fun handleDeviceDisconnected() {
66
+ stateMachine.handleDeviceDisconnected()
67
+ }
68
+
69
+ public suspend fun requestSendApdu(apdu: ByteArray): SendApduResult =
70
+ suspendCoroutine { cont ->
71
+ stateMachine.requestSendApdu(
72
+ DeviceConnectionStateMachine.SendApduRequestContent(
73
+ apdu = apdu,
74
+ triggersDisconnection = apduTriggersDisconnection(apdu),
75
+ resultCallback = cont::resume
76
+ )
77
+ )
78
+ }
79
+
80
+ public fun requestCloseConnection() {
81
+ stateMachine.requestCloseConnection()
82
+ }
83
+
84
+ private fun apduTriggersDisconnection(apdu: ByteArray): Boolean {
85
+ val apduMap = mapOf(
86
+ "openApp" to byteArrayOf(0xe0.toByte(), 0xd8.toByte(), 0x00, 0x00),
87
+ "closeApp" to byteArrayOf(0xb0.toByte(), 0xa7.toByte(), 0x00, 0x00)
88
+ )
89
+
90
+ return apduMap.values.any { known ->
91
+ (0 until 4).all { i -> known[i] == apdu[i] }
92
+ }
93
+ }
94
+
95
+ }
@@ -0,0 +1,314 @@
1
+ package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
2
+
3
+ import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
4
+ import com.ledger.devicesdk.shared.api.apdu.SendApduResult
5
+ import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
6
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleDebugLogInfo
7
+ import com.ledger.devicesdk.shared.internal.service.logger.buildSimpleInfoLogInfo
8
+ import kotlinx.coroutines.CoroutineDispatcher
9
+ import kotlinx.coroutines.CoroutineScope
10
+ import kotlinx.coroutines.Job
11
+ import kotlinx.coroutines.launch
12
+ import kotlin.time.Duration
13
+
14
+ internal class DeviceConnectionStateMachine(
15
+ private val sendApduFn: (apdu: ByteArray) -> Unit,
16
+ private val onTerminated: () -> Unit,
17
+ private val isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
18
+ private val reconnectionTimeoutDuration: Duration,
19
+ private val onError: (Throwable) -> Unit,
20
+ private val loggerService: LoggerService,
21
+ coroutineDispatcher: CoroutineDispatcher,
22
+ ) {
23
+ private val coroutineScope = CoroutineScope(coroutineDispatcher)
24
+ private var state: State = State.Connected
25
+
26
+ fun getState() = state
27
+
28
+ private fun pushState(newState: State) {
29
+ when (newState) {
30
+ is State.Connected -> {}
31
+ is State.SendingApdu -> {
32
+ sendApduFn(newState.requestContent.apdu)
33
+ }
34
+
35
+ is State.WaitingForReconnection -> {
36
+ startReconnectionTimeout()
37
+ }
38
+
39
+ is State.WaitingForReconnectionWithQueuedApdu -> {}
40
+ is State.Terminated -> {
41
+ onTerminated()
42
+ }
43
+ }
44
+ this.state = newState
45
+ }
46
+
47
+ private fun handleEvent(event: Event) {
48
+ val currentState = state
49
+ when (currentState) {
50
+ is State.Connected -> {
51
+ when (event) {
52
+ is Event.SendApduRequested -> {
53
+ pushState(State.SendingApdu(event.requestContent))
54
+ }
55
+
56
+ is Event.CloseConnectionRequested -> {
57
+ pushState(State.Terminated)
58
+ }
59
+
60
+ is Event.DeviceDisconnected -> {
61
+ pushState(State.WaitingForReconnection)
62
+ }
63
+
64
+ else -> {
65
+ onError(Exception("Unhandled event: $event in state: $currentState"))
66
+ }
67
+ }
68
+ }
69
+
70
+ is State.SendingApdu -> {
71
+ when (event) {
72
+ is Event.ApduResultReceived -> {
73
+ when (event.result) {
74
+ is SendApduResult.Failure -> {
75
+ if (isFatalSendApduFailure(event.result)) {
76
+ pushState(State.Terminated)
77
+ } else {
78
+ pushState(State.Connected)
79
+ }
80
+ }
81
+
82
+ is SendApduResult.Success -> {
83
+ // check if last 2 bytes of APDU are [0x90,OxO0]
84
+ val apdu = event.result.apdu
85
+ val apduSize = apdu.size
86
+ val isSuccessApdu =
87
+ apdu.size >= 2 &&
88
+ apdu[apduSize - 2] == 0x90.toByte() &&
89
+ apdu[apduSize - 1] == 0x00.toByte()
90
+
91
+ if (isSuccessApdu && currentState.requestContent.triggersDisconnection) {
92
+ pushState(State.WaitingForReconnection)
93
+ } else {
94
+ pushState(State.Connected)
95
+ }
96
+ }
97
+ }
98
+ currentState.requestContent.resultCallback(event.result)
99
+ }
100
+
101
+ is Event.CloseConnectionRequested -> {
102
+ pushState(State.Terminated)
103
+ currentState.requestContent.resultCallback(
104
+ SendApduResult.Failure(
105
+ SendApduFailureReason.DeviceDisconnected
106
+ )
107
+ )
108
+ }
109
+
110
+ is Event.DeviceDisconnected -> {
111
+ pushState(State.WaitingForReconnection)
112
+ currentState.requestContent.resultCallback(
113
+ SendApduResult.Failure(
114
+ SendApduFailureReason.DeviceDisconnected
115
+ )
116
+ )
117
+ }
118
+
119
+ is Event.SendApduRequested -> {
120
+ event.requestContent.resultCallback(
121
+ SendApduResult.Failure(
122
+ SendApduFailureReason.DeviceBusy
123
+ )
124
+ )
125
+ }
126
+
127
+ else -> {
128
+ onError(Exception("Unhandled event: $event in state: $currentState"))
129
+ }
130
+ }
131
+ }
132
+
133
+ is State.WaitingForReconnection -> {
134
+ when (event) {
135
+ is Event.DeviceConnected -> {
136
+ pushState(State.Connected)
137
+ cancelReconnectionTimeout()
138
+ }
139
+
140
+ is Event.SendApduRequested -> {
141
+ pushState(State.WaitingForReconnectionWithQueuedApdu(event.requestContent))
142
+ }
143
+
144
+ is Event.WaitingForReconnectionTimedOut,
145
+ is Event.CloseConnectionRequested -> {
146
+ pushState(State.Terminated)
147
+ cancelReconnectionTimeout()
148
+ }
149
+
150
+ is Event.DeviceDisconnected -> {
151
+ /**
152
+ * Do nothing, this will happen if we send an apdu that triggers a
153
+ * disconnection, because we will move to this state before the disconnection
154
+ * is detected:
155
+ *
156
+ * 1. APDU that triggers a disconnection is sent.
157
+ * 2. We receive a 0x9000 (success) response.
158
+ * -> We go to State.WaitingForReconnection.
159
+ * 3. Device disconnection is finally detected:
160
+ * -> Event.DeviceDisconnected is received here.
161
+ */
162
+ }
163
+
164
+ else -> {
165
+ onError(Exception("Unhandled event: $event in state: $currentState"))
166
+ }
167
+ }
168
+ }
169
+
170
+ is State.WaitingForReconnectionWithQueuedApdu -> {
171
+ when (event) {
172
+ is Event.DeviceConnected -> {
173
+ pushState(State.SendingApdu(currentState.requestContent))
174
+ cancelReconnectionTimeout()
175
+ }
176
+
177
+ is Event.CloseConnectionRequested,
178
+ is Event.WaitingForReconnectionTimedOut -> {
179
+ pushState(State.Terminated)
180
+ currentState.requestContent.resultCallback(
181
+ SendApduResult.Failure(
182
+ SendApduFailureReason.DeviceDisconnected
183
+ )
184
+ )
185
+ cancelReconnectionTimeout()
186
+ }
187
+
188
+ is Event.SendApduRequested -> {
189
+ event.requestContent.resultCallback(
190
+ SendApduResult.Failure(
191
+ SendApduFailureReason.DeviceBusy
192
+ )
193
+ )
194
+ }
195
+
196
+ is Event.DeviceDisconnected -> {
197
+ /**
198
+ * Do nothing, this will happen if we send an apdu that triggers a
199
+ * disconnection, because we will move to this state before the disconnection
200
+ * is detected:
201
+ *
202
+ * 1. APDU that triggers a disconnection is sent.
203
+ * 2. We receive a 0x9000 (success) response
204
+ * -> We go to State.WaitingForReconnection in anticipation of the disconnection event.
205
+ * 3. We receive Event.SendApduRequested
206
+ * -> We go to WaitingForReconnectionWithQueuedApdu
207
+ * 4. Device disconnection is finally detected:
208
+ * -> Event.DeviceDisconnected is received here.
209
+ *
210
+ * It can also happen if the device is disconnected while we are sending an APDU.
211
+ * cf. description of event below.
212
+ */
213
+ }
214
+
215
+ is Event.ApduResultReceived -> {
216
+ /**
217
+ * Do nothing, this will happen if while an APDU is being sent,
218
+ * the device disconnection is detected.
219
+ * 1. APDU is sent
220
+ * 2. Device disconnection is detected
221
+ * -> Event.DeviceDisconnected is received in SendingApdu state.
222
+ * -> We move to WaitingForReconnection state.
223
+ * 3. The function to send the APDU returns an error because the device is disconnected.
224
+ * -> Event.ApduResultReceived(result=Failure()) is received in the
225
+ * current state.
226
+ *
227
+ * It's a race condition between step 2 and 3.
228
+ */
229
+ }
230
+ }
231
+ }
232
+
233
+ is State.Terminated -> {
234
+ onError(Exception("Unhandled event: $event in state: $currentState"))
235
+ }
236
+ }
237
+ val logMessage = """
238
+ Received event:
239
+ In state: $currentState
240
+ -> Event: $event
241
+ -> New state: $state
242
+ """.trimIndent()
243
+ loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
244
+ }
245
+
246
+ private var timeoutJob: Job? = null
247
+ private fun startReconnectionTimeout() {
248
+ // start a timeout and at the end, emit a WaitingForReconnectionTimedOut event
249
+ timeoutJob = coroutineScope.launch {
250
+ kotlinx.coroutines.delay(reconnectionTimeoutDuration)
251
+ handleEvent(Event.WaitingForReconnectionTimedOut)
252
+ }
253
+ }
254
+
255
+ private fun cancelReconnectionTimeout() {
256
+ timeoutJob?.cancel()
257
+ timeoutJob = null
258
+ }
259
+
260
+ public fun requestSendApdu(requestContent: SendApduRequestContent) {
261
+ handleEvent(Event.SendApduRequested(requestContent))
262
+ }
263
+
264
+ public fun requestCloseConnection() {
265
+ handleEvent(Event.CloseConnectionRequested)
266
+ }
267
+
268
+ public fun handleApduResult(result: SendApduResult) {
269
+ handleEvent(Event.ApduResultReceived(result))
270
+ }
271
+
272
+ public fun handleDeviceConnected() {
273
+ handleEvent(Event.DeviceConnected)
274
+ }
275
+
276
+ public fun handleDeviceDisconnected() {
277
+ handleEvent(Event.DeviceDisconnected)
278
+ }
279
+
280
+ data class SendApduRequestContent(
281
+ val apdu: ByteArray,
282
+ val triggersDisconnection: Boolean,
283
+ val resultCallback: (SendApduResult) -> Unit
284
+ )
285
+
286
+ sealed class Event {
287
+ data object DeviceConnected : Event()
288
+
289
+ data object DeviceDisconnected : Event()
290
+
291
+ data class SendApduRequested(
292
+ val requestContent: SendApduRequestContent
293
+ ) : Event()
294
+
295
+ data object CloseConnectionRequested : Event()
296
+
297
+ data class ApduResultReceived(val result: SendApduResult) : Event()
298
+
299
+ data object WaitingForReconnectionTimedOut : Event()
300
+ }
301
+
302
+ sealed class State {
303
+ data object Connected : State()
304
+
305
+ data class SendingApdu(val requestContent: SendApduRequestContent) : State()
306
+
307
+ data object WaitingForReconnection : State()
308
+
309
+ data class WaitingForReconnectionWithQueuedApdu(val requestContent: SendApduRequestContent) :
310
+ State()
311
+
312
+ data object Terminated : State()
313
+ }
314
+ }
@@ -0,0 +1,44 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.api.apdu
7
+
8
+ public data class Apdu internal constructor(
9
+ internal val classInstruction: Byte,
10
+ internal val instructionMethod: Byte,
11
+ internal val parameter1: Byte,
12
+ internal val parameter2: Byte,
13
+ internal val data: ByteArray?,
14
+ internal val dataLength: Int,
15
+ ) {
16
+ override fun equals(other: Any?): Boolean {
17
+ if (this === other) return true
18
+ if (other == null || this::class != other::class) return false
19
+
20
+ other as Apdu
21
+
22
+ if (classInstruction != other.classInstruction) return false
23
+ if (instructionMethod != other.instructionMethod) return false
24
+ if (parameter1 != other.parameter1) return false
25
+ if (parameter2 != other.parameter2) return false
26
+ if (data != null) {
27
+ if (other.data == null) return false
28
+ if (!data.contentEquals(other.data)) return false
29
+ } else if (other.data != null) return false
30
+ if (dataLength != other.dataLength) return false
31
+
32
+ return true
33
+ }
34
+
35
+ override fun hashCode(): Int {
36
+ var result = classInstruction.toInt()
37
+ result = 31 * result + instructionMethod
38
+ result = 31 * result + parameter1
39
+ result = 31 * result + parameter2
40
+ result = 31 * result + (data?.contentHashCode() ?: 0)
41
+ result = 31 * result + dataLength
42
+ return result
43
+ }
44
+ }