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

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 (70) hide show
  1. package/android/.settings/org.eclipse.buildship.core.prefs +13 -0
  2. package/android/build.gradle +99 -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 +304 -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 +55 -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 +96 -0
  30. package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +311 -0
  31. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/DeviceAction.kt +23 -0
  32. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
  33. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
  34. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
  35. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
  36. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
  37. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
  38. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
  39. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
  40. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +74 -0
  41. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +6 -0
  42. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
  43. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
  44. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
  45. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
  46. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
  47. package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
  48. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
  49. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
  50. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
  51. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
  52. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
  53. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
  54. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
  55. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
  56. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
  57. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
  58. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
  59. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
  60. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
  61. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
  62. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
  63. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
  64. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
  65. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
  66. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
  67. package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
  68. package/lib/cjs/package.json +1 -0
  69. package/lib/esm/package.json +1 -0
  70. package/package.json +5 -4
@@ -0,0 +1,11 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.androidMain.transport.usb.model
7
+
8
+ @JvmInline
9
+ public value class ProductId(
10
+ public val id: Int,
11
+ )
@@ -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,55 @@
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.api.device.LedgerDevice
9
+ import com.ledger.devicesdk.shared.androidMain.transport.usb.model.ProductId
10
+
11
+ internal fun ProductId.toLedgerDevice(): LedgerDevice? =
12
+ when {
13
+ this.id.isLedgerDeviceProductId(LedgerDevice.NanoS)
14
+ -> {
15
+ LedgerDevice.NanoS
16
+ }
17
+
18
+ this.id.isLedgerDeviceProductId(LedgerDevice.NanoSPlus)
19
+ -> {
20
+ LedgerDevice.NanoSPlus
21
+ }
22
+
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
+ private fun Int.isLedgerDeviceProductId(device: LedgerDevice): Boolean {
44
+ val productId = device.usbInfo.productIdMask.sdkHexToInt()
45
+ val shiftedId = this shr 8
46
+ return shiftedId == productId
47
+ }
48
+
49
+ @OptIn(ExperimentalStdlibApi::class)
50
+ public fun String.sdkHexToInt(withPrefix: Boolean = true): Int =
51
+ if (withPrefix) {
52
+ this.substring(2).hexToInt()
53
+ } else {
54
+ this.hexToInt()
55
+ }
@@ -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 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,96 @@
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.CoroutineScope
7
+ import kotlinx.coroutines.launch
8
+ import kotlin.coroutines.resume
9
+ import kotlin.coroutines.suspendCoroutine
10
+ import kotlin.time.Duration
11
+
12
+ internal class DeviceConnection<Dependencies>(
13
+ val sessionId: String,
14
+ private var deviceApduSender: DeviceApduSender<Dependencies>,
15
+ isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
16
+ reconnectionTimeoutDuration: Duration,
17
+ private val onTerminated: (DeviceConnection<Dependencies>) -> Unit,
18
+ private val coroutineScope: CoroutineScope,
19
+ private val loggerService: LoggerService,
20
+ ) {
21
+ private val stateMachine: DeviceConnectionStateMachine
22
+
23
+ init {
24
+ stateMachine = DeviceConnectionStateMachine(
25
+ sendApduFn = {
26
+ coroutineScope.launch {
27
+ val res = deviceApduSender.send(it)
28
+ handleApduResult(res)
29
+ }
30
+ },
31
+ onTerminated = {
32
+ onTerminated(this)
33
+ },
34
+ isFatalSendApduFailure = isFatalSendApduFailure,
35
+ reconnectionTimeoutDuration = reconnectionTimeoutDuration,
36
+ coroutineScope = coroutineScope,
37
+ onError = {
38
+ loggerService.log(
39
+ buildSimpleErrorLogInfo(
40
+ "DeviceConnection",
41
+ "State machine error $it"
42
+ )
43
+ )
44
+ },
45
+ loggerService = loggerService,
46
+ )
47
+ }
48
+
49
+
50
+ private fun handleApduResult(result: SendApduResult) {
51
+ stateMachine.handleApduResult(result)
52
+ }
53
+
54
+ public fun setApduSender(apduSender: DeviceApduSender<Dependencies>) {
55
+ deviceApduSender = apduSender
56
+ }
57
+
58
+ public fun getApduSender(): DeviceApduSender<Dependencies> {
59
+ return deviceApduSender
60
+ }
61
+
62
+ public fun handleDeviceConnected() {
63
+ stateMachine.handleDeviceConnected()
64
+ }
65
+
66
+ public fun handleDeviceDisconnected() {
67
+ stateMachine.handleDeviceDisconnected()
68
+ }
69
+
70
+ public suspend fun requestSendApdu(apdu: ByteArray): SendApduResult =
71
+ suspendCoroutine { cont ->
72
+ stateMachine.requestSendApdu(
73
+ DeviceConnectionStateMachine.SendApduRequestContent(
74
+ apdu = apdu,
75
+ triggersDisconnection = apduTriggersDisconnection(apdu),
76
+ resultCallback = cont::resume
77
+ )
78
+ )
79
+ }
80
+
81
+ public fun requestCloseConnection() {
82
+ stateMachine.requestCloseConnection()
83
+ }
84
+
85
+ private fun apduTriggersDisconnection(apdu: ByteArray): Boolean {
86
+ val apduMap = mapOf(
87
+ "openApp" to byteArrayOf(0xe0.toByte(), 0xd8.toByte(), 0x00, 0x00),
88
+ "closeApp" to byteArrayOf(0xb0.toByte(), 0xa7.toByte(), 0x00, 0x00)
89
+ )
90
+
91
+ return apduMap.values.any { known ->
92
+ (0 until 4).all { i -> known[i] == apdu[i] }
93
+ }
94
+ }
95
+
96
+ }
@@ -0,0 +1,311 @@
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.CoroutineScope
9
+ import kotlinx.coroutines.Job
10
+ import kotlinx.coroutines.launch
11
+ import kotlin.time.Duration
12
+
13
+ internal class DeviceConnectionStateMachine(
14
+ private val sendApduFn: (apdu: ByteArray) -> Unit,
15
+ private val onTerminated: () -> Unit,
16
+ private val isFatalSendApduFailure: (SendApduResult.Failure) -> Boolean,
17
+ private val reconnectionTimeoutDuration: Duration,
18
+ private val coroutineScope: CoroutineScope,
19
+ private val onError: (Throwable) -> Unit,
20
+ private val loggerService: LoggerService,
21
+ ) {
22
+
23
+ private var state: State = State.Connected
24
+
25
+ private fun pushState(newState: State) {
26
+ when (newState) {
27
+ is State.Connected -> {}
28
+ is State.SendingApdu -> {
29
+ sendApduFn(newState.requestContent.apdu)
30
+ }
31
+
32
+ is State.WaitingForReconnection -> {
33
+ startReconnectionTimeout()
34
+ }
35
+
36
+ is State.WaitingForReconnectionWithQueuedApdu -> {}
37
+ is State.Terminated -> {
38
+ onTerminated()
39
+ }
40
+ }
41
+ this.state = newState
42
+ }
43
+
44
+ private fun handleEvent(event: Event) {
45
+ val currentState = state
46
+ when (currentState) {
47
+ is State.Connected -> {
48
+ when (event) {
49
+ is Event.SendApduRequested -> {
50
+ pushState(State.SendingApdu(event.requestContent))
51
+ }
52
+
53
+ is Event.CloseConnectionRequested -> {
54
+ pushState(State.Terminated)
55
+ }
56
+
57
+ is Event.DeviceDisconnected -> {
58
+ pushState(State.WaitingForReconnection)
59
+ }
60
+
61
+ else -> {
62
+ onError(Exception("Unhandled event: $event in state: $currentState"))
63
+ }
64
+ }
65
+ }
66
+
67
+ is State.SendingApdu -> {
68
+ when (event) {
69
+ is Event.ApduResultReceived -> {
70
+ when (event.result) {
71
+ is SendApduResult.Failure -> {
72
+ if (isFatalSendApduFailure(event.result)) {
73
+ pushState(State.Terminated)
74
+ } else {
75
+ pushState(State.Connected)
76
+ }
77
+ }
78
+
79
+ is SendApduResult.Success -> {
80
+ // check if last 2 bytes of APDU are [0x90,OxO0]
81
+ val apdu = event.result.apdu
82
+ val apduSize = apdu.size
83
+ val isSuccessApdu =
84
+ apdu.size >= 2 &&
85
+ apdu[apduSize - 2] == 0x90.toByte() &&
86
+ apdu[apduSize - 1] == 0x00.toByte()
87
+
88
+ if (isSuccessApdu && currentState.requestContent.triggersDisconnection) {
89
+ pushState(State.WaitingForReconnection)
90
+ } else {
91
+ pushState(State.Connected)
92
+ }
93
+ }
94
+ }
95
+ currentState.requestContent.resultCallback(event.result)
96
+ }
97
+
98
+ is Event.CloseConnectionRequested -> {
99
+ pushState(State.Terminated)
100
+ currentState.requestContent.resultCallback(
101
+ SendApduResult.Failure(
102
+ SendApduFailureReason.DeviceDisconnected
103
+ )
104
+ )
105
+ }
106
+
107
+ is Event.DeviceDisconnected -> {
108
+ pushState(State.WaitingForReconnection)
109
+ currentState.requestContent.resultCallback(
110
+ SendApduResult.Failure(
111
+ SendApduFailureReason.DeviceDisconnected
112
+ )
113
+ )
114
+ }
115
+
116
+ is Event.SendApduRequested -> {
117
+ event.requestContent.resultCallback(
118
+ SendApduResult.Failure(
119
+ SendApduFailureReason.DeviceBusy
120
+ )
121
+ )
122
+ }
123
+
124
+ else -> {
125
+ onError(Exception("Unhandled event: $event in state: $currentState"))
126
+ }
127
+ }
128
+ }
129
+
130
+ is State.WaitingForReconnection -> {
131
+ when (event) {
132
+ is Event.DeviceConnected -> {
133
+ pushState(State.Connected)
134
+ cancelReconnectionTimeout()
135
+ }
136
+
137
+ is Event.SendApduRequested -> {
138
+ pushState(State.WaitingForReconnectionWithQueuedApdu(event.requestContent))
139
+ }
140
+
141
+ is Event.WaitingForReconnectionTimedOut,
142
+ is Event.CloseConnectionRequested -> {
143
+ pushState(State.Terminated)
144
+ cancelReconnectionTimeout()
145
+ }
146
+
147
+ is Event.DeviceDisconnected -> {
148
+ /**
149
+ * Do nothing, this will happen if we send an apdu that triggers a
150
+ * disconnection, because we will move to this state before the disconnection
151
+ * is detected:
152
+ *
153
+ * 1. APDU that triggers a disconnection is sent.
154
+ * 2. We receive a 0x9000 (success) response.
155
+ * -> We go to State.WaitingForReconnection.
156
+ * 3. Device disconnection is finally detected:
157
+ * -> Event.DeviceDisconnected is received here.
158
+ */
159
+ }
160
+
161
+ else -> {
162
+ onError(Exception("Unhandled event: $event in state: $currentState"))
163
+ }
164
+ }
165
+ }
166
+
167
+ is State.WaitingForReconnectionWithQueuedApdu -> {
168
+ when (event) {
169
+ is Event.DeviceConnected -> {
170
+ pushState(State.SendingApdu(currentState.requestContent))
171
+ cancelReconnectionTimeout()
172
+ }
173
+
174
+ is Event.CloseConnectionRequested,
175
+ is Event.WaitingForReconnectionTimedOut -> {
176
+ pushState(State.Terminated)
177
+ currentState.requestContent.resultCallback(
178
+ SendApduResult.Failure(
179
+ SendApduFailureReason.DeviceDisconnected
180
+ )
181
+ )
182
+ cancelReconnectionTimeout()
183
+ }
184
+
185
+ is Event.SendApduRequested -> {
186
+ event.requestContent.resultCallback(
187
+ SendApduResult.Failure(
188
+ SendApduFailureReason.DeviceBusy
189
+ )
190
+ )
191
+ }
192
+
193
+ is Event.DeviceDisconnected -> {
194
+ /**
195
+ * Do nothing, this will happen if we send an apdu that triggers a
196
+ * disconnection, because we will move to this state before the disconnection
197
+ * is detected:
198
+ *
199
+ * 1. APDU that triggers a disconnection is sent.
200
+ * 2. We receive a 0x9000 (success) response
201
+ * -> We go to State.WaitingForReconnection in anticipation of the disconnection event.
202
+ * 3. We receive Event.SendApduRequested
203
+ * -> We go to WaitingForReconnectionWithQueuedApdu
204
+ * 4. Device disconnection is finally detected:
205
+ * -> Event.DeviceDisconnected is received here.
206
+ *
207
+ * It can also happen if the device is disconnected while we are sending an APDU.
208
+ * cf. description of event below.
209
+ */
210
+ }
211
+
212
+ is Event.ApduResultReceived -> {
213
+ /**
214
+ * Do nothing, this will happen if while an APDU is being sent,
215
+ * the device disconnection is detected.
216
+ * 1. APDU is sent
217
+ * 2. Device disconnection is detected
218
+ * -> Event.DeviceDisconnected is received in SendingApdu state.
219
+ * -> We move to WaitingForReconnection state.
220
+ * 3. The function to send the APDU returns an error because the device is disconnected.
221
+ * -> Event.ApduResultReceived(result=Failure()) is received in the
222
+ * current state.
223
+ *
224
+ * It's a race condition between step 2 and 3.
225
+ */
226
+ }
227
+ }
228
+ }
229
+
230
+ is State.Terminated -> {
231
+ onError(Exception("Unhandled event: $event in state: $currentState"))
232
+ }
233
+ }
234
+ val logMessage = """
235
+ Received event:
236
+ In state: $currentState
237
+ -> Event: $event
238
+ -> New state: $state
239
+ """.trimIndent()
240
+ loggerService.log(buildSimpleDebugLogInfo("DeviceConnectionStateMachine", logMessage))
241
+ }
242
+
243
+ private var timeoutJob: Job? = null
244
+ private fun startReconnectionTimeout() {
245
+ // start a timeout and at the end, emit a WaitingForReconnectionTimedOut event
246
+ timeoutJob = coroutineScope.launch {
247
+ kotlinx.coroutines.delay(reconnectionTimeoutDuration)
248
+ handleEvent(Event.WaitingForReconnectionTimedOut)
249
+ }
250
+ }
251
+
252
+ private fun cancelReconnectionTimeout() {
253
+ timeoutJob?.cancel()
254
+ timeoutJob = null
255
+ }
256
+
257
+ public fun requestSendApdu(requestContent: SendApduRequestContent) {
258
+ handleEvent(Event.SendApduRequested(requestContent))
259
+ }
260
+
261
+ public fun requestCloseConnection() {
262
+ handleEvent(Event.CloseConnectionRequested)
263
+ }
264
+
265
+ public fun handleApduResult(result: SendApduResult) {
266
+ handleEvent(Event.ApduResultReceived(result))
267
+ }
268
+
269
+ public fun handleDeviceConnected() {
270
+ handleEvent(Event.DeviceConnected)
271
+ }
272
+
273
+ public fun handleDeviceDisconnected() {
274
+ handleEvent(Event.DeviceDisconnected)
275
+ }
276
+
277
+ data class SendApduRequestContent(
278
+ val apdu: ByteArray,
279
+ val triggersDisconnection: Boolean,
280
+ val resultCallback: (SendApduResult) -> Unit
281
+ )
282
+
283
+ sealed class Event {
284
+ data object DeviceConnected : Event()
285
+
286
+ data object DeviceDisconnected : Event()
287
+
288
+ data class SendApduRequested(
289
+ val requestContent: SendApduRequestContent
290
+ ) : Event()
291
+
292
+ data object CloseConnectionRequested : Event()
293
+
294
+ data class ApduResultReceived(val result: SendApduResult) : Event()
295
+
296
+ data object WaitingForReconnectionTimedOut : Event()
297
+ }
298
+
299
+ sealed class State {
300
+ data object Connected : State()
301
+
302
+ data class SendingApdu(val requestContent: SendApduRequestContent) : State()
303
+
304
+ data object WaitingForReconnection : State()
305
+
306
+ data class WaitingForReconnectionWithQueuedApdu(val requestContent: SendApduRequestContent) :
307
+ State()
308
+
309
+ data object Terminated : State()
310
+ }
311
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2024 Ledger SAS
3
+ * SPDX-License-Identifier: LicenseRef-LEDGER
4
+ */
5
+
6
+ package com.ledger.devicesdk.shared.api
7
+
8
+ public sealed class DeviceAction {
9
+ public data object GetOsVersion : DeviceAction()
10
+
11
+ public data object GetAppAndVersion : DeviceAction()
12
+
13
+ public data class InstallApplication(
14
+ val appName: String,
15
+ ) : DeviceAction()
16
+
17
+ public data object OsUpdate : DeviceAction()
18
+
19
+ // fixme will be uncommented once theses device actions be concretely implemented
20
+ // public data object OpenApplication : DeviceAction()
21
+ //
22
+ // public data object UpdateApplication : DeviceAction()
23
+ }
@@ -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
+ }