Skip to content

fix(android): use bridge.registerForActivityResult for Fragment compatibility - #104

Merged
jcesarmobile merged 1 commit into
ionic-team:mainfrom
omarihamza:fix/android-portals-activityresult-bridge-registration
Oct 16, 2026
Merged

jcesarmobile merged 1 commit into
ionic-team:mainfrom
omarihamza:fix/android-portals-activityresult-bridge-registration

Conversation

@omarihamza

Copy link
Copy Markdown
Contributor

Summary

GeolocationPlugin fails to register during Bridge initialization on Android when the plugin is hosted inside Ionic Portals, making geolocation completely non-functional in Portals flows. iOS is unaffected.

In GeolocationPlugin.load() the plugin registers its ActivityResult launcher directly against the Activity. AndroidX only allows registerForActivityResult while the owner's lifecycle is ≤ CREATED. In a stock BridgeActivity the Bridge is built in the Activity's onCreate() (state CREATED), so this is legal. Under Portals the Bridge is built in PortalFragment.onViewCreated(), by which point the host Activity is already STARTED/RESUMED, so AndroidX throws and Capacitor masks it as a PluginLoadException.

Root cause

NativePlugin com.capacitorjs.plugins.geolocation.GeolocationPlugin failed to load
com.getcapacitor.PluginLoadException: Unable to load plugin instance. Ensure plugin is publicly accessible
    at com.getcapacitor.PluginHandle.load(PluginHandle.java:107)
    ...
Caused by:
java.lang.IllegalStateException: LifecycleOwner <host activity> is attempting to register
while current state is RESUMED. LifecycleOwners must call register before they are STARTED.

Capacitor already exposes Bridge.registerForActivityResult(...), which registers against the hosting Fragment when one exists (the PortalFragment, still CREATED during onViewCreated) and against the Activity otherwise. Every non-rewritten Capacitor plugin uses this path — which is why only the ion*-based plugins fail.

Fix

Change the registration target from the Activity to Capacitor's Bridge in GeolocationPlugin.load():

-        val activityLauncher = activity.registerForActivityResult(
+        val activityLauncher = bridge.registerForActivityResult(
             ActivityResultContracts.StartIntentSenderForResult()
         ) { result ->
             CoroutineScope(Dispatchers.Main).launch {
                 controller.onResolvableExceptionResult(result.resultCode)
             }
         }

The contract, callback, and runtime behavior are identical — only the lifecycle owner of the registration changes (to the Fragment, still CREATED), so the AndroidX guard passes. Under a standard BridgeActivity, Bridge.registerForActivityResult resolves to the Activity, so existing hosting is unaffected.

Steps to reproduce

  1. Host a Portal in a PortalFragment where the containing Activity is already STARTED/RESUMED when the Portal loads.
  2. Register @capacitor/geolocation
  3. Load the Portal and watch Logcat during PortalFragment.onViewCreated → Bridge init.
  4. The plugin fails to register with the trace above; geolocation features do nothing.

Testing

  • npm run verify:android (./gradlew clean build test) — passes: the Bridge.registerForActivityResult swap compiles against Capacitor core, lint reports no new issues, unit tests green.
  • Verified in the bundled example-app on Android (standard BridgeActivity) — permissions, getCurrentPosition, and watchPosition all work; no regression.
  • The StartIntentSenderForResult launcher touched here only fires when location services are off and Google Play Services shows the "turn on location" resolution dialog. Verified under a PortalFragment host that the plugin now registers and this resolution flow works correctly.

Current workaround

We ship a locally patched AAR of the plugin (pinned upstream tag + a tracked one-line source patch, rebuilt from source) that swaps activity.registerForActivityResult → bridge.registerForActivityResult. This resolves the issue, but we'd prefer to remove the fork once an official fix lands.

Notes

  • A sibling fix applies to @capacitor/camera (IonCameraFlow.setupLaunchers(), 6 activity.registerForActivityResult occurrences). IonCameraFlow already receives bridge: Bridge, so no signature change is needed there either.
  • Separately, it would help to improve PluginLoadException in capacitor core to chain the underlying exception — the current "Ensure plugin is publicly accessible" message obscures the real cause and initially misdirects debugging.

…tals compatibility

GeolocationPlugin.load() registered its ActivityResult launcher directly
against the Activity. Under Ionic Portals the Bridge is built in
PortalFragment.onViewCreated() when the host Activity is already
STARTED/RESUMED, so AndroidX's registerForActivityResult guard throws
IllegalStateException and the plugin fails to load (masked by Capacitor
as PluginLoadException).

Register against Capacitor's Bridge instead, which resolves to the
hosting Fragment (still CREATED) under Portals and to the Activity
otherwise. The contract, callback, and runtime behavior are unchanged.
@jcesarmobile jcesarmobile changed the title fix(android): register ActivityResult launcher against Bridge for Ionic Portals compatibility fix(android): use bridge.registerForActivityResult for Fragment compatibility Oct 16, 2026
@jcesarmobile
jcesarmobile merged commit 6a171fe into ionic-team:main Oct 16, 2026
8 checks passed
capacitor-bot pushed a commit that referenced this pull request Oct 16, 2026
## [8.2.4](v8.2.3...v8.2.4) (2026-10-16)

### Bug Fixes

* **android:** use bridge.registerForActivityResult for Fragment compatibility ([#104](#104)) ([6a171fe](6a171fe))
@omarihamza
omarihamza deleted the fix/android-portals-activityresult-bridge-registration branch October 21, 2026 09:38
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.

2 participants