feat(android): drop the MainActivity permission delegate requirement - #266
feat(android): drop the MainActivity permission delegate requirement#266matinzd wants to merge 8 commits into
Conversation
Permission and exercise route dialogs are now launched from a transparent activity the library declares in its own manifest, so consumers no longer need to call HealthConnectPermissionDelegate.setPermissionDelegate(this). The contracts cannot be launched with a plain startActivityForResult: on Android 14+ the permission contract produces a synthetic intent that only an ActivityResultRegistry can service, so hosting our own ComponentActivity is what makes this work without touching the host activity. HealthConnectPermissionDelegate is kept as a deprecated no-op, and the Expo ReactActivityLifecycleListener that used to register it is removed. Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
It existed only to register HealthConnectPermissionDelegate from a ReactActivityLifecycleListener. Permission dialogs no longer need anything registered on the host activity, so the module had nothing left to do. Expo apps now autolink android/ like any other React Native package; the config plugin in app.plugin.js is unaffected. Verified with a local expo prebuild, ./gradlew projects and a debug APK dex check. Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
…atinzd/react-native-health-connect into feat/permissions-without-main-activity
They belong to their own PR; only the implementation notes under .claude/plans/ are specific to this change. Co-Authored-By: Claude Opus 5 <[email protected]>
There was a problem hiding this comment.
🟡 Not ready to approve
The new dialog launcher currently has a confirmed race risk around in-flight request guarding (can overwrite the pending request and hang promises) and needs the requested concurrency/validation fixes before it’s safe to merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Removes the requirement for consumers to wire HealthConnectPermissionDelegate into MainActivity by moving Health Connect permission/exercise-route dialog launching into a library-declared transparent ComponentActivity, and cleans up the old Expo-native-module path and related docs/CI.
Changes:
- Introduces
HealthConnectPermissionActivity+HealthConnectPermissionLauncherto host Activity Result contracts inside the library (no hostMainActivityintegration). - Deprecates
HealthConnectPermissionDelegateinto a no-op; updatesHealthConnectManagerto use the new launcher and adds new exception codes. - Removes the
android-expo/Expo module + config, and updates README/docs/examples/CI accordingly.
File summaries
| File | Description |
|---|---|
| README.md | Removes MainActivity setup instructions; documents the new no-setup behavior and upgrade note. |
| package.json | Stops publishing android-expo/ and expo-module.config.json artifacts. |
| expo-module.config.json | Removed (Expo native module config no longer needed). |
| example/android/app/src/main/java/com/healthconnectexample/MainActivity.kt | Removes now-unnecessary setPermissionDelegate call. |
| example-expo/README.md | Updates Expo example explanation to match “no Expo native module” approach. |
| docs/docs/get-started.md | Mirrors README updates for “no MainActivity changes” and migration notes. |
| android/src/main/java/dev/matinzd/healthconnect/utils/ExceptionsUtils.kt | Adds two new exception types + error codes for new failure modes. |
| android/src/main/java/dev/matinzd/healthconnect/permissions/HealthConnectPermissionLauncher.kt | New launcher that starts the library-owned activity and awaits results. |
| android/src/main/java/dev/matinzd/healthconnect/permissions/HealthConnectPermissionDelegate.kt | Converts delegate to deprecated no-op for backwards compile compatibility. |
| android/src/main/java/dev/matinzd/healthconnect/permissions/HealthConnectPermissionActivity.kt | New transparent activity owning the ActivityResultRegistry and contracts. |
| android/src/main/java/dev/matinzd/healthconnect/HealthConnectManager.kt | Switches permission/route requests to the new launcher; stores provider package name. |
| android/src/main/AndroidManifest.xml | Declares the new internal transparent activity for manifest-merge into host apps. |
| android-expo/src/main/java/expo/modules/healthconnect/HealthConnectPermissionReactActivityHandler.kt | Removed (Expo lifecycle registration no longer needed). |
| android-expo/src/main/java/expo/modules/healthconnect/HealthConnectPackage.kt | Removed (Expo native module removed). |
| android-expo/src/main/AndroidManifest.xml | Removed (Expo module removed). |
| android-expo/build.gradle | Removed (Expo module removed). |
| .github/workflows/ci.yml | Updates CI to validate new Expo/bare linking expectations and that new activity is packaged. |
| .claude/plans/activity-result-without-main-activity.md | Adds design/verification plan documenting rationale and trade-offs for the new approach. |
Review details
Suppressed comments (1)
android/src/main/java/dev/matinzd/healthconnect/permissions/HealthConnectPermissionLauncher.kt:58
- Clearing
requestInFlightand setting the pending request should be coordinated with the same lock used to guard the in-flight check; otherwise concurrent calls can still interleave and overwritependingRequestbefore the activity consumes it.
return try {
deferred.await()
} finally {
requestInFlight = false
}
- Files reviewed: 18/18 changed files
- Comments generated: 2
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| when (intent.getStringExtra(EXTRA_REQUEST_TYPE)) { | ||
| REQUEST_TYPE_PERMISSIONS -> requestPermissions.launch( | ||
| intent.getStringArrayListExtra(EXTRA_PERMISSIONS)?.toSet() ?: emptySet() | ||
| ) | ||
|
|
||
| REQUEST_TYPE_EXERCISE_ROUTE -> requestExerciseRoute.launch( | ||
| intent.getStringExtra(EXTRA_RECORD_ID) ?: "" | ||
| ) | ||
|
|
||
| else -> finish() | ||
| } |
There was a problem hiding this comment.
I'd go a step further, validate that the record id is not empty before you even get to this step, and bail out (throw out a log.w telling the dev that there was something wrong and gracefully finish)
| private var requestInFlight = false | ||
|
|
||
| suspend fun requestPermissions( | ||
| providerPackageName: String, permissions: Set<String> | ||
| ): Set<String> { | ||
| val deferred = CompletableDeferred<Set<String>>() | ||
|
|
||
| start(PendingHealthConnectRequest.Permissions(deferred)) { | ||
| putExtra( | ||
| HealthConnectPermissionActivity.EXTRA_REQUEST_TYPE, | ||
| HealthConnectPermissionActivity.REQUEST_TYPE_PERMISSIONS | ||
| ) | ||
| putExtra( | ||
| HealthConnectPermissionActivity.EXTRA_PROVIDER_PACKAGE_NAME, providerPackageName | ||
| ) | ||
| putStringArrayListExtra( | ||
| HealthConnectPermissionActivity.EXTRA_PERMISSIONS, ArrayList(permissions) | ||
| ) | ||
| } | ||
|
|
||
| return try { | ||
| deferred.await() | ||
| } finally { | ||
| requestInFlight = false | ||
| } |
|
oooooh let me take a look |
Ehhhhhh, not sure about this bud. Google has been going on a campaign of "Single activity for android apps" for quite some time now. https://developer.android.com/topic/architecture#app_composition |
|
|
||
| Bare React Native consumers used to be required to edit `MainActivity`: | ||
|
|
||
| ```kotlin | ||
| HealthConnectPermissionDelegate.setPermissionDelegate(this) | ||
| ``` | ||
|
|
||
| That existed purely because `ComponentActivity.registerForActivityResult` must be called **before | ||
| the activity reaches STARTED**. A native module is constructed far too late to do it itself, so the | ||
| library had to borrow the host's activity. Expo projects sidestepped this with a | ||
| `ReactActivityLifecycleListener` (`android-expo/.../HealthConnectPermissionReactActivityHandler.kt`); | ||
| bare RN has no equivalent hook. |
There was a problem hiding this comment.
Ah, so it's this conversation again. Ok i at least understand why you want to do it, but again, i am not 100% sure having a separate activity is the right way to go for it. hmmm
There was a problem hiding this comment.
What was the consensus from the RN core team on your old PR for adding this to core? too much overhead?
There was a problem hiding this comment.
Yep. There wasn't enough bandwidth to move this forward in React Native core at the time.
I think I'll need to start digging into RN core myself and see if I can help get this over the line.
There was a problem hiding this comment.
This is how it is done with expo modules:
And how its handled in ModuleRegistry:
And the docs:
https://docs.expo.dev/modules/module-api/#registeractivitycontracts
There was a problem hiding this comment.
Okay :)
Let's see if this PR lands: react/react-native#57798
TheRogue76
left a comment
There was a problem hiding this comment.
NGL, i would prefer to keep the MainActivity requirement around for the Bare apps. It's better than altering an applications general architecture (as someone who has had to deal with libraries doing sneaky things with their own activity, those usually suck, they sometimes mess up deep links, and they also sometimes mess up other things)
Couldn't agree more if there was a way out in bare RN. |
|
Depends on: react/react-native#57798 Let's see if it lands in core. |
Consumers no longer need to call
HealthConnectPermissionDelegate.setPermissionDelegate(this)fromMainActivity. Permission and exercise route dialogs are launched from a transparent activity this library declares in its own manifest, which merges into the app automatically.The obvious alternative — building the intent from the contract and using
startActivityForResult— does not work: on Android 14+ the permission contract produces a synthetic intent that only anActivityResultRegistrycan service, so it fails withActivityNotFoundException. Hosting our ownComponentActivityprovides that registry.HealthConnectPermissionDelegateremains as a deprecated no-op so existingMainActivitycode keeps compiling. The ExpoReactActivityLifecycleListenerthat used to register it is removed, along with the setup steps in the README and docs site.Testing
Verified on an API 36 emulator with the bare example (no
setPermissionDelegateanywhere): initialize succeeds, the real Health Connect consent screen appears, granting returns the permission list to JS, and a repeat request works. Manifest merge confirmed in the app's merged manifest.🤖 Generated with Claude Code