Hi WebUI X Team,
I’m a PhD student researching Android thread-related issues. My research group recently ran a static analysis scan for thread-related bugs in real-world Android apps, and our prototype flagged a potential issue in Webui X.
Checked target
- Source-level caller:
com.dergoogler.mmrl.webui.interfaces.ApplicationInterface.exit()
- Detected API / pattern:
@JavascriptInterface callback directly reaches Activity.finish()
- Observed context: WebView JavaScript-interface background thread
- Expected context: main/UI thread before Activity lifecycle operations
What I found
WXView registers ApplicationInterface as one of the built-in JavaScript interfaces:
private fun addJavascriptInterfaces() {
addJavascriptInterface<FileInputInterface>()
addJavascriptInterface<FileOutputInterface>()
addJavascriptInterface<ApplicationInterface>()
addJavascriptInterface<FileInterface>()
addJavascriptInterface<ModuleInterface>()
addJavascriptInterface<UserManagerInterface>()
addJavascriptInterface<PackageManagerInterface>()
addJavascriptInterface<IntentInterface>()
...
}
ApplicationInterface exposes the bridge object name as webui:
class ApplicationInterface(
wxOptions: WXOptions,
) : WXInterface(wxOptions) {
override var name: String = "webui"
override var tag: String = "ApplicationInterface"
...
}
The JavaScript-callable exit() method directly calls into the Activity helper:
@JavascriptInterface
fun exit() {
withActivity {
exit(options)
}
}
withActivity { ... } directly invokes the block on the current thread:
fun <R> withActivity(block: Activity.() -> R): R? {
if (activity == null) {
console.trace("withActivity -> activity == null")
console.error("[$tag->withActivity] Activity not found")
return null
}
return block(activity)
}
The target exit(options) helper calls finish():
internal fun Activity.exit(options: WebUIOptions) {
if (!options.forceKillWebUIProcess) {
finish()
return
}
finish()
Process.killProcess(Process.myPid())
exitProcess(0)
}
The Android documentation for the legacy addJavascriptInterface(...) bridge says the system calls annotated methods on a background thread and requires careful synchronization on the Kotlin/Java side. Therefore, a JavaScript call to webui.exit() can reach Activity.finish() from the WebView bridge background thread.
Verified bug trace
WebUI page JavaScript
-> calls webui.exit()
-> WebView invokes ApplicationInterface.exit() on the JavaScript-bridge background thread
-> ApplicationInterface.exit()
-> withActivity { exit(options) }
-> WXActivity.Companion.exit(options)
-> Activity.finish()
-> Activity lifecycle operation is reached from the WebView bridge thread
-> possible wrong-thread lifecycle access / inconsistent close behavior
This looks like a source-verified candidate because the latest source still registers ApplicationInterface, exposes exit() via @JavascriptInterface, and reaches Activity.finish() without a WebView.post { ... }, Handler(Looper.getMainLooper()).post { ... }, or runOnUiThread { ... } hop.
I also searched the current GitHub issues and pull requests for this pattern using terms such as JavascriptInterface exit finish thread and ApplicationInterface exit, and did not find an existing matching discussion.
Why this matters
WebView JavaScript bridge methods are easy to treat like ordinary UI callbacks, but they are not delivered on the Android main thread. Closing or finishing an Activity is a lifecycle/UI operation and should be performed on the main thread.
This issue may be hard to reproduce consistently because finish() is often tolerant in simple cases, but the pattern is fragile. It can become more problematic when the Activity is already changing state, when WebView teardown is happening at the same time, or when device/WebView implementations are stricter about UI-thread access.
Possible fix
Marshal the Activity operation back to the main thread before calling exit(options).
A minimal local fix is to use the existing post { ... } helper:
@JavascriptInterface
fun exit() {
post {
withActivity {
exit(options)
}
}
}
Alternatively, withActivity could be split into two helpers: one for pure Activity access and another for Activity/UI operations that always dispatches to the main thread.
For example:
fun withActivityOnMain(block: Activity.() -> Unit) {
post {
withActivity {
block()
}
}
}
Then exit() can become:
@JavascriptInterface
fun exit() {
withActivityOnMain {
exit(options)
}
}
Reference
Relevant Android documentation wording:
The system calls methods on a background thread, requiring careful synchronization on the Kotlin or Java side.
Hi WebUI X Team,
I’m a PhD student researching Android thread-related issues. My research group recently ran a static analysis scan for thread-related bugs in real-world Android apps, and our prototype flagged a potential issue in Webui X.
Checked target
com.dergoogler.mmrl.webui.interfaces.ApplicationInterface.exit()@JavascriptInterfacecallback directly reachesActivity.finish()What I found
WXViewregistersApplicationInterfaceas one of the built-in JavaScript interfaces:ApplicationInterfaceexposes the bridge object name aswebui:The JavaScript-callable
exit()method directly calls into the Activity helper:withActivity { ... }directly invokes the block on the current thread:The target
exit(options)helper callsfinish():The Android documentation for the legacy
addJavascriptInterface(...)bridge says the system calls annotated methods on a background thread and requires careful synchronization on the Kotlin/Java side. Therefore, a JavaScript call towebui.exit()can reachActivity.finish()from the WebView bridge background thread.Verified bug trace
This looks like a source-verified candidate because the latest source still registers
ApplicationInterface, exposesexit()via@JavascriptInterface, and reachesActivity.finish()without aWebView.post { ... },Handler(Looper.getMainLooper()).post { ... }, orrunOnUiThread { ... }hop.I also searched the current GitHub issues and pull requests for this pattern using terms such as
JavascriptInterface exit finish threadandApplicationInterface exit, and did not find an existing matching discussion.Why this matters
WebView JavaScript bridge methods are easy to treat like ordinary UI callbacks, but they are not delivered on the Android main thread. Closing or finishing an Activity is a lifecycle/UI operation and should be performed on the main thread.
This issue may be hard to reproduce consistently because
finish()is often tolerant in simple cases, but the pattern is fragile. It can become more problematic when the Activity is already changing state, when WebView teardown is happening at the same time, or when device/WebView implementations are stricter about UI-thread access.Possible fix
Marshal the Activity operation back to the main thread before calling
exit(options).A minimal local fix is to use the existing
post { ... }helper:Alternatively,
withActivitycould be split into two helpers: one for pure Activity access and another for Activity/UI operations that always dispatches to the main thread.For example:
Then
exit()can become:Reference
Relevant Android documentation wording: