Skip to content

fix(android): start the permission request on the main thread (#92) - #93

Merged
marcocrupi merged 1 commit into
marcocrupi:mainfrom
pinpong:fix/request-permission-ui-thread
Nov 13, 2026
Merged

marcocrupi merged 1 commit into
marcocrupi:mainfrom
pinpong:fix/request-permission-ui-thread

Conversation

@pinpong

@pinpong pinpong commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Summary

requestPermission() on Android 13+ called activity.requestPermissions(...) on the native modules thread, because TurboModule promise methods run there. Starting that activity walks the view hierarchy (cancelPendingInputEvents), and if the UI thread changes the hierarchy at the same time it can fail with a NullPointerException from View.dispatchCancelPendingInputEvents(). We saw that in production right after a screen transition.

This moves the call, together with its existing try/catch, into UiThreadUtil.runOnUiThread { }. Same idea as Notifee.openNotificationSettings, which already starts its activity with activity.runOnUiThread(...).

Nothing else changes: the callback is still set before the request, results still come back through onRequestPermissionsResult, a throw from requestPermissions still rejects the promise, and the path below API 33 and the missing Activity path stay as they were.

Type of change

  • Bug fix
  • Feature
  • Documentation
  • Refactor
  • Test
  • Build / CI / tooling
  • Other

Related issue

Closes #92

Scope

  • This pull request is focused on one logical change.
  • I have not included unrelated cleanup or formatting changes.
  • I have not changed public APIs, runtime behavior, native configuration, package metadata, or documentation scope unless explicitly intended.

Validation

Added RequestPermissionThreadTest (Robolectric, API 33 and 36). It calls requestPermission from a background thread, checks that requestPermissions isn't called on that thread, idles the main looper, then checks it ran once on the main thread with the right arguments. A second test checks that a throw on the main thread still rejects the promise. The first test fails without the change.

yarn format:all stops at the iOS clang-format step on this machine (x86_64 binary, no Rosetta). The Java formatter ran clean, and no iOS files change.

  • yarn format:all
  • yarn validate:all
  • yarn test:all
  • Manual Android validation
  • Manual iOS validation
  • Not applicable

Manual validation details:

Platform: Android
Device / simulator: Pixel 9a emulator
OS version: Android 17 (API 37)
React Native version: 0.88.1
Scenario tested: our app with the same change as a yarn patch. Debugger breakpoint in ReactActivityDelegate.requestPermissions: thread mqt_v_native before, main after (via Handler / Looper.loop / ActivityThread.main). First request, allow: dialog, resolves AUTHORIZED. After one denial, ask again, deny: dialog, resolves DENIED, rationale false afterwards. Permanently denied: no dialog, resolves DENIED. Already granted: no dialog, resolves AUTHORIZED.

Documentation

  • Documentation was updated.
  • CHANGELOG.md was updated under [Unreleased].
  • Documentation changes are not needed.
  • Changelog entry is not needed because this is not a user-facing product change.

Breaking changes

Does this pull request introduce a breaking change?

  • No
  • Yes

Additional notes

The change is in the bridge (NotifeeApiModule.kt), core is untouched.

I can't reproduce the NPE on demand, it's a race. The test covers the thread, not the crash itself. React Native's own PermissionsAndroid module calls requestPermissions from the modules thread too, so this isn't unique to notify-kit.

@marcocrupi
marcocrupi merged commit 12ccbdc into marcocrupi:main Nov 13, 2026
6 checks passed
@pinpong
pinpong deleted the fix/request-permission-ui-thread branch November 13, 2026 07:55
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.

[Bug] requestPermission() on Android can reject with NPE in View.dispatchCancelPendingInputEvents()

2 participants