Skip to content

Dispatch MRAID JS bridge calls to the main thread - #1012

Open
mdanylov-sigma wants to merge 2 commits into
masterfrom
claude/prebid-mobile-android-739-4a76d5
Open

mdanylov-sigma wants to merge 2 commits into
masterfrom
claude/prebid-mobile-android-739-4a76d5

Conversation

@mdanylov-sigma

Copy link
Copy Markdown
Collaborator

Closes #739

Problem

WebView calls every @JavascriptInterface method on its background JavaBridge thread, but BaseJSInterface was registered directly and works with views and state owned by the main thread. #736 fixed the crash from #735 by making getCurrentPosition wait for the main thread, with no timeout. The other bridge methods still ran off the main thread, for example:

  • After an orientation change, resize() added a screen-metrics request to ScreenMetricsWaiter's unsynchronized LinkedList while the main thread was removing from it. If that request is lost, nativeCallComplete() never reaches mraid.js, and every later command, close included, stays queued.
  • javaScriptCallback ran its Handler on the bridge thread. That is only safe today because every caller passes FetchPropertiesHandler, which re-posts to the main thread.
  • Every command wrote to one shared MraidEvent that main-thread code read later.

Changes

1. MainThreadJSInterface is registered as jsBridge for banners and interstitials

  • MRAID commands (open, close, expand, resize, …) are posted to the main thread. mraid.js already waits for nativeCallComplete() between commands, so it doesn't rely on these calls returning synchronously.
  • Getters run directly when called on the main thread. From the bridge thread they wait up to 250 ms for the main thread, and return the existing error value ({}, or -1 for getLocation) if it doesn't answer in time.
  • getPlacementType and supports don't read shared state, so they answer on the calling thread.
  • A command that throws is logged instead of crashing the app on the main thread, and still sends nativeCallComplete() so the ad's command queue keeps moving.
  • Calls that reach the main thread after BaseJSInterface.destroy() are ignored.
  • The wait that Run getGlobalVisibleRect on the main thread instead of a background thread #736 added inside getCurrentPosition is removed, since the method now always runs on the main thread.
  • mraid.js and the jsBridge name are unchanged.

2. A new MraidEvent for each command (separate commit)

BaseJSInterface reused one MraidEvent for every command. MraidController reads it later, in posted runnables and completion callbacks, and two-part expand stores it for InterstitialManager. A later command could overwrite the action or URL before that code read it. Each command now creates its own event.

Tests

  • MainThreadJSInterfaceTest: getters from the main and a background thread, the timeout fallback, command order, commands that throw, calls after destroy, and a reflection check that every @JavascriptInterface method of BaseJSInterface is exposed on the bridge.
  • WebViewBannerTest and WebViewInterstitialTest: the registered jsBridge is a MainThreadJSInterface.
  • BaseJSInterfaceTest: event checks capture the event passed to MraidController instead of reading the removed field; new checks that consecutive commands get separate events and that destroy() sets the flag.
  • ./gradlew :PrebidMobile-core:testReleaseUnitTest passes locally: 1,313 tests, no failures (JDK 21).

Questions for reviewers

  • Is 250 ms the right getter timeout?
  • Should a getter that times out return the last known value instead of {}?

🤖 Generated with Claude Code

mdanylov-sigma and others added 2 commits September 15, 2026 22:13
WebView calls JavaScript interfaces on its JavaBridge background thread,
but BaseJSInterface works with views and state owned by the main thread
(the crash in #735 was one symptom). Register MainThreadJSInterface as
"jsBridge" instead: it posts MRAID commands to the main thread and waits
up to 250 ms for getters, returning the existing error value when the
main thread does not answer in time.

This replaces the unbounded main-thread wait that #736 added to
getCurrentPosition. Commands that throw are logged and still release the
mraid.js command queue, and calls that reach the main thread after
destroy() are ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BaseJSInterface reused one MraidEvent for every command, but
MraidController reads it later in posted runnables and completion
callbacks, and two-part expand stores it for InterstitialManager. A later
command could overwrite the action or URL before that code read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

Wrap Managed WebView in background-thread-safe-invocation wrapper so JS Bridge doesn't throw Errors

1 participant