Skip to content

Deliver native events to webview screens on Android - #379

Open
gwleuverink wants to merge 5 commits into
NativePHP:mainfrom
trailhead-labs:fix/360-webview-native-events
Open

Deliver native events to webview screens on Android#379
gwleuverink wants to merge 5 commits into
NativePHP:mainfrom
trailhead-labs:fix/360-webview-native-events

Conversation

@gwleuverink

@gwleuverink gwleuverink commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #360.

AppearanceChanged and ShakeDetected were sent with NativeElementBridge.sendNativeEvent, which only feeds the EDGE element queue. Nothing drains that queue on a webview screen, so neither PHP nor the page's JS listener ever heard them. System::appearance() froze on its first read for the life of the process, and shakes vanished entirely.

  • Android webview: appearance stuck at its first value, no #[On], no Event::listen, no native-event in the page
  • Android EDGE: both worked, so the bug looked Android-specific when it is about the surface
  • iOS: appearance worked, shake did not, for a different reason (below)

The fix

sendNativeEvent becomes the one dispatch channel. It writes the element queue as before, then hands the event to an installable WebEventSink:

nativeElementWriteEvent(EventType.NATIVE, 0, 0, buf.array())

if (!eventName.startsWith("__")) {
    webEventSink?.get()?.onNativeEvent(eventName, payloadJson)
}

MainActivity implements the sink and injects into the page through NativeActionCoordinator.dispatchToWebView (CustomEvent, Livewire dispatch, POST to /_native/api/events). It skips while NativeUIBridge.isActive, because an EDGE screen's runloop is already draining the queue and injecting into the page behind it would deliver the same event twice when that page returns.

Fixing the channel rather than the two call sites is deliberate. Every future emitter gets web delivery, and calling the primitive directly can no longer skip the page. __-prefixed signals (__deeplink) stay queue-only, since they exist purely to wake the runloop and carry no PHP event class.

NativeActionCoordinator.dispatch now just calls the channel, so alert and file-picker events stop doing their own separate injection and deliver once instead of twice.

Two smaller pieces

ShakeDetected now implements BroadcastsGlobally, like AppearanceChanged already did. Webview screens reached Event::listen through the POST; EDGE screens only reached #[On] handlers. A shake has no owning component, so the marker closes that gap without relying on the page injection.

On iOS, ShakeDetector called NativeElementBridge.sendNativeEvent directly instead of going through LaravelBridge.send, which ContentView upgrades to the web-injecting closure once a WebView exists. So iOS shake never reached webview screens either. It now routes like every other device event.

Verification

Rig with probes on both surfaces, logging one line per delivery arm with the PHP pid, so a process restart can't be mistaken for a fix.

Android webview, appearance flipped with adb shell cmd uimode night yes then no:

read      pid=25538 api=light raw=light
js-event  pid=25538 event=...AppearanceChanged payload={"mode":"dark"}
php-event pid=25538 source=global-listen mode_payload=dark
read      pid=25538 api=dark  raw=dark
js-event  pid=25538 event=...AppearanceChanged payload={"mode":"light"}
php-event pid=25538 source=global-listen mode_payload=light
read      pid=25538 api=light raw=light

Same pid throughout, so the API tracked the OS rather than being reseeded by a relaunch. Every shake delivers exactly once per arm, on both platforms:

Android  php-events: 1   js-events: 1   (per detection)
iOS      php-events: 1   js-events: 1

EDGE screens are unchanged, verified in the hybrid case too (an EDGE screen over a cached WebView): one #[On] call, no page injection, no duplicate POST. __deeplink still navigates and never reaches the web arm.

Suite is green, 58 passed.

Merge order with #359

Larry's thermal monitor in #359 calls the same primitive, so it inherits this fix without changes to his branch. #359 merges onto this one with no conflicts (MainActivity.kt is the only shared file and it auto-merges), and the combined tree's suite is green including his new thermal tests. Built with both applied, a thermal override now reaches every arm and the cached read follows:

js-event  pid=25861 event=...ThermalStateChanged payload={"state":"hot","previous":"normal"}
php-event pid=25861 source=global-listen
read      pid=25861 api=hot
js-event  pid=25861 event=...ThermalStateChanged payload={"state":"critical","previous":"hot"}
read      pid=25861 api=critical
js-event  pid=25861 event=...ThermalStateChanged payload={"state":"normal","previous":"critical"}
read      pid=25861 api=normal

Before this fix nothing arrived on a webview screen at all. The enum 500 I reported on #359 is gone since his 48074ed. Suggested order: this PR first, then #359 on top.

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.

Native events never reach webview apps on Android (appearance, shake)

1 participant