Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (rn-hid) [DSDK-611]: React Native HID transport #662

Draft
wants to merge 19 commits into
base: feat/dsdk-610-rn-mobile-ble
Choose a base branch
from

Conversation

ofreyssinet-ledger
Copy link
Contributor

@ofreyssinet-ledger ofreyssinet-ledger commented Feb 5, 2025

πŸ“ Description

  • Android: Copy paste code of Android HID transport from the Mobile DMK.
  • Android+TS: Implement RNHIDTransport with a Native module + the TS layer on top.
  • Sample app: Add a screen for testing the RN HID transport (temporary until we have a fully working sample app)

❓ Context

  • Feature:

βœ… Checklist

Pull Requests must pass CI checks and undergo code review. Set the PR as Draft if it is not yet ready for review.

  • Covered by automatic tests
  • Changeset is provided
  • Impact of the changes:
    • list of the changes

🧐 Checklist for the PR Reviewers

  • The code aligns with the requirements described in the linked JIRA or GitHub issue.
  • The PR description clearly documents the changes made and explains any technical trade-offs or design decisions.
  • There are no undocumented trade-offs, technical debt, or maintainability issues.
  • The PR has been tested thoroughly, and any potential edge cases have been considered and handled.
  • Any new dependencies have been justified and documented.

Copy link

vercel bot commented Feb 5, 2025

The latest updates on your projects. Learn more about Vercel for Git β†—οΈŽ

Name Status Preview Comments Updated (UTC)
device-sdk-ts-sample βœ… Ready (Inspect) Visit Preview πŸ’¬ Add feedback Feb 21, 2025 11:56am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
doc-device-management-kit ⬜️ Ignored (Inspect) Visit Preview Feb 21, 2025 11:56am

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporary changes just to test the transport

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘

Copy link

@smartine-ledger smartine-ledger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GG. Just a few comments πŸ˜‰

}
else -> {}
}
}.launchIn(CoroutineScope(Dispatchers.Default))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT] You never stop collecting the flow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done βœ…

sendEvent(reactContext, BridgeEvents.TransportLog(info))
}

private val transport: AndroidUsbTransport? by lazy {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ASK] Why is it nullable. In addition you never set it to null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT] As it's lazy I ask myself if it'd be better to create all the instances in the init{} bloc instead of when we need the transport (meaning when calling the startScan() method) to avoid losing timeπŸ€”

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nullable because reactContext.currentActivity can be null so in that case it's set to null.
The lazy initialization could be useful in case the DMK feature flag isn't enabled in LL. In that case we save some resources. But I'm not 100% sure it's necessary, I can also do that stuff in init...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so I checked, the activity is null when TransportHidModule is initialized. React Native's lifecycle is like that πŸ€·β€β™‚οΈ
Looking at the implementation of ReactContext, I can assume that activity won't be null when onResume is called... So I could do the initialization there.
https://github.com/facebook/react-native/blob/f9bffef6c78091d3e89da5e3abba87d1b30b0b69/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/ReactContext.java#L257-L261

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘

InternalDeviceDisconnected,
} from "@api/transport/types";

export class StubNativeModuleWrapper implements NativeModuleWrapper {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ASK] Useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes for iOS. It would crash on iOS if we use DefaultNativeModuleWrapper as the native module does not exist in that case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘

Comment on lines 111 to 187
val establishConnectionFn = {
val sessionId = generateSessionId(id = device.deviceId.toString())
val newConnection =
AndroidUsbDeviceConnection(
usbManager = usbManager,
usbDevice = device,
ioDispatcher = Dispatchers.IO,
framerService = FramerService(loggerService),
request = UsbRequest(),
)
}.onEach {
loggerService.log(info = buildSimpleInfoLogInfo(
tag = "DefaultUsbTransport",
message = "event received while connecting : $it")
val connectedDevice =
InternalConnectedDevice(
sessionId,
discoveryDevice.name,
discoveryDevice.ledgerDevice,
discoveryDevice.connectivityType,
sendApduFn = { apdu -> newConnection.send(apdu) },
)
}.first {
it is UsbPermissionEvent.PermissionGranted ||
it is UsbPermissionEvent.PermissionDenied ||
it is UsbState.Detached
}
usbConnections[sessionId] = newConnection
InternalConnectionResult.Connected(device = connectedDevice, sessionId = sessionId)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT] Can be simplify without using a lambda and so creating a anonymous object :

 override suspend fun connect(discoveryDevice: DiscoveryDevice): InternalConnectionResult {
    .....
    return if (device == null) {
            InternalConnectionResult.ConnectionError(error = InternalConnectionResult.Failure.DeviceNotFound)
        } else {
            val sessionId = generateSessionId(id = device.deviceId.toString())
            val newConnection =
                AndroidUsbDeviceConnection(
                    usbManager = usbManager,
                    usbDevice = device,
                    framerService = FramerService(loggerService),
                    loggerService = loggerService,
                    request = UsbRequest(),
                    ioDispatcher = Dispatchers.IO,
                )
            val connectedDevice =
                InternalConnectedDevice(
                    sessionId,
                    discoveryDevice.name,
                    discoveryDevice.ledgerDevice,
                    discoveryDevice.connectivityType,
                    sendApduFn = { apdu -> newConnection.send(apdu) },
                )
            usbConnections[sessionId] = newConnection
            val establishConnection = InternalConnectionResult.Connected(device = connectedDevice, sessionId = sessionId)

            if (usbManager.hasPermission(device)) {
                return establishConnection
            }
            .....
}

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end I extracted the permission check/request logic into a checkAndRequestPermission method as it's reused now when a device gets attached (for reconnecting it)

Comment on lines 6 to 12
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android < 12 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- common -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[ASK] Why do you need these permissions? Don't you just implement the USB transport?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is coming from the previous commits which my branch is based on, I need them because it includes the base for the sample app. And the sample app supports BLE, hence those permissions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. My point here was : why don't you add these permissions in the transport/rn-hid/android/src/main instead of the app πŸ™‚

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean in rn-ble ? In the rn-ble package, there is no native code, we use the react-native-ble-plx which implements the native part. So we don't have control over the AndroidManifest there.

Comment on lines 53 to 78
android {
namespace "com.ledger.androidtransporthid"
compileSdkVersion safeExtGet("compileSdkVersion", 34)

defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 30)
targetSdkVersion safeExtGet("targetSdkVersion", 32)
versionCode 1
versionName "1.0"
}

compileOptions {
// Use JavaVersion enums for compatibility
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = jvmTargetVersion
}

lintOptions {
abortOnError false
warning 'InvalidPackage'
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT] be careful. It turns out you don't use the same config that the android/app

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean exactly? You're talking about compileSdkVersion minSdkVersion and targetSdkVersion?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you just need to create and use an constant πŸ˜‰

return
}

CoroutineScope(Dispatchers.Default).launch {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[COMMENT] Prefer having only one scope as parameter of the class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, done βœ…

Copy link
Contributor

github-actions bot commented Feb 14, 2025

Messages
βœ… Danger: All checks passed successfully! πŸŽ‰

Generated by 🚫 dangerJS against bce3df6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants