From 1335f48f5fed91ccdcd0bfca3ce2d056b1e92206 Mon Sep 17 00:00:00 2001 From: Andrey Bocharnikov Date: Mon, 18 May 2026 14:08:45 +0400 Subject: [PATCH] fix: deadlock * qt create webview * android ui thread focuses URL field fixes status-im/status-app#20886 --- .github/workflows/ci.yml | 3 + .../src/org/mobilewebview/MobileWebView.java | 82 ++++++++++--------- .../org/mobilewebview/PendingActionQueue.java | 35 ++++++++ .../MobileWebViewPendingActionsTest.java | 75 +++++++++++++++++ .../android/mobilewebviewbackend_android.cpp | 11 --- 5 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 mobilewebview/android/src/org/mobilewebview/PendingActionQueue.java create mode 100644 mobilewebview/android/tests/org/mobilewebview/MobileWebViewPendingActionsTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4aacf4f..88086e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,12 +27,15 @@ jobs: javac -d build/java-tests \ mobilewebview/android/src/org/mobilewebview/OriginUtils.java \ mobilewebview/android/src/org/mobilewebview/BridgeScriptBuilder.java \ + mobilewebview/android/src/org/mobilewebview/PendingActionQueue.java \ mobilewebview/android/src/org/mobilewebview/WebViewUrlPolicy.java \ mobilewebview/android/tests/org/mobilewebview/OriginUtilsTest.java \ mobilewebview/android/tests/org/mobilewebview/BridgeScriptBuilderTest.java \ + mobilewebview/android/tests/org/mobilewebview/MobileWebViewPendingActionsTest.java \ mobilewebview/android/tests/org/mobilewebview/WebViewUrlPolicyTest.java java -cp build/java-tests org.mobilewebview.OriginUtilsTest java -cp build/java-tests org.mobilewebview.BridgeScriptBuilderTest + java -cp build/java-tests org.mobilewebview.MobileWebViewPendingActionsTest java -cp build/java-tests org.mobilewebview.WebViewUrlPolicyTest - name: Install build dependencies diff --git a/mobilewebview/android/src/org/mobilewebview/MobileWebView.java b/mobilewebview/android/src/org/mobilewebview/MobileWebView.java index ea0e27f..a6aa4e9 100644 --- a/mobilewebview/android/src/org/mobilewebview/MobileWebView.java +++ b/mobilewebview/android/src/org/mobilewebview/MobileWebView.java @@ -40,8 +40,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.CountDownLatch; /** * MobileWebView - Native Android WebView wrapper for Qt integration @@ -71,6 +69,8 @@ public class MobileWebView { // Navigation state private boolean mBridgeInstalled = false; private String mPendingUrl = null; + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + private final PendingActionQueue mPendingActionQueue = new PendingActionQueue(); @FunctionalInterface private interface NativeCallback { @@ -89,35 +89,22 @@ public MobileWebView(Context context, long nativePtr, View rootView) { // WebView must be created on Android main/UI thread. if (Looper.myLooper() == Looper.getMainLooper()) { - mWebView = new WebView(context); - setupWebView(); + initializeWebViewOnMainThread(context); return; } - CountDownLatch latch = new CountDownLatch(1); - final AtomicReference creationError = new AtomicReference<>(); - Handler mainHandler = new Handler(Looper.getMainLooper()); - mainHandler.post(() -> { - try { - mWebView = new WebView(context); - setupWebView(); - } catch (RuntimeException e) { - creationError.set(e); - } finally { - latch.countDown(); - } - }); + mMainHandler.post(() -> initializeWebViewOnMainThread(context)); + } + private void initializeWebViewOnMainThread(Context context) { try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted while creating WebView on main thread", e); - } - - RuntimeException error = creationError.get(); - if (error != null) { - throw error; + mWebView = new WebView(context); + setupWebView(); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to initialize WebView on main thread", e); + mWebView = null; + } finally { + mPendingActionQueue.markReady(); } } @@ -138,6 +125,10 @@ private ViewGroup resolveRootView(View rootView) { } private void runOnMainThread(Runnable action) { + if (mPendingActionQueue.enqueueIfNotReady(action)) { + return; + } + if (Looper.myLooper() == Looper.getMainLooper()) { action.run(); return; @@ -157,7 +148,7 @@ private void runOnMainThread(Runnable action) { } } - new Handler(Looper.getMainLooper()).post(action); + mMainHandler.post(action); } /** @@ -253,7 +244,10 @@ public void loadUrl(String url) { } } - runOnMainThread(() -> mWebView.loadUrl(url)); + runOnMainThread(() -> { + if (mWebView == null) return; + mWebView.loadUrl(url); + }); } /** @@ -269,7 +263,10 @@ public void loadHtml(String html, String baseUrl) { } } - runOnMainThread(() -> mWebView.loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null)); + runOnMainThread(() -> { + if (mWebView == null) return; + mWebView.loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null); + }); } public void goBack() { @@ -380,11 +377,12 @@ public void stopFind() { * Evaluate JavaScript and notify result via callback */ public void evaluateJavaScript(String script) { - runOnMainThread(() -> + runOnMainThread(() -> { + if (mWebView == null) return; mWebView.evaluateJavascript(script, result -> withNativePtr(ptr -> nativeOnJavaScriptResult(ptr, result != null ? result : "", "")) - ) - ); + ); + }); } /** @@ -397,11 +395,12 @@ public void postMessageToJavaScript(String json) { } String deliverScript = BridgeScriptBuilder.buildDeliverScript(namespace, json); - runOnMainThread(() -> + runOnMainThread(() -> { + if (mWebView == null) return; mWebView.evaluateJavascript(deliverScript, value -> Log.d(TAG, "postMessageToJavaScript result: " + value) - ) - ); + ); + }); } /** @@ -409,6 +408,7 @@ public void postMessageToJavaScript(String json) { */ public void setGeometry(int x, int y, int width, int height) { runOnMainThread(() -> { + if (mWebView == null) return; ViewGroup.LayoutParams params = mWebView.getLayoutParams(); if (params == null) { params = new ViewGroup.LayoutParams(width, height); @@ -426,7 +426,10 @@ public void setGeometry(int x, int y, int width, int height) { * Set WebView visibility */ public void setVisible(boolean visible) { - runOnMainThread(() -> mWebView.setVisibility(visible ? View.VISIBLE : View.GONE)); + runOnMainThread(() -> { + if (mWebView == null) return; + mWebView.setVisibility(visible ? View.VISIBLE : View.GONE); + }); } /** @@ -469,10 +472,9 @@ public void captureSnapshotForFreeze(long requestId) { public void setInteractionEnabled(boolean enabled) { runOnMainThread(() -> { - if (mWebView != null) { - mWebView.setFocusable(enabled); - mWebView.setFocusableInTouchMode(enabled); - } + if (mWebView == null) return; + mWebView.setFocusable(enabled); + mWebView.setFocusableInTouchMode(enabled); }); } diff --git a/mobilewebview/android/src/org/mobilewebview/PendingActionQueue.java b/mobilewebview/android/src/org/mobilewebview/PendingActionQueue.java new file mode 100644 index 0000000..87e4354 --- /dev/null +++ b/mobilewebview/android/src/org/mobilewebview/PendingActionQueue.java @@ -0,0 +1,35 @@ +package org.mobilewebview; + +import java.util.ArrayDeque; + +final class PendingActionQueue { + private final Object mLock = new Object(); + private final ArrayDeque mPendingActions = new ArrayDeque<>(); + private boolean mReady = false; + + boolean enqueueIfNotReady(Runnable action) { + synchronized (mLock) { + if (mReady) { + return false; + } + mPendingActions.addLast(action); + return true; + } + } + + void markReady() { + ArrayDeque actionsToRun; + synchronized (mLock) { + if (mReady) { + return; + } + mReady = true; + actionsToRun = new ArrayDeque<>(mPendingActions); + mPendingActions.clear(); + } + + while (!actionsToRun.isEmpty()) { + actionsToRun.removeFirst().run(); + } + } +} diff --git a/mobilewebview/android/tests/org/mobilewebview/MobileWebViewPendingActionsTest.java b/mobilewebview/android/tests/org/mobilewebview/MobileWebViewPendingActionsTest.java new file mode 100644 index 0000000..ddfed03 --- /dev/null +++ b/mobilewebview/android/tests/org/mobilewebview/MobileWebViewPendingActionsTest.java @@ -0,0 +1,75 @@ +package org.mobilewebview; + +import java.util.ArrayList; +import java.util.List; + +public final class MobileWebViewPendingActionsTest { + public static void main(String[] args) { + shouldReplayPendingActionsInOrderWhenQueueBecomesReady(); + shouldRunActionImmediatelyAfterReady(); + shouldIgnoreSecondMarkReadyCall(); + System.out.println("MobileWebViewPendingActionsTest passed"); + } + + private static void shouldReplayPendingActionsInOrderWhenQueueBecomesReady() { + PendingActionQueue queue = new PendingActionQueue(); + List events = new ArrayList<>(); + + assertTrue(queue.enqueueIfNotReady(() -> events.add("first"))); + assertTrue(queue.enqueueIfNotReady(() -> events.add("second"))); + + queue.markReady(); + + assertEquals(2, events.size()); + assertEquals("first", events.get(0)); + assertEquals("second", events.get(1)); + } + + private static void shouldRunActionImmediatelyAfterReady() { + PendingActionQueue queue = new PendingActionQueue(); + List events = new ArrayList<>(); + + queue.markReady(); + assertFalse(queue.enqueueIfNotReady(() -> events.add("queued"))); + + events.add("immediate"); + assertEquals(1, events.size()); + assertEquals("immediate", events.get(0)); + } + + private static void shouldIgnoreSecondMarkReadyCall() { + PendingActionQueue queue = new PendingActionQueue(); + List events = new ArrayList<>(); + + assertTrue(queue.enqueueIfNotReady(() -> events.add("once"))); + queue.markReady(); + queue.markReady(); + + assertEquals(1, events.size()); + assertEquals("once", events.get(0)); + } + + private static void assertTrue(boolean condition) { + if (!condition) { + throw new AssertionError("Expected true"); + } + } + + private static void assertFalse(boolean condition) { + if (condition) { + throw new AssertionError("Expected false"); + } + } + + private static void assertEquals(int expected, int actual) { + if (expected != actual) { + throw new AssertionError("Expected [" + expected + "], got [" + actual + "]"); + } + } + + private static void assertEquals(String expected, String actual) { + if (!expected.equals(actual)) { + throw new AssertionError("Expected [" + expected + "], got [" + actual + "]"); + } + } +} diff --git a/mobilewebview/src/android/mobilewebviewbackend_android.cpp b/mobilewebview/src/android/mobilewebviewbackend_android.cpp index deb037b..32e8f7a 100644 --- a/mobilewebview/src/android/mobilewebviewbackend_android.cpp +++ b/mobilewebview/src/android/mobilewebviewbackend_android.cpp @@ -245,17 +245,6 @@ jobject AndroidWebViewPrivate::createWebView() jobject globalObj = env->NewGlobalRef(localObj); env->DeleteLocalRef(localObj); - // Get WebView once to verify Java object setup. - // View attachment is performed in Java on Android UI thread. - jmethodID getWebViewMethod = env->GetMethodID(m_webViewClass, "getWebView", - "()Landroid/webkit/WebView;"); - if (getWebViewMethod) { - jobject webView = env->CallObjectMethod(globalObj, getWebViewMethod); - if (webView) { - env->DeleteLocalRef(webView); - } - } - if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear();