Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 42 additions & 40 deletions mobilewebview/android/src/org/mobilewebview/MobileWebView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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<RuntimeException> 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();
}
}

Expand All @@ -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;
Expand All @@ -157,7 +148,7 @@ private void runOnMainThread(Runnable action) {
}
}

new Handler(Looper.getMainLooper()).post(action);
mMainHandler.post(action);
}

/**
Expand Down Expand Up @@ -253,7 +244,10 @@ public void loadUrl(String url) {
}
}

runOnMainThread(() -> mWebView.loadUrl(url));
runOnMainThread(() -> {
if (mWebView == null) return;
mWebView.loadUrl(url);
});
}

/**
Expand All @@ -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() {
Expand Down Expand Up @@ -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 : "", ""))
)
);
);
});
Comment on lines 379 to +385
}

/**
Expand All @@ -397,18 +395,20 @@ 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)
)
);
);
});
}

/**
* Set WebView geometry (x, y, width, height)
*/
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);
Expand All @@ -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);
});
}

/**
Expand Down Expand Up @@ -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);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.mobilewebview;

import java.util.ArrayDeque;

final class PendingActionQueue {
private final Object mLock = new Object();
private final ArrayDeque<Runnable> 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<Runnable> actionsToRun;
synchronized (mLock) {
if (mReady) {
return;
}
mReady = true;
actionsToRun = new ArrayDeque<>(mPendingActions);
mPendingActions.clear();
}

while (!actionsToRun.isEmpty()) {
actionsToRun.removeFirst().run();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> 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 + "]");
}
}
}
11 changes: 0 additions & 11 deletions mobilewebview/src/android/mobilewebviewbackend_android.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading