From a779ecb9a02463e489313a049a477ee314d5a348 Mon Sep 17 00:00:00 2001 From: Jani Hautakangas Date: Mon, 29 Jul 2024 16:13:17 +0300 Subject: [PATCH] [wpe] Add support for Alert and Confirm dialogs --- wpe/src/main/cpp/Browser/Page.cpp | 37 +++++++- wpe/src/main/java/com/wpe/wpe/Page.java | 85 ++++++++++++++++++ .../java/com/wpe/wpeview/WPEChromeClient.java | 86 +++++++++++++++++-- .../java/com/wpe/wpeview/WPEJsResult.java | 7 ++ .../main/java/com/wpe/wpeview/WPEView.java | 12 +++ 5 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 wpe/src/main/java/com/wpe/wpeview/WPEJsResult.java diff --git a/wpe/src/main/cpp/Browser/Page.cpp b/wpe/src/main/cpp/Browser/Page.cpp index add408db7..b469fb250 100644 --- a/wpe/src/main/cpp/Browser/Page.cpp +++ b/wpe/src/main/cpp/Browser/Page.cpp @@ -85,6 +85,18 @@ class JNIPageCache final : public JNI::TypedClass { static_cast(webkit_web_view_can_go_forward(webView))); } + static gboolean onScriptDialog(Page* page, WebKitScriptDialog* dialog, WebKitWebView* webView) + { + auto dialogPtr = static_cast(webkit_script_dialog_ref(dialog)); + auto jActiveURL = JNI::String(webkit_web_view_get_uri(webView)); + auto jMessage = JNI::String(webkit_script_dialog_get_message(dialog)); + callJavaMethod(getJNIPageCache().m_onScriptDialog, page->m_pageJavaInstance.get(), dialogPtr, + webkit_script_dialog_get_dialog_type(dialog), static_cast(jActiveURL), + static_cast(jMessage)); + + return TRUE; + } + static bool onFullscreenRequest(Page* page, bool fullscreen) noexcept { if (page->m_viewBackend != nullptr) { @@ -154,6 +166,7 @@ class JNIPageCache final : public JNI::TypedClass { const JNI::Method m_onLoadProgress; const JNI::Method m_onUriChanged; const JNI::Method m_onTitleChanged; + const JNI::Method m_onScriptDialog; const JNI::Method m_onInputMethodContextIn; const JNI::Method m_onInputMethodContextOut; const JNI::Method m_onEnterFullscreenMode; @@ -183,6 +196,8 @@ class JNIPageCache final : public JNI::TypedClass { static void nativeRequestExitFullscreenMode(JNIEnv* env, jobject obj, jlong pagePtr) noexcept; static void nativeEvaluateJavascript( JNIEnv* env, jobject obj, jlong pagePtr, jstring script, JNIWKCallback callback) noexcept; + static void nativeScriptDialogClose(JNIEnv* env, jobject obj, jlong dialogPtr) noexcept; + static void nativeScriptDialogConfirm(JNIEnv* env, jobject obj, jlong dialogPtr, jboolean confirm) noexcept; }; const JNIPageCache& getJNIPageCache() @@ -198,6 +213,7 @@ JNIPageCache::JNIPageCache() , m_onLoadProgress(getMethod("onLoadProgress")) , m_onUriChanged(getMethod("onUriChanged")) , m_onTitleChanged(getMethod("onTitleChanged")) + , m_onScriptDialog(getMethod("onScriptDialog")) , m_onInputMethodContextIn(getMethod("onInputMethodContextIn")) , m_onInputMethodContextOut(getMethod("onInputMethodContextOut")) , m_onEnterFullscreenMode(getMethod("onEnterFullscreenMode")) @@ -226,7 +242,9 @@ JNIPageCache::JNIPageCache() JNI::NativeMethod( "nativeRequestExitFullscreenMode", JNIPageCache::nativeRequestExitFullscreenMode), JNI::NativeMethod( - "nativeEvaluateJavascript", JNIPageCache::nativeEvaluateJavascript)); + "nativeEvaluateJavascript", JNIPageCache::nativeEvaluateJavascript), + JNI::NativeMethod("nativeScriptDialogClose", JNIPageCache::nativeScriptDialogClose), + JNI::NativeMethod("nativeScriptDialogConfirm", JNIPageCache::nativeScriptDialogConfirm)); } jlong JNIPageCache::nativeInit( @@ -438,6 +456,21 @@ void JNIPageCache::nativeEvaluateJavascript( } } +void JNIPageCache::nativeScriptDialogClose(JNIEnv* /*env*/, jobject /*obj*/, jlong dialogPtr) noexcept +{ + Logging::logDebug("Page::nativeScriptDialogClose() [tid %d]", gettid()); + auto* dialog = reinterpret_cast(dialogPtr); // NOLINT(performance-no-int-to-ptr) + webkit_script_dialog_close(dialog); +} + +void JNIPageCache::nativeScriptDialogConfirm( + JNIEnv* /*env*/, jobject /*obj*/, jlong dialogPtr, jboolean confirm) noexcept +{ + Logging::logDebug("Page::nativeScriptDialogConfirm() [tid %d]", gettid()); + auto* dialog = reinterpret_cast(dialogPtr); // NOLINT(performance-no-int-to-ptr) + webkit_script_dialog_confirm_set_confirmed(dialog, static_cast(confirm)); +} + /*********************************************************************************************************************** * Native Page class implementation **********************************************************************************************************************/ @@ -478,6 +511,8 @@ Page::Page( g_signal_connect_swapped(m_webView, "notify::uri", G_CALLBACK(JNIPageCache::onUriChanged), this)); m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "notify::title", G_CALLBACK(JNIPageCache::onTitleChanged), this)); + m_signalHandlers.push_back( + g_signal_connect_swapped(m_webView, "script-dialog", G_CALLBACK(JNIPageCache::onScriptDialog), this)); wpe_view_backend_set_fullscreen_handler( wpeBackend, reinterpret_cast(JNIPageCache::onFullscreenRequest), this); diff --git a/wpe/src/main/java/com/wpe/wpe/Page.java b/wpe/src/main/java/com/wpe/wpe/Page.java index 8683a526f..e45e9608b 100644 --- a/wpe/src/main/java/com/wpe/wpe/Page.java +++ b/wpe/src/main/java/com/wpe/wpe/Page.java @@ -24,7 +24,9 @@ package com.wpe.wpe; import android.annotation.SuppressLint; +import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.os.Handler; import android.os.Looper; import android.util.DisplayMetrics; @@ -41,9 +43,12 @@ import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import com.wpe.wpeview.WPEJsResult; import com.wpe.wpeview.WPEView; import java.lang.ref.WeakReference; +import java.net.MalformedURLException; +import java.net.URL; /** * A Page roughly corresponds with a tab in a regular browser UI. @@ -61,6 +66,11 @@ public final class Page { public static final int LOAD_COMMITTED = 2; public static final int LOAD_FINISHED = 3; + public static final int WEBKIT_SCRIPT_DIALOG_ALERT = 0; + public static final int WEBKIT_SCRIPT_DIALOG_CONFIRM = 1; + public static final int WEBKIT_SCRIPT_DIALOG_PROMPT = 2; + public static final int WEBKIT_SCRIPT_DIALOG_BEFORE_UNLOAD_CONFIRM = 3; + protected long nativePtr = 0; public long getNativePtr() { return nativePtr; } @@ -83,6 +93,8 @@ public final class Page { private native void nativeDeleteInputMethodContent(long nativePtr, int offset); private native void nativeRequestExitFullscreenMode(long nativePtr); private native void nativeEvaluateJavascript(long nativePtr, String script, WKCallback callback); + private native void nativeScriptDialogClose(long nativeDialogPtr); + private native void nativeScriptDialogConfirm(long nativeDialogPtr, boolean confirm); private final WPEView wpeView; private final PageSurfaceView surfaceView; @@ -199,6 +211,79 @@ public void onTitleChanged(@NonNull String title, boolean canGoBack, boolean can wpeView.onTitleChanged(title); } + private class ScriptDialogResult implements WPEJsResult { + + private final long nativeScriptDialogPtr; + + public ScriptDialogResult(long nativeScriptDialogPtr) { this.nativeScriptDialogPtr = nativeScriptDialogPtr; } + @Override + @SuppressWarnings("SyntheticAccessor") + public void cancel() { + nativeScriptDialogConfirm(nativeScriptDialogPtr, false); + nativeScriptDialogClose(nativeScriptDialogPtr); + } + @Override + @SuppressWarnings("SyntheticAccessor") + public void confirm() { + nativeScriptDialogConfirm(nativeScriptDialogPtr, true); + nativeScriptDialogClose(nativeScriptDialogPtr); + } + } + + private static class ScriptDialogCancelListener + implements DialogInterface.OnCancelListener, DialogInterface.OnClickListener { + private final WPEJsResult result; + + public ScriptDialogCancelListener(@NonNull WPEJsResult result) { this.result = result; } + @Override + public void onCancel(DialogInterface dialogInterface) { + result.cancel(); + } + @Override + public void onClick(DialogInterface dialogInterface, int which) { + result.cancel(); + } + } + + private static class ScriptDialogPositiveListener implements DialogInterface.OnClickListener { + private final WPEJsResult result; + + public ScriptDialogPositiveListener(@NonNull WPEJsResult result) { this.result = result; } + + @Override + public void onClick(DialogInterface dialogInterface, int which) { + result.confirm(); + } + } + + @Keep + public boolean onScriptDialog(long nativeDialogPtr, int dialogType, @NonNull String url, @NonNull String message) { + ScriptDialogResult result = new ScriptDialogResult(nativeDialogPtr); + if (!wpeView.onDialogScript(dialogType, url, message, result)) { + if (dialogType == Page.WEBKIT_SCRIPT_DIALOG_ALERT || dialogType == Page.WEBKIT_SCRIPT_DIALOG_CONFIRM) { + final AlertDialog.Builder builder = new AlertDialog.Builder(wpeView.getContext()); + String title = url; + try { + URL alertUrl = new URL(url); + title = "The page at " + alertUrl.getProtocol() + "://" + alertUrl.getHost() + " says"; + } catch (MalformedURLException ex) { + // NOOP + } + builder.setTitle(title); + builder.setMessage(message); + builder.setOnCancelListener(new ScriptDialogCancelListener(result)); + builder.setPositiveButton("Yes", new ScriptDialogPositiveListener(result)); + if (dialogType != Page.WEBKIT_SCRIPT_DIALOG_ALERT) { + builder.setNegativeButton("No", new ScriptDialogCancelListener(result)); + } + builder.show(); + return true; + } + } + + return false; + } + @Keep public void onInputMethodContextIn() { WeakReference weakRefecence = new WeakReference<>(wpeView); diff --git a/wpe/src/main/java/com/wpe/wpeview/WPEChromeClient.java b/wpe/src/main/java/com/wpe/wpeview/WPEChromeClient.java index 7ef316883..0147f88e9 100644 --- a/wpe/src/main/java/com/wpe/wpeview/WPEChromeClient.java +++ b/wpe/src/main/java/com/wpe/wpeview/WPEChromeClient.java @@ -53,6 +53,18 @@ default void onProgressChanged(@NonNull WPEView view, int progress) {} */ default void onReceivedTitle(@NonNull WPEView view, @NonNull String title) {} + /** + * A callback interface used by the host application to notify + * the current page that its custom view has been dismissed. + */ + interface CustomViewCallback { + /** + * Invoked when the host application dismisses the + * custom view. + */ + void onCustomViewHidden(); + } + /** * Notify the host application that the current page has entered full screen mode. * @param view is the View object to be shown. @@ -67,14 +79,72 @@ default void onShowCustomView(@NonNull View view, @NonNull WPEChromeClient.Custo default void onHideCustomView() {} /** - * A callback interface used by the host application to notify - * the current page that its custom view has been dismissed. + * Notify the host application that the web page wants to display a + * JavaScript {@code alert()} dialog. + *

The default behavior if this method returns {@code false} or is not + * overridden is to show a dialog containing the alert message and suspend + * JavaScript execution until the dialog is dismissed. + *

To show a custom dialog, the app should return {@code true} from this + * method, in which case the default dialog will not be shown and JavaScript + * execution will be suspended. The app should call + * {@code WPEJsResult.confirm()} when the custom dialog is dismissed such that + * JavaScript execution can be resumed. + *

To suppress the dialog and allow JavaScript execution to + * continue, call {@code WPEJsResult.confirm()} immediately and then return + * {@code true}. + *

Note that if the {@link WPEChromeClient} is set to be {@code null}, + * or if {@link WPEChromeClient} is not set at all, the default dialog will + * be suppressed and Javascript execution will continue immediately. + *

Note that the default dialog does not inherit the {@link + * android.view.Display#FLAG_SECURE} flag from the parent window. + * + * @param view The WPEView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param result A WPEJsResult to confirm that the user closed the window. + * @return boolean {@code true} if the request is handled or ignored. + * {@code false} if WPEView needs to show the default dialog. */ - interface CustomViewCallback { - /** - * Invoked when the host application dismisses the - * custom view. - */ - void onCustomViewHidden(); + default boolean onJsAlert(@NonNull WPEView view, @NonNull String url, @NonNull String message, + @NonNull WPEJsResult result) { + return false; + } + + /** + * Notify the host application that the web page wants to display a + * JavaScript {@code confirm()} dialog. + *

The default behavior if this method returns {@code false} or is not + * overridden is to show a dialog containing the message and suspend + * JavaScript execution until the dialog is dismissed. The default dialog + * will return {@code true} to the JavaScript {@code confirm()} code when + * the user presses the 'confirm' button, and will return {@code false} to + * the JavaScript code when the user presses the 'cancel' button or + * dismisses the dialog. + *

To show a custom dialog, the app should return {@code true} from this + * method, in which case the default dialog will not be shown and JavaScript + * execution will be suspended. The app should call + * {@code WPEJsResult.confirm()} or {@code WPEJsResult.cancel()} when the custom + * dialog is dismissed. + *

To suppress the dialog and allow JavaScript execution to continue, + * call {@code WPEJsResult.confirm()} or {@code WPEJsResult.cancel()} immediately + * and then return {@code true}. + *

Note that if the {@link WPEChromeClient} is set to be {@code null}, + * or if {@link WPEChromeClient} is not set at all, the default dialog will + * be suppressed and the default value of {@code false} will be returned to + * the JavaScript code immediately. + *

Note that the default dialog does not inherit the {@link + * android.view.Display#FLAG_SECURE} flag from the parent window. + * + * @param view The WPEView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param result A WPEJsResult used to send the user's response to + * javascript. + * @return boolean {@code true} if the request is handled or ignored. + * {@code false} if WPEView needs to show the default dialog. + */ + default boolean onJsConfirm(@NonNull WPEView view, @NonNull String url, @NonNull String message, + @NonNull WPEJsResult result) { + return false; } } diff --git a/wpe/src/main/java/com/wpe/wpeview/WPEJsResult.java b/wpe/src/main/java/com/wpe/wpeview/WPEJsResult.java new file mode 100644 index 000000000..83f80d2b9 --- /dev/null +++ b/wpe/src/main/java/com/wpe/wpeview/WPEJsResult.java @@ -0,0 +1,7 @@ +package com.wpe.wpeview; + +public interface WPEJsResult { + void cancel(); + + void confirm(); +} diff --git a/wpe/src/main/java/com/wpe/wpeview/WPEView.java b/wpe/src/main/java/com/wpe/wpeview/WPEView.java index 4955414e5..01829e269 100644 --- a/wpe/src/main/java/com/wpe/wpeview/WPEView.java +++ b/wpe/src/main/java/com/wpe/wpeview/WPEView.java @@ -164,6 +164,18 @@ public void onTitleChanged(@NonNull String title) { wpeChromeClient.onReceivedTitle(this, title); } + public boolean onDialogScript(int dialogType, @NonNull String url, @NonNull String message, + @NonNull WPEJsResult result) { + if (wpeChromeClient != null) { + if (dialogType == Page.WEBKIT_SCRIPT_DIALOG_ALERT) { + return wpeChromeClient.onJsAlert(this, url, message, result); + } else if (dialogType == Page.WEBKIT_SCRIPT_DIALOG_CONFIRM) { + return wpeChromeClient.onJsConfirm(this, url, message, result); + } + } + return false; + } + public void onEnterFullscreenMode() { if ((surfaceView != null) && (wpeChromeClient != null)) { removeView(surfaceView);