From d311e374d115a1e4e09b0b3455b56d3862006d2f Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Fri, 2 Jan 2026 21:53:52 +0800 Subject: [PATCH 1/7] Update build for WPE development In clang-tidy rules, change WarningsAsErrors to warnings only. Preserve Cerbero build folder during gradle clean. --- .clang-tidy | 2 +- build.gradle | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ec0d03ece..c4d1df720 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -44,7 +44,7 @@ Checks: -hicpp-move-const-arg, llvm-*, -llvm-header-guard -WarningsAsErrors: '*' +WarningsAsErrors: '' HeaderFilterRegex: '(Browser|Common|JNI)/[^/]+\.h$' AnalyzeTemporaryDtors: false FormatStyle: file diff --git a/build.gradle b/build.gradle index 9da4e79bb..0ed50bb5c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,14 @@ plugins { } tasks.register("clean", Delete) { - delete rootProject.layout.buildDirectory + def buildDir = rootProject.layout.buildDirectory.get().asFile + if (buildDir.exists()) { + buildDir.eachFile { file -> + if (file.name != "cerbero" && file.name != "sysroot") { + delete file + } + } + } delete file(".git/hooks/pre-commit") } From bec282a5e967711a98f27893f045dce96402e510 Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Fri, 2 Jan 2026 22:10:49 +0800 Subject: [PATCH 2/7] Enable WPE Platform API for Android Replace the legacy wpe-android view-backend with the new WPE Platform API, providing a cleaner integration with WebKit's rendering pipeline. New implementations: - WPEDisplayAndroid: Android-specific WPEDisplay - WPEToplevelAndroid: Window/toplevel management - WPEViewAndroid: View with buffer handling via AHardwareBuffer - WPEInputMethodContextAndroid: Input method context Key changes: - RendererSurfaceControl uses WPEBuffer directly instead of WPEAndroidBuffer - Connect fundamental WebKit signals for navigation, loading, and UI --- tools/scripts/bootstrap.py | 1 + wpeview/src/main/cpp/CMakeLists.txt | 20 +- .../src/main/cpp/Common/WPEDisplayAndroid.cpp | 183 +++++++ .../src/main/cpp/Common/WPEDisplayAndroid.h | 34 ++ .../Common/WPEInputMethodContextAndroid.cpp | 203 ++++++++ .../cpp/Common/WPEInputMethodContextAndroid.h | 55 ++ .../main/cpp/Common/WPEToplevelAndroid.cpp | 167 ++++++ .../src/main/cpp/Common/WPEToplevelAndroid.h | 31 ++ .../main/cpp/Runtime/InputMethodContext.cpp | 135 ----- .../src/main/cpp/Runtime/InputMethodContext.h | 54 -- wpeview/src/main/cpp/Runtime/Renderer.h | 9 +- .../cpp/Runtime/RendererSurfaceControl.cpp | 323 +++++++----- .../main/cpp/Runtime/RendererSurfaceControl.h | 51 +- .../main/cpp/Runtime/ScopedWPEAndroidBuffer.h | 71 --- .../src/main/cpp/Runtime/SurfaceControl.cpp | 1 - wpeview/src/main/cpp/Runtime/SurfaceControl.h | 2 - wpeview/src/main/cpp/Runtime/WKRuntime.cpp | 1 + wpeview/src/main/cpp/Runtime/WKSettings.cpp | 2 + wpeview/src/main/cpp/Runtime/WKWebView.cpp | 478 +++++++++++++----- wpeview/src/main/cpp/Runtime/WKWebView.h | 24 +- .../src/main/cpp/Runtime/WPEViewAndroid.cpp | 217 ++++++++ wpeview/src/main/cpp/Runtime/WPEViewAndroid.h | 45 ++ wpeview/src/main/cpp/Service/EntryPoint.cpp | 24 + .../java/org/wpewebkit/wpe/WKWebView.java | 115 ++++- .../wpewebkit/wpeview/WPEChromeClient.java | 78 +++ .../org/wpewebkit/wpeview/WPEContext.java | 2 +- .../java/org/wpewebkit/wpeview/WPEView.java | 24 +- .../org/wpewebkit/wpeview/WPEViewClient.java | 65 +++ 28 files changed, 1843 insertions(+), 572 deletions(-) create mode 100644 wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp create mode 100644 wpeview/src/main/cpp/Common/WPEDisplayAndroid.h create mode 100644 wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp create mode 100644 wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h create mode 100644 wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp create mode 100644 wpeview/src/main/cpp/Common/WPEToplevelAndroid.h delete mode 100644 wpeview/src/main/cpp/Runtime/InputMethodContext.cpp delete mode 100644 wpeview/src/main/cpp/Runtime/InputMethodContext.h delete mode 100644 wpeview/src/main/cpp/Runtime/ScopedWPEAndroidBuffer.h create mode 100644 wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp create mode 100644 wpeview/src/main/cpp/Runtime/WPEViewAndroid.h diff --git a/tools/scripts/bootstrap.py b/tools/scripts/bootstrap.py index abb4da1e5..f0997b4a5 100755 --- a/tools/scripts/bootstrap.py +++ b/tools/scripts/bootstrap.py @@ -109,6 +109,7 @@ class Bootstrap: ("wpe-1.0", "wpe"), ("wpe-android", "wpe-android"), ("wpe-webkit-2.0", "wpe-webkit"), + ("wpe-webkit-2.0/wpe-platform", "wpe-platform"), ("xkbcommon", "xkbcommon") ] _soname_replacements = [ diff --git a/wpeview/src/main/cpp/CMakeLists.txt b/wpeview/src/main/cpp/CMakeLists.txt index 4c129a546..0449ad204 100644 --- a/wpeview/src/main/cpp/CMakeLists.txt +++ b/wpeview/src/main/cpp/CMakeLists.txt @@ -77,9 +77,11 @@ set_target_properties(soup-3.0 PROPERTIES IMPORTED_LOCATION add_library(WPEWebKit-2.0 SHARED IMPORTED) set_target_properties( WPEWebKit-2.0 - PROPERTIES IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/imported/lib/${ANDROID_ABI}/libWPEWebKit-2.0.so" - INTERFACE_INCLUDE_DIRECTORIES - "${CMAKE_SOURCE_DIR}/imported/include/wpe-webkit;${CMAKE_SOURCE_DIR}/imported/include/libsoup-3.0") + PROPERTIES + IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/imported/lib/${ANDROID_ABI}/libWPEWebKit-2.0.so" + INTERFACE_INCLUDE_DIRECTORIES + "${CMAKE_SOURCE_DIR}/imported/include/wpe-webkit;${CMAKE_SOURCE_DIR}/imported/include/wpe-platform;${CMAKE_SOURCE_DIR}/imported/include;${CMAKE_SOURCE_DIR}/imported/include/libsoup-3.0" +) add_library(libwpe-1.0 SHARED IMPORTED) set_target_properties( @@ -109,6 +111,7 @@ target_link_libraries( ${android-lib} ${log-lib} ${sync-lib} + gio-2.0 glib-2.0 gobject-2.0 gstreamer-1.0 @@ -120,7 +123,6 @@ add_library( WPEAndroidRuntime SHARED Runtime/EntryPoint.cpp Runtime/Fence.cpp - Runtime/InputMethodContext.cpp Runtime/LooperThread.cpp Runtime/MessagePump.cpp Runtime/RendererSurfaceControl.cpp @@ -132,9 +134,14 @@ add_library( Runtime/WKWebContext.cpp Runtime/WKSettings.cpp Runtime/WKWebsiteDataManager.cpp - Runtime/WKWebView.cpp) + Runtime/WKWebView.cpp + Runtime/WPEViewAndroid.cpp + Common/WPEDisplayAndroid.cpp + Common/WPEInputMethodContextAndroid.cpp + Common/WPEToplevelAndroid.cpp) target_configure_quality(WPEAndroidRuntime) target_compile_definitions(WPEAndroidRuntime PRIVATE WPE_ENABLE_PROCESS) +target_include_directories(WPEAndroidRuntime PRIVATE Runtime Common) target_link_libraries( WPEAndroidRuntime EGL @@ -144,7 +151,6 @@ target_link_libraries( gobject-2.0 libwpe-1.0 soup-3.0 - WPEBackend WPEWebKit-2.0 WPEAndroidCommon) @@ -156,7 +162,7 @@ target_link_libraries( gio-2.0 glib-2.0 gmodule-2.0 - WPEBackend WPEWebKit-2.0 + WPEAndroidRuntime WPEAndroidCommon WPEWebDriver) diff --git a/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp new file mode 100644 index 000000000..4ba77d972 --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp @@ -0,0 +1,183 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WPEDisplayAndroid.h" + +#include "Logging.h" +#include "WPEInputMethodContextAndroid.h" +#include "WPEToplevelAndroid.h" +#include "WPEViewAndroid.h" + +#include +#include + +struct _WPEDisplayAndroid { + WPEDisplay parent; +}; + +typedef struct { + gpointer eglDisplay; + WPEToplevel* toplevel; +} WPEDisplayAndroidPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE(WPEDisplayAndroid, wpe_display_android, WPE_TYPE_DISPLAY) + +static void wpeDisplayAndroidDispose(GObject* object) +{ + Logging::logDebug("WPEDisplayAndroid::dispose(%p)", object); + + auto* priv + = static_cast(wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(object))); + + // Clean up toplevel + g_clear_object(&priv->toplevel); + + // Clean up EGL display if initialized + if (priv->eglDisplay != nullptr) { + eglTerminate(static_cast(priv->eglDisplay)); + priv->eglDisplay = nullptr; + } + + G_OBJECT_CLASS(wpe_display_android_parent_class)->dispose(object); +} + +static gboolean wpeDisplayAndroidConnect(WPEDisplay* display, GError** /*error*/) +{ + Logging::logDebug("WPEDisplayAndroid::connect(%p)", display); + return TRUE; +} + +static WPEView* wpeDisplayAndroidCreateView(WPEDisplay* display) +{ + Logging::logDebug("WPEDisplayAndroid::create_view(%p)", display); + + auto* view = wpe_view_android_new(display); + auto* priv = static_cast( + wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + if (priv->toplevel) { + wpe_view_set_toplevel(view, priv->toplevel); + } + + return view; +} + +static gpointer wpeDisplayAndroidGetEGLDisplay(WPEDisplay* display, GError** error) +{ + Logging::logDebug("WPEDisplayAndroid::get_egl_display(%p)", display); + + auto* priv = static_cast( + wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + + if (priv->eglDisplay != nullptr) { + return priv->eglDisplay; + } + + EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL_NO_DISPLAY) { + EGLint eglError = eglGetError(); + Logging::logError("WPEDisplayAndroid::get_egl_display - eglGetDisplay failed with error 0x%x", eglError); + g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "Failed to get EGL display"); + return nullptr; + } + + EGLint major = 0; + EGLint minor = 0; + if (!eglInitialize(eglDisplay, &major, &minor)) { + EGLint eglError = eglGetError(); + Logging::logError("WPEDisplayAndroid::get_egl_display - eglInitialize failed with error 0x%x", eglError); + g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "Failed to initialize EGL"); + return nullptr; + } + + Logging::logDebug("EGL initialized: version %d.%d", major, minor); + + priv->eglDisplay = eglDisplay; + return priv->eglDisplay; +} + +static gboolean wpeDisplayAndroidUseExplicitSync(WPEDisplay* /*display*/) { return TRUE; } + +static WPEInputMethodContext* wpeDisplayAndroidCreateInputMethodContext(WPEDisplay* display, WPEView* view) +{ + Logging::logDebug("WPEDisplayAndroid::create_input_method_context(%p, %p)", display, view); + return wpe_input_method_context_android_new(view); +} + +static WPEBufferFormats* wpeDisplayAndroidGetPreferredBufferFormats(WPEDisplay* /*display*/) +{ + static const struct { + uint32_t fourcc; + uint64_t modifier; + } formats[] = { + {0x34324152, 0}, // DRM_FORMAT_RGBA8888 + {0x34325852, 0}, // DRM_FORMAT_RGBX8888 + {0x34324752, 0}, // DRM_FORMAT_RGB888 + {0x36314752, 0}, // DRM_FORMAT_RGB565 + }; + + auto* builder = wpe_buffer_formats_builder_new(nullptr); + wpe_buffer_formats_builder_append_group(builder, nullptr, WPE_BUFFER_FORMAT_USAGE_RENDERING); + + for (const auto& format : formats) { + wpe_buffer_formats_builder_append_format(builder, format.fourcc, format.modifier); + } + + return wpe_buffer_formats_builder_end(builder); +} + +static void wpe_display_android_class_init(WPEDisplayAndroidClass* klass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(klass); + objectClass->dispose = wpeDisplayAndroidDispose; + + WPEDisplayClass* displayClass = WPE_DISPLAY_CLASS(klass); + displayClass->connect = wpeDisplayAndroidConnect; + displayClass->create_view = wpeDisplayAndroidCreateView; + displayClass->get_egl_display = wpeDisplayAndroidGetEGLDisplay; + displayClass->get_preferred_buffer_formats = wpeDisplayAndroidGetPreferredBufferFormats; + displayClass->use_explicit_sync = wpeDisplayAndroidUseExplicitSync; + displayClass->create_input_method_context = wpeDisplayAndroidCreateInputMethodContext; +} + +static void wpe_display_android_init(WPEDisplayAndroid* display) +{ + Logging::logDebug("WPEDisplayAndroid::init(%p)", display); + + auto* priv = static_cast( + wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + + // Set available input devices for Android + auto inputDevices = static_cast( + WPE_AVAILABLE_INPUT_DEVICE_TOUCHSCREEN | WPE_AVAILABLE_INPUT_DEVICE_KEYBOARD); + wpe_display_set_available_input_devices(WPE_DISPLAY(display), inputDevices); + + // Create the toplevel for this display + priv->toplevel = wpe_toplevel_android_new(WPE_DISPLAY(display)); +} + +WPEDisplay* wpe_display_android_new(void) { return WPE_DISPLAY(g_object_new(WPE_TYPE_DISPLAY_ANDROID, nullptr)); } + +WPEToplevel* wpe_display_android_get_toplevel(WPEDisplay* display) +{ + g_return_val_if_fail(WPE_IS_DISPLAY_ANDROID(display), nullptr); + + auto* priv = static_cast( + wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + + return priv->toplevel; +} diff --git a/wpeview/src/main/cpp/Common/WPEDisplayAndroid.h b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.h new file mode 100644 index 000000000..d58c09b41 --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.h @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_DISPLAY_ANDROID (wpe_display_android_get_type()) +G_DECLARE_FINAL_TYPE(WPEDisplayAndroid, wpe_display_android, WPE, DISPLAY_ANDROID, WPEDisplay) + +WPE_API WPEDisplay* wpe_display_android_new(void); + +WPE_API WPEToplevel* wpe_display_android_get_toplevel(WPEDisplay* display); + +G_END_DECLS diff --git a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp new file mode 100644 index 000000000..213117ada --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp @@ -0,0 +1,203 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WPEInputMethodContextAndroid.h" + +#include "Logging.h" + +#include + +// Map from WPEView to WPEInputMethodContext for lookup from JNI +static std::unordered_map s_contextMap; + +// Map from WPEView to focus callbacks (registered before context is created) +struct FocusCallbacks { + WPEInputMethodContextAndroidFocusCallback focusInCallback = nullptr; + WPEInputMethodContextAndroidFocusCallback focusOutCallback = nullptr; + void* userData = nullptr; +}; +static std::unordered_map s_pendingCallbacks; + +struct _WPEInputMethodContextAndroid { + WPEInputMethodContext parent; +}; + +typedef struct { + WPEInputMethodContextAndroidFocusCallback focusInCallback; + WPEInputMethodContextAndroidFocusCallback focusOutCallback; + void* callbackUserData; +} WPEInputMethodContextAndroidPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE( + WPEInputMethodContextAndroid, wpe_input_method_context_android, WPE_TYPE_INPUT_METHOD_CONTEXT) + +static inline WPEInputMethodContextAndroidPrivate* getPrivate(WPEInputMethodContextAndroid* context) +{ + return static_cast( + wpe_input_method_context_android_get_instance_private(context)); +} + +static void wpeInputMethodContextAndroidDispose(GObject* object) +{ + auto* context = WPE_INPUT_METHOD_CONTEXT(object); + auto* view = wpe_input_method_context_get_view(context); + if (view != nullptr) { + Logging::logDebug("WPEInputMethodContextAndroid::dispose - removing from map for view %p", view); + s_contextMap.erase(view); + } + G_OBJECT_CLASS(wpe_input_method_context_android_parent_class)->dispose(object); +} + +static void wpeInputMethodContextAndroidGetPreeditString( + WPEInputMethodContext* /*context*/, char** text, GList** underlines, guint* cursorOffset) +{ + // No preedit support for now - all text is committed immediately + if (text != nullptr) + *text = g_strdup(""); + if (underlines != nullptr) + *underlines = nullptr; + if (cursorOffset != nullptr) + *cursorOffset = 0; +} + +static void wpeInputMethodContextAndroidFocusIn(WPEInputMethodContext* context) +{ + auto* view = wpe_input_method_context_get_view(context); + Logging::logDebug("WPEInputMethodContextAndroid::focus_in(%p) view=%p callback=%p", context, view, + getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context))->focusInCallback); + auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); + if (priv->focusInCallback != nullptr) + priv->focusInCallback(priv->callbackUserData); +} + +static void wpeInputMethodContextAndroidFocusOut(WPEInputMethodContext* context) +{ + Logging::logDebug("WPEInputMethodContextAndroid::focus_out(%p)", context); + auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); + if (priv->focusOutCallback != nullptr) + priv->focusOutCallback(priv->callbackUserData); +} + +static void wpeInputMethodContextAndroidReset(WPEInputMethodContext* /*context*/) +{ + // No state to reset +} + +static void wpe_input_method_context_android_class_init(WPEInputMethodContextAndroidClass* klass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(klass); + objectClass->dispose = wpeInputMethodContextAndroidDispose; + + WPEInputMethodContextClass* imContextClass = WPE_INPUT_METHOD_CONTEXT_CLASS(klass); + imContextClass->get_preedit_string = wpeInputMethodContextAndroidGetPreeditString; + imContextClass->focus_in = wpeInputMethodContextAndroidFocusIn; + imContextClass->focus_out = wpeInputMethodContextAndroidFocusOut; + imContextClass->reset = wpeInputMethodContextAndroidReset; +} + +static void wpe_input_method_context_android_init(WPEInputMethodContextAndroid* /*self*/) +{ + Logging::logDebug("WPEInputMethodContextAndroid::init"); +} + +WPEInputMethodContext* wpe_input_method_context_android_new(WPEView* view) +{ + Logging::logDebug("WPEInputMethodContextAndroid::new(%p)", view); + auto* context + = WPE_INPUT_METHOD_CONTEXT(g_object_new(WPE_TYPE_INPUT_METHOD_CONTEXT_ANDROID, "view", view, nullptr)); + + // Store in map for later lookup from JNI + s_contextMap[view] = context; + Logging::logDebug("WPEInputMethodContextAndroid::new - added to map, now has %zu entries", s_contextMap.size()); + + // Check if there are pending callbacks registered before this context was created + auto it = s_pendingCallbacks.find(view); + if (it != s_pendingCallbacks.end()) { + auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); + priv->focusInCallback = it->second.focusInCallback; + priv->focusOutCallback = it->second.focusOutCallback; + priv->callbackUserData = it->second.userData; + s_pendingCallbacks.erase(it); + Logging::logDebug("WPEInputMethodContextAndroid::new - applied pending callbacks"); + } + + return context; +} + +WPEInputMethodContext* wpe_input_method_context_android_get_for_view(WPEView* view) +{ + auto it = s_contextMap.find(view); + if (it != s_contextMap.end()) + return it->second; + return nullptr; +} + +void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* context, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) +{ + g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); + auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); + priv->focusInCallback = focusInCallback; + priv->focusOutCallback = focusOutCallback; + priv->callbackUserData = userData; + Logging::logDebug("WPEInputMethodContextAndroid::set_focus_callbacks(%p, %p, %p, %p)", context, focusInCallback, + focusOutCallback, userData); +} + +void wpe_input_method_context_android_set_focus_callbacks_for_view(WPEView* view, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) +{ + g_return_if_fail(view != nullptr); + + // Check if context already exists + auto* context = wpe_input_method_context_android_get_for_view(view); + if (context != nullptr) { + wpe_input_method_context_android_set_focus_callbacks(context, focusInCallback, focusOutCallback, userData); + return; + } + + // Context doesn't exist yet - store callbacks for later + s_pendingCallbacks[view] = {focusInCallback, focusOutCallback, userData}; + Logging::logDebug("WPEInputMethodContextAndroid::set_focus_callbacks_for_view(%p) - stored as pending", view); +} + +void wpe_input_method_context_android_commit_text(WPEInputMethodContext* context, const char* text) +{ + g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); + auto* view = wpe_input_method_context_get_view(context); + Logging::logDebug("WPEInputMethodContextAndroid::commit_text(%p, '%s') view=%p", context, text, view); + + // Check if this context is in our map (sanity check) + auto* mapContext = wpe_input_method_context_android_get_for_view(view); + if (mapContext != context) { + Logging::logError( + "WPEInputMethodContextAndroid::commit_text - context mismatch! map=%p, this=%p", mapContext, context); + } + + g_signal_emit_by_name(context, "committed", text); + Logging::logDebug("WPEInputMethodContextAndroid::commit_text - signal emitted"); +} + +void wpe_input_method_context_android_delete_surrounding(WPEInputMethodContext* context, int offset, unsigned int count) +{ + g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); + Logging::logDebug("WPEInputMethodContextAndroid::delete_surrounding(%p, %d, %u)", context, offset, count); + g_signal_emit_by_name(context, "delete-surrounding", offset, count); +} diff --git a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h new file mode 100644 index 000000000..28dbd0815 --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_INPUT_METHOD_CONTEXT_ANDROID (wpe_input_method_context_android_get_type()) +G_DECLARE_FINAL_TYPE(WPEInputMethodContextAndroid, wpe_input_method_context_android, WPE, INPUT_METHOD_CONTEXT_ANDROID, + WPEInputMethodContext) + +// Callback type for focus notifications +typedef void (*WPEInputMethodContextAndroidFocusCallback)(void* userData); + +WPE_API WPEInputMethodContext* wpe_input_method_context_android_new(WPEView* view); + +// Get the WPEInputMethodContext for a given view (for JNI lookup) +WPE_API WPEInputMethodContext* wpe_input_method_context_android_get_for_view(WPEView* view); + +// Set callbacks for focus in/out events (called by WebKit when input field gains/loses focus) +// Use this version if you have the context +WPE_API void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* context, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); + +// Register callbacks by view - use this when context may not exist yet +// Callbacks will be applied when the context is created +WPE_API void wpe_input_method_context_android_set_focus_callbacks_for_view(WPEView* view, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); + +// Methods for Java to call via JNI to commit text and delete surrounding text +WPE_API void wpe_input_method_context_android_commit_text(WPEInputMethodContext* context, const char* text); +WPE_API void wpe_input_method_context_android_delete_surrounding( + WPEInputMethodContext* context, int offset, unsigned int count); + +G_END_DECLS diff --git a/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp new file mode 100644 index 000000000..ba4e01bf6 --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp @@ -0,0 +1,167 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WPEToplevelAndroid.h" + +#include "Logging.h" + +/** + * WPEToplevelAndroid: + * + * Android implementation of #WPEToplevel for the WPE Platform API. + * This provides a minimal toplevel implementation for Android applications. + */ + +struct _WPEToplevelAndroid { + WPEToplevel parent; +}; + +typedef struct { + // Reserved for future use +} WPEToplevelAndroidPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE(WPEToplevelAndroid, wpe_toplevel_android, WPE_TYPE_TOPLEVEL) + +static void wpeToplevelAndroidConstructed(GObject* object) +{ + G_OBJECT_CLASS(wpe_toplevel_android_parent_class)->constructed(object); + + Logging::logDebug("WPEToplevelAndroid::constructed(%p)", object); + + auto* toplevel = WPE_TOPLEVEL(object); + + // Initialize the toplevel with ACTIVE state + // Android apps start in active state when visible + wpe_toplevel_state_changed(toplevel, WPE_TOPLEVEL_STATE_ACTIVE); +} + +static void wpeToplevelAndroidSetTitle(WPEToplevel* toplevel, const char* title) +{ + Logging::logDebug("WPEToplevelAndroid::set_title(%p, %s)", toplevel, title ? title : "(null)"); +} + +static WPEScreen* wpeToplevelAndroidGetScreen(WPEToplevel* toplevel) +{ + Logging::logDebug("WPEToplevelAndroid::get_screen(%p)", toplevel); + return nullptr; +} + +static gboolean wpeToplevelAndroidResize(WPEToplevel* toplevel, int width, int height) +{ + Logging::logDebug("WPEToplevelAndroid::resize(%p, %d, %d)", toplevel, width, height); + wpe_toplevel_resized(toplevel, width, height); + return TRUE; +} + +static gboolean wpeToplevelAndroidSetFullscreen(WPEToplevel* toplevel, gboolean fullscreen) +{ + Logging::logDebug("WPEToplevelAndroid::set_fullscreen(%p, %s)", toplevel, fullscreen ? "true" : "false"); + + WPEToplevelState const currentState = wpe_toplevel_get_state(toplevel); + WPEToplevelState newState; + + if (fullscreen) { + newState = static_cast(currentState | WPE_TOPLEVEL_STATE_FULLSCREEN); + } else { + newState = static_cast(currentState & ~WPE_TOPLEVEL_STATE_FULLSCREEN); + } + + if (newState != currentState) + wpe_toplevel_state_changed(toplevel, newState); + + return TRUE; +} + +static gboolean wpeToplevelAndroidSetMaximized(WPEToplevel* toplevel, gboolean maximized) +{ + Logging::logDebug("WPEToplevelAndroid::set_maximized(%p, %s)", toplevel, maximized ? "true" : "false"); + + WPEToplevelState const currentState = wpe_toplevel_get_state(toplevel); + WPEToplevelState newState; + + if (maximized) { + newState = static_cast(currentState | WPE_TOPLEVEL_STATE_MAXIMIZED); + } else { + newState = static_cast(currentState & ~WPE_TOPLEVEL_STATE_MAXIMIZED); + } + + if (newState != currentState) + wpe_toplevel_state_changed(toplevel, newState); + + return TRUE; +} + +static gboolean wpeToplevelAndroidSetMinimized(WPEToplevel* toplevel) +{ + Logging::logDebug("WPEToplevelAndroid::set_minimized(%p)", toplevel); + + WPEToplevelState const currentState = wpe_toplevel_get_state(toplevel); + auto newState = static_cast(currentState & ~WPE_TOPLEVEL_STATE_ACTIVE); + + if (newState != currentState) + wpe_toplevel_state_changed(toplevel, newState); + + return TRUE; +} + +static WPEBufferFormats* wpeToplevelAndroidGetPreferredBufferFormats(WPEToplevel* toplevel) +{ + auto* display = wpe_toplevel_get_display(toplevel); + + if (display) { + return wpe_display_get_preferred_buffer_formats(display); + } + return nullptr; +} + +static void wpe_toplevel_android_class_init(WPEToplevelAndroidClass* toplevelAndroidClass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(toplevelAndroidClass); + objectClass->constructed = wpeToplevelAndroidConstructed; + + WPEToplevelClass* toplevelClass = WPE_TOPLEVEL_CLASS(toplevelAndroidClass); + toplevelClass->set_title = wpeToplevelAndroidSetTitle; + toplevelClass->get_screen = wpeToplevelAndroidGetScreen; + toplevelClass->resize = wpeToplevelAndroidResize; + toplevelClass->set_fullscreen = wpeToplevelAndroidSetFullscreen; + toplevelClass->set_maximized = wpeToplevelAndroidSetMaximized; + toplevelClass->set_minimized = wpeToplevelAndroidSetMinimized; + toplevelClass->get_preferred_buffer_formats = wpeToplevelAndroidGetPreferredBufferFormats; +} + +static void wpe_toplevel_android_init(WPEToplevelAndroid* toplevel) +{ + Logging::logDebug("WPEToplevelAndroid::init(%p)", toplevel); +} + +/** + * wpe_toplevel_android_new: + * @display: a #WPEDisplay + * + * Create a new #WPEToplevel on @display. + * + * Returns: (transfer full): a #WPEToplevel + */ +WPEToplevel* wpe_toplevel_android_new(WPEDisplay* display) +{ + g_return_val_if_fail(WPE_IS_DISPLAY(display), nullptr); + + Logging::logDebug("wpe_toplevel_android_new(%p)", display); + + return WPE_TOPLEVEL(g_object_new(WPE_TYPE_TOPLEVEL_ANDROID, "display", display, nullptr)); +} diff --git a/wpeview/src/main/cpp/Common/WPEToplevelAndroid.h b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.h new file mode 100644 index 000000000..24e4b6163 --- /dev/null +++ b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.h @@ -0,0 +1,31 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_TOPLEVEL_ANDROID (wpe_toplevel_android_get_type()) +G_DECLARE_FINAL_TYPE(WPEToplevelAndroid, wpe_toplevel_android, WPE, TOPLEVEL_ANDROID, WPEToplevel) + +WPEToplevel* wpe_toplevel_android_new(WPEDisplay* display); + +G_END_DECLS diff --git a/wpeview/src/main/cpp/Runtime/InputMethodContext.cpp b/wpeview/src/main/cpp/Runtime/InputMethodContext.cpp deleted file mode 100644 index e8fa74d59..000000000 --- a/wpeview/src/main/cpp/Runtime/InputMethodContext.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/** - * Copyright (C) 2022 Igalia S.L. - * Author: Fernando Jimenez Moreno - * Author: Loïc Le Page - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "InputMethodContext.h" - -#include "Logging.h" - -namespace { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" -G_DECLARE_DERIVABLE_TYPE(InternalInputMethodContext, internal_input_method_context, WPE_ANDROID, - INTERNAL_INPUT_METHOD_CONTEXT, WebKitInputMethodContext); -#pragma clang diagnostic pop - -struct _InternalInputMethodContextClass { - WebKitInputMethodContextClass m_parentClass; -}; - -enum class InternalInputMethodContextProperty : guint { - ObserverPropertyId = 1, - NumberOfProperties -}; - -struct InternalInputMethodContextPrivate { - InputMethodContextObserver* m_observer; -}; - -G_DEFINE_TYPE_WITH_PRIVATE(InternalInputMethodContext, internal_input_method_context, WEBKIT_TYPE_INPUT_METHOD_CONTEXT); - -inline InternalInputMethodContextPrivate* getInternalInputMethodContextPrivate(GObject* object) noexcept -{ - return reinterpret_cast( - internal_input_method_context_get_instance_private(WPE_ANDROID_INTERNAL_INPUT_METHOD_CONTEXT(object))); -} - -void setProperty(GObject* object, guint propertyId, const GValue* value, GParamSpec* paramSpec) noexcept -{ - InternalInputMethodContextPrivate* ctx = getInternalInputMethodContextPrivate(object); - switch (static_cast(propertyId)) { - case InternalInputMethodContextProperty::ObserverPropertyId: - ctx->m_observer = reinterpret_cast(g_value_get_pointer(value)); - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propertyId, paramSpec); - break; - } -} - -void getProperty(GObject* object, guint propertyId, GValue* value, GParamSpec* paramSpec) noexcept -{ - InternalInputMethodContextPrivate* ctx = getInternalInputMethodContextPrivate(object); - switch (static_cast(propertyId)) { - case InternalInputMethodContextProperty::ObserverPropertyId: - g_value_set_pointer(value, ctx->m_observer); - break; - - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, propertyId, paramSpec); - break; - } -} - -void notifyFocusIn(WebKitInputMethodContext* context) noexcept -{ - InternalInputMethodContextPrivate* ctx = getInternalInputMethodContextPrivate(G_OBJECT(context)); - Logging::logDebug("internal_input_method_context_notify_focus_in %p", ctx->m_observer); - if (ctx->m_observer != nullptr) - ctx->m_observer->onInputMethodContextIn(); -} - -void notifyFocusOut(WebKitInputMethodContext* context) noexcept -{ - InternalInputMethodContextPrivate* ctx = getInternalInputMethodContextPrivate(G_OBJECT(context)); - Logging::logDebug("internal_input_method_context_notify_focus_out %p", ctx->m_observer); - if (ctx->m_observer != nullptr) - ctx->m_observer->onInputMethodContextOut(); -} - -void internal_input_method_context_class_init(InternalInputMethodContextClass* klass) -{ - GObjectClass* objectKlass = G_OBJECT_CLASS(klass); - objectKlass->set_property = setProperty; - objectKlass->get_property = getProperty; - - static GParamSpec* s_properties[static_cast(InternalInputMethodContextProperty::NumberOfProperties)] - = {nullptr, - g_param_spec_pointer("observer", "Observer", "WKWebView event observer", - // NOLINTNEXTLINE(hicpp-signed-bitwise, clang-analyzer-optin.core.EnumCastOutOfRange) - static_cast(G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE))}; - g_object_class_install_properties( - objectKlass, static_cast(InternalInputMethodContextProperty::NumberOfProperties), s_properties); - - WebKitInputMethodContextClass* webkitInputMethodContextKlass = WEBKIT_INPUT_METHOD_CONTEXT_CLASS(klass); - webkitInputMethodContextKlass->notify_focus_in = notifyFocusIn; - webkitInputMethodContextKlass->notify_focus_out = notifyFocusOut; -} - -void internal_input_method_context_init(InternalInputMethodContext* /*self*/) {} -} // namespace - -InputMethodContext::InputMethodContext(InputMethodContextObserver* observer) - : m_observer(observer) - , m_webKitInputMethodContext({WEBKIT_INPUT_METHOD_CONTEXT(g_object_new( - internal_input_method_context_get_type(), "observer", m_observer, nullptr)), - [](auto* ptr) { g_object_unref(ptr); }}) -{ -} - -void InputMethodContext::setContent(const char* utf8Content) const noexcept -{ - g_signal_emit_by_name(m_webKitInputMethodContext.get(), "committed", utf8Content); -} - -void InputMethodContext::deleteContent(int offset) const noexcept -{ - g_signal_emit_by_name(m_webKitInputMethodContext.get(), "delete-surrounding", offset, 1); -} diff --git a/wpeview/src/main/cpp/Runtime/InputMethodContext.h b/wpeview/src/main/cpp/Runtime/InputMethodContext.h deleted file mode 100644 index 2fc5e7fcc..000000000 --- a/wpeview/src/main/cpp/Runtime/InputMethodContext.h +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (C) 2022 Igalia S.L. - * Author: Fernando Jimenez Moreno - * Author: Loïc Le Page - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include -#include - -class InputMethodContextObserver { -public: - InputMethodContextObserver() = default; - virtual ~InputMethodContextObserver() = default; - - InputMethodContextObserver(InputMethodContextObserver&&) = default; - InputMethodContextObserver& operator=(InputMethodContextObserver&&) = default; - InputMethodContextObserver(const InputMethodContextObserver&) = default; - InputMethodContextObserver& operator=(const InputMethodContextObserver&) = default; - - virtual void onInputMethodContextIn() noexcept = 0; - virtual void onInputMethodContextOut() noexcept = 0; -}; - -class InputMethodContext final { -public: - InputMethodContext(InputMethodContextObserver* observer); - - WebKitInputMethodContext* webKitInputMethodContext() const noexcept { return m_webKitInputMethodContext.get(); } - - void setContent(const char* utf8Content) const noexcept; - void deleteContent(int offset) const noexcept; - -private: - InputMethodContextObserver* m_observer; - - template using ProtectedUniquePointer = std::unique_ptr>; - ProtectedUniquePointer m_webKitInputMethodContext {}; -}; diff --git a/wpeview/src/main/cpp/Runtime/Renderer.h b/wpeview/src/main/cpp/Runtime/Renderer.h index 01ba906d4..e4dfa0d80 100644 --- a/wpeview/src/main/cpp/Runtime/Renderer.h +++ b/wpeview/src/main/cpp/Runtime/Renderer.h @@ -21,13 +21,14 @@ #pragma once -#include "ScopedWPEAndroidBuffer.h" - +#include #include #include #include "ScopedFD.h" +typedef struct _WPEBufferAndroid WPEBufferAndroid; + class Renderer { public: Renderer() = default; @@ -46,5 +47,7 @@ class Renderer { virtual void onSurfaceRedrawNeeded() noexcept = 0; virtual void onSurfaceDestroyed() noexcept = 0; - virtual void commitBuffer(std::shared_ptr buffer, std::shared_ptr fenceFD) = 0; + virtual void commitBuffer( + AHardwareBuffer* hardwareBuffer, WPEBufferAndroid* wpeBuffer, std::shared_ptr fenceFD) + = 0; }; diff --git a/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.cpp b/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.cpp index 7cfe6f6cb..01bd078a0 100644 --- a/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.cpp +++ b/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.cpp @@ -21,189 +21,276 @@ #include "RendererSurfaceControl.h" -#include #include -#include "Fence.h" #include "Logging.h" #include "SurfaceControl.h" -#include "WKRuntime.h" -RendererSurfaceControl::RendererSurfaceControl(WPEAndroidViewBackend* viewBackend, uint32_t width, uint32_t height) - : m_viewBackend(viewBackend) - , m_size({width, height}) +#include + +namespace { +// Surface transaction constants +// Visibility is always SHOW for active rendering surfaces +constexpr ASurfaceTransactionVisibility kSurfaceVisibility = ASURFACE_TRANSACTION_VISIBILITY_SHOW; +// Z-order determines stacking; 0 is the base layer +constexpr int32_t kSurfaceZOrder = 0; +} // namespace + +RendererSurfaceControl::RendererSurfaceControl(uint32_t width, uint32_t height) + : m_size({width, height}) { - Logging::logDebug("RendererSurfaceControl(%p, %u, %u)", m_viewBackend, m_size.m_width, m_size.m_height); + Logging::logDebug("RendererSurfaceControl(%u, %u)", m_size.m_width, m_size.m_height); } RendererSurfaceControl::~RendererSurfaceControl() { Logging::logDebug("~RendererSurfaceControl()"); while (!m_pendingTransactionQueue.empty()) { - m_pendingTransactionQueue.front().setParent(*m_surface, nullptr); - m_pendingTransactionQueue.front().apply(); + if (m_surface) { + m_pendingTransactionQueue.front().setParent(*m_surface, nullptr); + m_pendingTransactionQueue.front().apply(); + } m_pendingTransactionQueue.pop(); - m_numTransactionCommitOrAckPending--; + } + + if (m_currentFrameBuffer != nullptr) { + g_object_unref(m_currentFrameBuffer); + m_currentFrameBuffer = nullptr; + } + + if (m_pendingCommitBuffer != nullptr) { + g_object_unref(m_pendingCommitBuffer); + m_pendingCommitBuffer = nullptr; } } +// Initialize the surface control when the Android surface is created void RendererSurfaceControl::onSurfaceCreated(ANativeWindow* window) noexcept { m_surface = std::make_shared(window, "Surface"); + + // Clear any existing queued transactions for the old surface. + m_numTransactionCommitOrAckPending = 0; + while (!m_pendingTransactionQueue.empty()) { + m_pendingTransactionQueue.pop(); + } } +// Update surface dimensions when the Android surface changes void RendererSurfaceControl::onSurfaceChanged(int /*format*/, uint32_t width, uint32_t height) noexcept { + Logging::logDebug( + "onSurfaceChanged(%u, %u) - previous size (%u, %u)", width, height, m_size.m_width, m_size.m_height); + + bool const sizeChanged = (m_size.m_width != width || m_size.m_height != height); m_size.m_width = width; m_size.m_height = height; + + if (!sizeChanged) + return; + + // Size changed - invalidate all stale buffers that have old dimensions + + // Release pending commit buffer (old dimensions) + if (m_pendingCommitBuffer != nullptr) { + if (m_wpeView != nullptr) + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(m_pendingCommitBuffer)); + g_object_unref(m_pendingCommitBuffer); + m_pendingCommitBuffer = nullptr; + m_pendingCommitFenceFD.reset(); + } + + // Release current frame buffer (in-flight with old dimensions) + if (m_currentFrameBuffer != nullptr) { + if (m_wpeView != nullptr) + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(m_currentFrameBuffer)); + g_object_unref(m_currentFrameBuffer); + m_currentFrameBuffer = nullptr; + } + + // Clear pending transaction queue (old-sized buffers) + while (!m_pendingTransactionQueue.empty()) + m_pendingTransactionQueue.pop(); + m_numTransactionCommitOrAckPending = 0; + + Logging::logDebug("onSurfaceChanged: cleared stale buffers for resize"); } -void RendererSurfaceControl::onSurfaceRedrawNeeded() noexcept // NOLINT(bugprone-exception-escape) +// Handle Android surface redraw requests by committing pending buffers +void RendererSurfaceControl::onSurfaceRedrawNeeded() noexcept { - if (m_surface != nullptr) { - if (m_pendingCommitBuffer != nullptr && m_pendingCommitFenceFD != nullptr) { - Logging::logDebug("RendererSurfaceControl::onSurfaceRedrawNeeded - sending pending commit"); - auto buffer = m_pendingCommitBuffer; - auto fence = m_pendingCommitFenceFD; - commitBuffer(buffer, fence); - } else if (m_frontBuffer != nullptr && m_pendingFrontBufferRedraw) { - auto fence = std::make_shared(-1); - Logging::logDebug("RendererSurfaceControl::onSurfaceRedrawNeeded - front buffer commit"); - commitBuffer(m_frontBuffer, fence); + Logging::logDebug("onSurfaceRedrawNeeded()"); + + if (m_surface == nullptr) { + Logging::logDebug("No surface available"); + return; + } + + // Only handle deferred commits from when surface was unavailable + if (m_pendingCommitBuffer != nullptr) { + Logging::logDebug("Committing pending buffer"); + + AHardwareBuffer* hardwareBuffer = wpe_buffer_android_get_hardware_buffer(m_pendingCommitBuffer); + if (hardwareBuffer == nullptr) { + Logging::logError("Failed to get hardware buffer from pending buffer"); + return; } + + // Transfer ownership and apply transaction + WPEBufferAndroid* bufferToCommit = m_pendingCommitBuffer; + int fenceFD = m_pendingCommitFenceFD ? m_pendingCommitFenceFD->release() : -1; + m_pendingCommitBuffer = nullptr; + m_pendingCommitFenceFD.reset(); + + applyBufferTransaction(hardwareBuffer, bufferToCommit, fenceFD); + g_object_unref(bufferToCommit); + return; } + + // No pending buffer - WebKit will render new content when ready + Logging::logDebug("No pending buffer - waiting for WebKit"); } +// Release the surface control when the Android surface is destroyed void RendererSurfaceControl::onSurfaceDestroyed() noexcept { + Logging::logDebug("onSurfaceDestroyed()"); m_surface.reset(); - m_pendingFrontBufferRedraw = true; } +// Commit a new buffer to the surface or defer if surface is unavailable void RendererSurfaceControl::commitBuffer( - std::shared_ptr buffer, std::shared_ptr fenceFD) + AHardwareBuffer* hardwareBuffer, WPEBufferAndroid* wpeBuffer, std::shared_ptr fenceFD) { - if (m_surface == nullptr) { // surface is lost - if (m_pendingCommitBuffer != nullptr) - WPEAndroidViewBackend_dispatchReleaseBuffer(m_viewBackend, m_pendingCommitBuffer->wpeBuffer()); + // Defer commit until surface is restored + if (m_surface == nullptr) { + // Release old pending commit buffer if exists + if (m_pendingCommitBuffer != nullptr) { + if (m_wpeView != nullptr) + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(m_pendingCommitBuffer)); + g_object_unref(m_pendingCommitBuffer); + } - m_pendingCommitBuffer = buffer; + // Store new buffer as pending + m_pendingCommitBuffer = wpeBuffer; + g_object_ref(m_pendingCommitBuffer); m_pendingCommitFenceFD = fenceFD; - if (m_frontBuffer != nullptr) { - WPEAndroidViewBackend_dispatchReleaseBuffer(m_viewBackend, m_frontBuffer->wpeBuffer()); - m_frontBuffer = nullptr; + + // Release current frame buffer since we can't display anything + if (m_currentFrameBuffer != nullptr) { + if (m_wpeView != nullptr) + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(m_currentFrameBuffer)); + g_object_unref(m_currentFrameBuffer); + m_currentFrameBuffer = nullptr; } return; } + // Surface available - release any pending buffer from when surface was unavailable if (m_pendingCommitBuffer != nullptr) { + if (m_wpeView != nullptr) + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(m_pendingCommitBuffer)); + g_object_unref(m_pendingCommitBuffer); m_pendingCommitBuffer = nullptr; - m_pendingCommitFenceFD = nullptr; + m_pendingCommitFenceFD.reset(); } - m_pendingFrontBufferRedraw = false; + applyBufferTransaction(hardwareBuffer, wpeBuffer, fenceFD->release()); +} +// Handle transaction completion acknowledgment and release buffer for reuse +void RendererSurfaceControl::onTransActionAckOnBrowserThread( + std::optional releasedBuffer, SurfaceControl::TransactionStats stats) +{ + // Android framework releases its buffer reference in this callback; wait for it before buffer reuse + if (releasedBuffer.has_value() && releasedBuffer.value() != nullptr) { + auto* buffer = releasedBuffer.value(); + + // Set release fence on buffer so Web Process waits before reusing + if (!stats.m_surfaceStats.empty() && stats.m_surfaceStats[0].m_fence) { + int releaseFence = stats.m_surfaceStats[0].m_fence->release(); + wpe_buffer_set_release_fence(WPE_BUFFER(buffer), releaseFence); + } + + // Notify WebKit that the buffer can be reused + if (m_wpeView != nullptr) { + Logging::logDebug("onTransActionAckOnBrowserThread: buffer_released %p", buffer); + wpe_view_buffer_released(m_wpeView, WPE_BUFFER(buffer)); + } + // Note: Callback handles g_object_unref for the captured buffer + } +} + +// Process transaction commit completion, trigger buffer rendered callback, and apply queued transactions +void RendererSurfaceControl::onTransactionCommittedOnBrowserThread(std::optional renderedBuffer) +{ + // Notify WebKit that the buffer is now on screen + if (renderedBuffer.has_value() && renderedBuffer.value() != nullptr) { + if (m_wpeView != nullptr) { + Logging::logDebug("onTransactionCommittedOnBrowserThread: buffer_rendered %p", renderedBuffer.value()); + wpe_view_buffer_rendered(m_wpeView, WPE_BUFFER(renderedBuffer.value())); + } + } + + if (m_numTransactionCommitOrAckPending > 0) + m_numTransactionCommitOrAckPending--; + + // Apply next queued transaction if any + if (!m_pendingTransactionQueue.empty()) { + m_numTransactionCommitOrAckPending++; + m_pendingTransactionQueue.front().apply(); + m_pendingTransactionQueue.pop(); + } +} + +// Create and apply buffer transaction with callbacks, queueing if another transaction is pending +void RendererSurfaceControl::applyBufferTransaction( + AHardwareBuffer* hardwareBuffer, WPEBufferAndroid* wpeBuffer, int fenceFD) +{ SurfaceControl::Transaction transaction; - transaction.setVisibility(*m_surface, ASURFACE_TRANSACTION_VISIBILITY_SHOW); - transaction.setZOrder(*m_surface, 0); - transaction.setBuffer(*m_surface, buffer->buffer(), fenceFD->release()); + transaction.setVisibility(*m_surface, kSurfaceVisibility); + transaction.setZOrder(*m_surface, kSurfaceZOrder); + transaction.setBuffer(*m_surface, hardwareBuffer, fenceFD); - ResourceRefs resourcesToRelease; - resourcesToRelease.swap(m_currentFrameResources); - m_currentFrameResources.clear(); + // Previous buffer will be released in onCompleteCallback + WPEBufferAndroid* bufferToRelease = m_currentFrameBuffer; - auto& resourceRef = m_currentFrameResources[m_surface->surfaceControl()]; - resourceRef.m_surface = m_surface; - resourceRef.m_scopedBuffer = buffer; + // Take ownership of the new buffer + m_currentFrameBuffer = wpeBuffer; + g_object_ref(m_currentFrameBuffer); std::weak_ptr const weakPtr(shared_from_this()); - auto onCompleteCallback = [weakPtr, resources = std::move(resourcesToRelease)](auto&& stats) { + + // Complete callback - release previous buffer + if (bufferToRelease != nullptr) + g_object_ref(bufferToRelease); + + transaction.setOnCompleteCallback([weakPtr, buffer = bufferToRelease](SurfaceControl::TransactionStats&& stats) { auto ptr = weakPtr.lock(); if (ptr) - ptr->onTransActionAckOnBrowserThread(resources, std::forward(stats)); - }; - transaction.setOnCompleteCallback(std::move(onCompleteCallback)); + ptr->onTransActionAckOnBrowserThread(buffer, std::move(stats)); + // Always unref - callback owns this ref + if (buffer != nullptr) + g_object_unref(buffer); + }); + + // Commit callback - notify buffer rendered + g_object_ref(wpeBuffer); - auto onCommitCallback = [weakPtr]() { + transaction.setOnCommitCallback([weakPtr, buffer = wpeBuffer]() { auto ptr = weakPtr.lock(); if (ptr) - ptr->onTransactionCommittedOnBrowserThread(); - }; - transaction.setOnCommitCallback(std::move(onCommitCallback)); + ptr->onTransactionCommittedOnBrowserThread(buffer); + // Always unref - callback owns this ref + if (buffer != nullptr) + g_object_unref(buffer); + }); + // Queue transaction if one is already pending; otherwise apply immediately if (m_numTransactionCommitOrAckPending > 0) { m_pendingTransactionQueue.push(std::move(transaction)); } else { m_numTransactionCommitOrAckPending++; transaction.apply(); - m_frontBuffer = buffer; - } -} - -void RendererSurfaceControl::onTransActionAckOnBrowserThread( - ResourceRefs releasedResources, SurfaceControl::TransactionStats stats) -{ - for (auto& surfaceStat : stats.m_surfaceStats) { - auto resourceIterator = releasedResources.find(surfaceStat.m_surface); - if (resourceIterator == releasedResources.end()) { - continue; - } - - if (surfaceStat.m_fence) - resourceIterator->second.m_scopedBuffer->setReleaseFenceFD(surfaceStat.m_fence); - m_releaseBufferQueue.push(std::move(resourceIterator->second.m_scopedBuffer)); - } - releasedResources.clear(); - - // Following is from Android ASurfaceControl documentation in surface_control.h - // - // Each time a buffer is set through ASurfaceTransaction_setBuffer() on a transaction - // which is applied, the framework takes a ref on this buffer. The framework treats the - // addition of a buffer to a particular surface as a unique ref. When a transaction updates or - // removes a buffer from a surface, or removes the surface itself from the tree, this ref is - // guaranteed to be released in the OnComplete callback for this transaction. The - // ASurfaceControlStats provided in the callback for this surface may contain an optional fence - // which must be signaled before the ref is assumed to be released. - // - // The client must ensure that all pending refs on a buffer are released before attempting to reuse - // this buffer, otherwise synchronization errors may occur. - // - // TBD: Based on above description it seems that fence must be checked and waited if present before reusing the - // buffer. But in practice it seems to have no significant effect if check is done or not done - while (!m_releaseBufferQueue.empty()) { - auto& pendingBuffer = m_releaseBufferQueue.front(); - auto status = pendingBuffer->getReleaseFenceFD() != -1 ? Fence::getStatus(pendingBuffer->getReleaseFenceFD()) - : Fence::Invalid; - - if (status == Fence::NotSignaled) - break; - - if (m_frontBuffer && m_frontBuffer->wpeBuffer() == pendingBuffer->wpeBuffer()) - m_frontBuffer = nullptr; - - WPEAndroidViewBackend_dispatchReleaseBuffer(m_viewBackend, pendingBuffer->wpeBuffer()); - m_releaseBufferQueue.pop(); - } -} - -void RendererSurfaceControl::onTransactionCommittedOnBrowserThread() -{ - // We can notify WebProcess already at this point to start rendering next frame. - // This causes WPEBackend-android to use triple buffering but performance is better this way - // - // If this dispatch_frame_complete is called in onTransActionAckOnBrowserThread then WPEBackend-android - // stays in double buffering - WPEAndroidViewBackend_dispatchFrameComplete(m_viewBackend); - - processTransactionQueue(); -} - -void RendererSurfaceControl::processTransactionQueue() -{ - m_numTransactionCommitOrAckPending--; - if (!m_pendingTransactionQueue.empty()) { - m_numTransactionCommitOrAckPending++; - m_pendingTransactionQueue.front().apply(); - m_pendingTransactionQueue.pop(); } } diff --git a/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.h b/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.h index b388e5dde..b8b379ebd 100644 --- a/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.h +++ b/wpeview/src/main/cpp/Runtime/RendererSurfaceControl.h @@ -23,15 +23,22 @@ #include "Renderer.h" -#include +#include #include -#include +#include +#include +#include "ScopedFD.h" #include "SurfaceControl.h" +#include + +typedef struct _WPEBufferAndroid WPEBufferAndroid; +typedef struct _WPEView WPEView; + class RendererSurfaceControl final : public Renderer, public std::enable_shared_from_this { public: - RendererSurfaceControl(WPEAndroidViewBackend* viewBackend, uint32_t width, uint32_t height); + RendererSurfaceControl(uint32_t width, uint32_t height); ~RendererSurfaceControl() override; RendererSurfaceControl(RendererSurfaceControl&&) = delete; @@ -47,30 +54,20 @@ class RendererSurfaceControl final : public Renderer, public std::enable_shared_ void onSurfaceRedrawNeeded() noexcept override; // NOLINT(bugprone-exception-escape) void onSurfaceDestroyed() noexcept override; - void commitBuffer(std::shared_ptr buffer, std::shared_ptr fenceFD) override; - -private: - struct ResourceRef { - ResourceRef() = default; - ~ResourceRef() = default; - - ResourceRef(const ResourceRef& other) = default; - ResourceRef& operator=(const ResourceRef& other) = default; - - ResourceRef(ResourceRef&& other) = default; - ResourceRef& operator=(ResourceRef&& other) = default; + // Set WPEView for direct buffer API calls + void setWPEView(WPEView* view) { m_wpeView = view; } - std::shared_ptr m_surface; - std::shared_ptr m_scopedBuffer; - }; - using ResourceRefs = std::map; + void commitBuffer( + AHardwareBuffer* hardwareBuffer, WPEBufferAndroid* wpeBuffer, std::shared_ptr fenceFD) override; - void onTransActionAckOnBrowserThread(ResourceRefs releasedResources, SurfaceControl::TransactionStats stats); - void onTransactionCommittedOnBrowserThread(); +private: + void onTransActionAckOnBrowserThread( + std::optional releasedBuffer, SurfaceControl::TransactionStats stats); + void onTransactionCommittedOnBrowserThread(std::optional renderedBuffer); + void applyBufferTransaction(AHardwareBuffer* hardwareBuffer, WPEBufferAndroid* wpeBuffer, int fenceFD); - void processTransactionQueue(); + WPEView* m_wpeView = nullptr; - WPEAndroidViewBackend* m_viewBackend = nullptr; std::shared_ptr m_surface; struct { @@ -81,11 +78,7 @@ class RendererSurfaceControl final : public Renderer, public std::enable_shared_ std::queue m_pendingTransactionQueue; uint32_t m_numTransactionCommitOrAckPending = 0U; - ResourceRefs m_currentFrameResources; - - std::queue> m_releaseBufferQueue; - std::shared_ptr m_pendingCommitBuffer; + WPEBufferAndroid* m_currentFrameBuffer = nullptr; + WPEBufferAndroid* m_pendingCommitBuffer = nullptr; std::shared_ptr m_pendingCommitFenceFD; - std::shared_ptr m_frontBuffer; - bool m_pendingFrontBufferRedraw = false; }; diff --git a/wpeview/src/main/cpp/Runtime/ScopedWPEAndroidBuffer.h b/wpeview/src/main/cpp/Runtime/ScopedWPEAndroidBuffer.h deleted file mode 100644 index 9eb7807f9..000000000 --- a/wpeview/src/main/cpp/Runtime/ScopedWPEAndroidBuffer.h +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (C) 2022 Igalia S.L. - * Author: Zan Dobersek - * Author: Loïc Le Page - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include -#include -#include - -#include "ScopedFD.h" - -class ScopedWPEAndroidBuffer final { -public: - ScopedWPEAndroidBuffer(WPEAndroidBuffer* buffer) - : m_buffer(buffer) - { - if (m_buffer != nullptr) { - m_hardwareBuffer = WPEAndroidBuffer_getAHardwareBuffer(m_buffer); - AHardwareBuffer_acquire(m_hardwareBuffer); - AHardwareBuffer_Desc desc = {}; - AHardwareBuffer_describe(m_hardwareBuffer, &desc); - m_size = {desc.width, desc.height}; - } - } - - ScopedWPEAndroidBuffer(ScopedWPEAndroidBuffer&&) = delete; - ScopedWPEAndroidBuffer& operator=(ScopedWPEAndroidBuffer&&) = delete; - ScopedWPEAndroidBuffer(const ScopedWPEAndroidBuffer&) = delete; - ScopedWPEAndroidBuffer& operator=(const ScopedWPEAndroidBuffer&) = delete; - - ~ScopedWPEAndroidBuffer() - { - if (m_buffer != nullptr) - AHardwareBuffer_release(m_hardwareBuffer); - } - - WPEAndroidBuffer* wpeBuffer() const { return m_buffer; } - AHardwareBuffer* buffer() const noexcept { return WPEAndroidBuffer_getAHardwareBuffer(m_buffer); } - uint32_t width() const noexcept { return m_size.m_width; } - uint32_t height() const noexcept { return m_size.m_height; } - - int getReleaseFenceFD() const { return m_releaseFenceFD ? m_releaseFenceFD->get() : -1; } - void setReleaseFenceFD(std::shared_ptr releaseFenceFD) { m_releaseFenceFD = std::move(releaseFenceFD); } - -private: - WPEAndroidBuffer* m_buffer = nullptr; - ; - AHardwareBuffer* m_hardwareBuffer = nullptr; - std::shared_ptr m_releaseFenceFD; - struct { - uint32_t m_width; - uint32_t m_height; - } m_size = {}; -}; diff --git a/wpeview/src/main/cpp/Runtime/SurfaceControl.cpp b/wpeview/src/main/cpp/Runtime/SurfaceControl.cpp index 867a0fff2..e0280c0e7 100644 --- a/wpeview/src/main/cpp/Runtime/SurfaceControl.cpp +++ b/wpeview/src/main/cpp/Runtime/SurfaceControl.cpp @@ -19,7 +19,6 @@ #include "SurfaceControl.h" -#include "ScopedWPEAndroidBuffer.h" #include "WKRuntime.h" namespace { diff --git a/wpeview/src/main/cpp/Runtime/SurfaceControl.h b/wpeview/src/main/cpp/Runtime/SurfaceControl.h index 414151fa4..065339c0b 100644 --- a/wpeview/src/main/cpp/Runtime/SurfaceControl.h +++ b/wpeview/src/main/cpp/Runtime/SurfaceControl.h @@ -25,8 +25,6 @@ #include "ScopedFD.h" -class ScopedWPEAndroidBuffer; - class SurfaceControl final { public: class Surface final { diff --git a/wpeview/src/main/cpp/Runtime/WKRuntime.cpp b/wpeview/src/main/cpp/Runtime/WKRuntime.cpp index 6d57c88fb..63c628ff8 100644 --- a/wpeview/src/main/cpp/Runtime/WKRuntime.cpp +++ b/wpeview/src/main/cpp/Runtime/WKRuntime.cpp @@ -28,6 +28,7 @@ #include #include +#include /*********************************************************************************************************************** * JNI mapping with Java WKRuntime class diff --git a/wpeview/src/main/cpp/Runtime/WKSettings.cpp b/wpeview/src/main/cpp/Runtime/WKSettings.cpp index 17972c597..6d54edb72 100644 --- a/wpeview/src/main/cpp/Runtime/WKSettings.cpp +++ b/wpeview/src/main/cpp/Runtime/WKSettings.cpp @@ -21,6 +21,8 @@ #include "WKWebView.h" +#include + namespace { // FIXME: in wpe-webkit the same WebKitSettings can be shared by different // WebKitView instances which is not possible here because all the next diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.cpp b/wpeview/src/main/cpp/Runtime/WKWebView.cpp index e866c3a6b..e3563fa47 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.cpp +++ b/wpeview/src/main/cpp/Runtime/WKWebView.cpp @@ -27,22 +27,25 @@ #include "WKCallback.h" #include "WKRuntime.h" #include "WKWebContext.h" +#include "WPEDisplayAndroid.h" +#include "WPEInputMethodContextAndroid.h" +#include "WPEViewAndroid.h" #include +#include #include #include -#include namespace { -void handleCommitBuffer(void* context, WPEAndroidBuffer* buffer, int fenceID) -{ - auto* wkWebView = static_cast(context); - wkWebView->commitBuffer(buffer, fenceID); -} - const int httpErrorsStart = 400; +inline float safeDeviceDensity(float density) noexcept { return (density > 0.0F) ? density : 1.0F; } + +void onInputMethodFocusIn(void* userData) { static_cast(userData)->onInputMethodContextIn(); } + +void onInputMethodFocusOut(void* userData) { static_cast(userData)->onInputMethodContextOut(); } + class SslErrorHandler final { public: static SslErrorHandler* createHandler( @@ -157,6 +160,42 @@ class JNIWKWebViewCache final : public JNI::TypedClass { getJNIPageCache().m_onLoadChanged, wkWebView->m_webViewJavaInstance.get(), static_cast(loadEvent)); } + static void onIsLoadingChanged(WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onIsLoadingChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onIsLoadingChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_is_loading(webView))); + } + + static gboolean onLoadFailed(WKWebView* wkWebView, WebKitLoadEvent loadEvent, const char* failingURI, GError* error, + WebKitWebView* /*webView*/) noexcept + { + Logging::logDebug("WKWebView::onLoadFailed() [tid %d] uri=%s error=%s", gettid(), failingURI, + error ? error->message : "null"); + + auto jFailingUri = JNI::String(failingURI); + auto jErrorDomain = JNI::String(error ? g_quark_to_string(error->domain) : ""); + auto jErrorMessage = JNI::String(error ? error->message : ""); + + try { + return static_cast(getJNIPageCache().m_onLoadFailed.invoke(wkWebView->m_webViewJavaInstance.get(), + static_cast(loadEvent), static_cast(jFailingUri), + static_cast(error ? error->code : 0), static_cast(jErrorDomain), + static_cast(jErrorMessage))); + } catch (const std::exception& ex) { + Logging::logError("Cannot call onLoadFailed (%s)", ex.what()); + return FALSE; + } + } + + static void onWebProcessTerminated( + WKWebView* wkWebView, WebKitWebProcessTerminationReason reason, WebKitWebView* /*webView*/) noexcept + { + Logging::logDebug("WKWebView::onWebProcessTerminated(%d) [tid %d]", static_cast(reason), gettid()); + callJavaMethod(getJNIPageCache().m_onWebProcessTerminated, wkWebView->m_webViewJavaInstance.get(), + static_cast(reason)); + } + static void onEstimatedLoadProgress(WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept { Logging::logDebug("WKWebView::onEstimatedLoadProgress() [tid %d]", gettid()); @@ -179,6 +218,44 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static_cast(webkit_web_view_can_go_forward(webView))); } + static void onIsPlayingAudioChanged(WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onIsPlayingAudioChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onIsPlayingAudioChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_is_playing_audio(webView))); + } + + static void onIsMutedChanged(WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onIsMutedChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onIsMutedChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_get_is_muted(webView))); + } + + static void onCameraCaptureStateChanged( + WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onCameraCaptureStateChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onCameraCaptureStateChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_get_camera_capture_state(webView))); + } + + static void onMicrophoneCaptureStateChanged( + WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onMicrophoneCaptureStateChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onMicrophoneCaptureStateChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_get_microphone_capture_state(webView))); + } + + static void onDisplayCaptureStateChanged( + WKWebView* wkWebView, GParamSpec* /*pspec*/, WebKitWebView* webView) noexcept + { + Logging::logDebug("WKWebView::onDisplayCaptureStateChanged() [tid %d]", gettid()); + callJavaMethod(getJNIPageCache().m_onDisplayCaptureStateChanged, wkWebView->m_webViewJavaInstance.get(), + static_cast(webkit_web_view_get_display_capture_state(webView))); + } + static gboolean onScriptDialog(WKWebView* wkWebView, WebKitScriptDialog* dialog, WebKitWebView* webView) noexcept { auto dialogPtr = reinterpret_cast(webkit_script_dialog_ref(dialog)); @@ -197,6 +274,34 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static gboolean onDecidePolicy(WKWebView* wkWebView, WebKitPolicyDecision* decision, WebKitPolicyDecisionType decisionType, WebKitWebView* /*webView*/) noexcept { + // Handle navigation policy decisions + if (decisionType == WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) { + auto* navigationDecision = WEBKIT_NAVIGATION_POLICY_DECISION(decision); + auto* navigationAction = webkit_navigation_policy_decision_get_navigation_action(navigationDecision); + auto* uriRequest = webkit_navigation_action_get_request(navigationAction); + const char* uri = webkit_uri_request_get_uri(uriRequest); + + if (uri != nullptr) { + auto jUri = JNI::String(uri); + auto isRedirect + = static_cast(webkit_navigation_action_is_redirect(navigationAction) ? TRUE : FALSE); + auto isUserGesture + = static_cast(webkit_navigation_action_is_user_gesture(navigationAction) ? TRUE : FALSE); + + try { + if (getJNIPageCache().m_shouldOverrideUrlLoading.invoke(wkWebView->m_webViewJavaInstance.get(), + static_cast(jUri), isRedirect, isUserGesture)) { + webkit_policy_decision_ignore(decision); + return TRUE; + } + } catch (const std::exception& ex) { + Logging::logError("Cannot call shouldOverrideUrlLoading (%s)", ex.what()); + } + } + return FALSE; + } + + // Handle response policy decisions (HTTP errors) if (decisionType != WEBKIT_POLICY_DECISION_TYPE_RESPONSE) return FALSE; auto* responseDecision = WEBKIT_RESPONSE_POLICY_DECISION(decision); @@ -319,14 +424,14 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static bool onFullscreenRequest(WKWebView* wkWebView, bool fullscreen) noexcept { - if (wkWebView->m_viewBackend != nullptr) { + if (wkWebView->wpeView() != nullptr) { wkWebView->m_isFullscreenRequested = fullscreen; if (fullscreen) { callJavaMethod(getJNIPageCache().m_onEnterFullscreenMode, wkWebView->m_webViewJavaInstance.get()); + wpe_view_android_set_toplevel_state(wkWebView->wpeView(), WPE_TOPLEVEL_STATE_FULLSCREEN); } else { callJavaMethod(getJNIPageCache().m_onExitFullscreenMode, wkWebView->m_webViewJavaInstance.get()); - wpe_view_backend_dispatch_did_exit_fullscreen( - WPEAndroidViewBackend_getWPEViewBackend(wkWebView->m_viewBackend)); + wpe_view_android_set_toplevel_state(wkWebView->wpeView(), static_cast(0)); } } @@ -379,9 +484,18 @@ class JNIWKWebViewCache final : public JNI::TypedClass { // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members) const JNI::Method m_onClose; const JNI::Method m_onLoadChanged; + const JNI::Method m_shouldOverrideUrlLoading; + const JNI::Method m_onIsLoadingChanged; + const JNI::Method m_onLoadFailed; + const JNI::Method m_onWebProcessTerminated; const JNI::Method m_onEstimatedLoadProgress; const JNI::Method m_onUriChanged; const JNI::Method m_onTitleChanged; + const JNI::Method m_onIsPlayingAudioChanged; + const JNI::Method m_onIsMutedChanged; + const JNI::Method m_onCameraCaptureStateChanged; + const JNI::Method m_onMicrophoneCaptureStateChanged; + const JNI::Method m_onDisplayCaptureStateChanged; const JNI::Method m_onScriptDialog; const JNI::Method m_onInputMethodContextIn; const JNI::Method m_onInputMethodContextOut; @@ -408,10 +522,13 @@ class JNIWKWebViewCache final : public JNI::TypedClass { JNIEnv* env, jobject obj, jlong wkWebViewPtr, jint format, jint width, jint height) noexcept; static void nativeSurfaceRedrawNeeded(JNIEnv* env, jobject obj, jlong wkWebViewPtr) noexcept; static void nativeSetZoomLevel(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jdouble zoomLevel) noexcept; + static jboolean nativeIsMuted(JNIEnv* env, jobject obj, jlong wkWebViewPtr) noexcept; + static void nativeSetMuted(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jboolean muted) noexcept; static void nativeOnTouchEvent(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jlong time, jint type, jint pointerCount, jintArray ids, jfloatArray xs, jfloatArray ys) noexcept; static void nativeSetInputMethodContent(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jint unicodeChar) noexcept; - static void nativeDeleteInputMethodContent(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jint offset) noexcept; + static void nativeDeleteInputMethodContent( + JNIEnv* env, jobject obj, jlong wkWebViewPtr, jint offset, jint count) noexcept; static void nativeRequestExitFullscreenMode(JNIEnv* env, jobject obj, jlong wkWebViewPtr) noexcept; static void nativeEvaluateJavascript( JNIEnv* env, jobject obj, jlong wkWebViewPtr, jstring script, JNIWKCallback callback) noexcept; @@ -422,6 +539,11 @@ class JNIWKWebViewCache final : public JNI::TypedClass { static void nativeTriggerSslErrorHandler( JNIEnv* env, jclass klass, jlong handlerPtr, jboolean acceptCertificate) noexcept; + + static void nativeFocusIn(JNIEnv* env, jobject obj, jlong wkWebViewPtr) noexcept; + static void nativeFocusOut(JNIEnv* env, jobject obj, jlong wkWebViewPtr) noexcept; + static void nativeOnKeyEvent(JNIEnv* env, jobject obj, jlong wkWebViewPtr, jlong time, jint action, jint keyCode, + jint unicodeChar, jint modifiers) noexcept; }; const JNIWKWebViewCache& getJNIPageCache() @@ -434,9 +556,18 @@ JNIWKWebViewCache::JNIWKWebViewCache() : JNI::TypedClass(true) , m_onClose(getMethod("onClose")) , m_onLoadChanged(getMethod("onLoadChanged")) + , m_shouldOverrideUrlLoading(getMethod("shouldOverrideUrlLoading")) + , m_onIsLoadingChanged(getMethod("onIsLoadingChanged")) + , m_onLoadFailed(getMethod("onLoadFailed")) + , m_onWebProcessTerminated(getMethod("onWebProcessTerminated")) , m_onEstimatedLoadProgress(getMethod("onEstimatedLoadProgress")) , m_onUriChanged(getMethod("onUriChanged")) , m_onTitleChanged(getMethod("onTitleChanged")) + , m_onIsPlayingAudioChanged(getMethod("onIsPlayingAudioChanged")) + , m_onIsMutedChanged(getMethod("onIsMutedChanged")) + , m_onCameraCaptureStateChanged(getMethod("onCameraCaptureStateChanged")) + , m_onMicrophoneCaptureStateChanged(getMethod("onMicrophoneCaptureStateChanged")) + , m_onDisplayCaptureStateChanged(getMethod("onDisplayCaptureStateChanged")) , m_onScriptDialog(getMethod("onScriptDialog")) , m_onInputMethodContextIn(getMethod("onInputMethodContextIn")) , m_onInputMethodContextOut(getMethod("onInputMethodContextOut")) @@ -464,11 +595,13 @@ JNIWKWebViewCache::JNIWKWebViewCache() JNI::NativeMethod("nativeSurfaceRedrawNeeded", JNIWKWebViewCache::nativeSurfaceRedrawNeeded), JNI::NativeMethod("nativeSurfaceDestroyed", JNIWKWebViewCache::nativeSurfaceDestroyed), JNI::NativeMethod("nativeSetZoomLevel", JNIWKWebViewCache::nativeSetZoomLevel), + JNI::NativeMethod("nativeIsMuted", JNIWKWebViewCache::nativeIsMuted), + JNI::NativeMethod("nativeSetMuted", JNIWKWebViewCache::nativeSetMuted), JNI::NativeMethod( "nativeOnTouchEvent", JNIWKWebViewCache::nativeOnTouchEvent), JNI::NativeMethod( "nativeSetInputMethodContent", JNIWKWebViewCache::nativeSetInputMethodContent), - JNI::NativeMethod( + JNI::NativeMethod( "nativeDeleteInputMethodContent", JNIWKWebViewCache::nativeDeleteInputMethodContent), JNI::NativeMethod( "nativeRequestExitFullscreenMode", JNIWKWebViewCache::nativeRequestExitFullscreenMode), @@ -479,7 +612,11 @@ JNIWKWebViewCache::JNIWKWebViewCache() "nativeScriptDialogConfirm", JNIWKWebViewCache::nativeScriptDialogConfirm), JNI::NativeMethod("nativeSetTLSErrorsPolicy", JNIWKWebViewCache::nativeSetTLSErrorsPolicy), JNI::StaticNativeMethod( - "nativeTriggerSslErrorHandler", JNIWKWebViewCache::nativeTriggerSslErrorHandler)); + "nativeTriggerSslErrorHandler", JNIWKWebViewCache::nativeTriggerSslErrorHandler), + JNI::NativeMethod("nativeFocusIn", JNIWKWebViewCache::nativeFocusIn), + JNI::NativeMethod("nativeFocusOut", JNIWKWebViewCache::nativeFocusOut), + JNI::NativeMethod( + "nativeOnKeyEvent", JNIWKWebViewCache::nativeOnKeyEvent)); } jlong JNIWKWebViewCache::nativeInit( @@ -574,8 +711,15 @@ void JNIWKWebViewCache::nativeSurfaceCreated( { Logging::logDebug("WKWebView::nativeSurfaceCreated(%p) [tid %d]", surface, gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && wkWebView->m_renderer) - wkWebView->m_renderer->onSurfaceCreated(ANativeWindow_fromSurface(env, surface)); + if ((wkWebView != nullptr) && wkWebView->wpeView()) { + wpe_view_android_on_surface_created(wkWebView->wpeView(), ANativeWindow_fromSurface(env, surface)); + + wpe_view_set_visible(WPE_VIEW(wkWebView->wpeView()), TRUE); + wpe_view_map(WPE_VIEW(wkWebView->wpeView())); + WPEToplevelState const currentState = wpe_view_get_toplevel_state(WPE_VIEW(wkWebView->wpeView())); + wpe_view_android_set_toplevel_state( + wkWebView->wpeView(), static_cast(currentState | WPE_TOPLEVEL_STATE_ACTIVE)); + } } void JNIWKWebViewCache::nativeSurfaceChanged( @@ -583,15 +727,16 @@ void JNIWKWebViewCache::nativeSurfaceChanged( { Logging::logDebug("WKWebView::nativeSurfaceChanged(%d, %d, %d) [tid %d]", format, width, height, gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && (wkWebView->m_viewBackend != nullptr) && wkWebView->m_renderer) { + if ((wkWebView != nullptr) && wkWebView->wpeView()) { const uint32_t physicalWidth = std::max(0, width); const uint32_t physicalHeight = std::max(0, height); - const uint32_t logicalWidth = std::floor(static_cast(physicalWidth) / wkWebView->deviceScale()); - const uint32_t logicalHeight = std::floor(static_cast(physicalHeight) / wkWebView->deviceScale()); + wpe_view_android_on_surface_changed(wkWebView->wpeView(), format, physicalWidth, physicalHeight); - wpe_view_backend_dispatch_set_size( - WPEAndroidViewBackend_getWPEViewBackend(wkWebView->m_viewBackend), logicalWidth, logicalHeight); - wkWebView->m_renderer->onSurfaceChanged(format, physicalWidth, physicalHeight); + const float scale = safeDeviceDensity(wkWebView->deviceScale()); + const uint32_t logicalWidth = static_cast(std::round(physicalWidth / scale)); + const uint32_t logicalHeight = static_cast(std::round(physicalHeight / scale)); + wpe_view_android_resize(wkWebView->wpeView(), logicalWidth, logicalHeight); + wpe_view_android_set_scale(wkWebView->wpeView(), scale); } } @@ -599,16 +744,22 @@ void JNIWKWebViewCache::nativeSurfaceRedrawNeeded(JNIEnv* /*env*/, jobject /*obj { Logging::logDebug("WKWebView::nativeSurfaceRedrawNeeded() [tid %d]", gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && wkWebView->m_renderer) - wkWebView->m_renderer->onSurfaceRedrawNeeded(); + if ((wkWebView != nullptr) && wkWebView->wpeView()) + wpe_view_android_on_surface_redraw_needed(wkWebView->wpeView()); } void JNIWKWebViewCache::nativeSurfaceDestroyed(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr) noexcept { Logging::logDebug("WKWebView::nativeSurfaceDestroyed() [tid %d]", gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && wkWebView->m_renderer) - wkWebView->m_renderer->onSurfaceDestroyed(); + if ((wkWebView != nullptr) && wkWebView->wpeView()) { + wpe_view_set_visible(WPE_VIEW(wkWebView->wpeView()), FALSE); + wpe_view_unmap(WPE_VIEW(wkWebView->wpeView())); + WPEToplevelState const currentState = wpe_view_get_toplevel_state(WPE_VIEW(wkWebView->wpeView())); + wpe_view_android_set_toplevel_state( + wkWebView->wpeView(), static_cast(currentState & ~WPE_TOPLEVEL_STATE_ACTIVE)); + wpe_view_android_on_surface_destroyed(wkWebView->wpeView()); + } } void JNIWKWebViewCache::nativeSetZoomLevel( @@ -620,58 +771,62 @@ void JNIWKWebViewCache::nativeSetZoomLevel( webkit_web_view_set_zoom_level(wkWebView->m_webView, zoomLevel); } +jboolean JNIWKWebViewCache::nativeIsMuted(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr) noexcept +{ + auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) + if ((wkWebView != nullptr) && (wkWebView->m_webView != nullptr)) + return webkit_web_view_get_is_muted(wkWebView->m_webView); + return JNI_FALSE; +} + +void JNIWKWebViewCache::nativeSetMuted(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr, jboolean muted) noexcept +{ + Logging::logDebug("WKWebView::nativeSetMuted(%d) [tid %d]", muted, gettid()); + auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) + if ((wkWebView != nullptr) && (wkWebView->m_webView != nullptr)) + webkit_web_view_set_is_muted(wkWebView->m_webView, muted); +} + void JNIWKWebViewCache::nativeOnTouchEvent(JNIEnv* env, jobject /*obj*/, jlong wkWebViewPtr, jlong time, jint type, jint pointerCount, jintArray ids, jfloatArray xs, jfloatArray ys) noexcept { - auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && (wkWebView->m_viewBackend != nullptr)) { - wpe_input_touch_event_type touchEventType = wpe_input_touch_event_type_null; - switch (type) { - case 0: - touchEventType = wpe_input_touch_event_type_down; - break; - - case 1: - touchEventType = wpe_input_touch_event_type_motion; - break; - - case 2: - touchEventType = wpe_input_touch_event_type_up; - break; - - default: - break; - } + auto* wkWebView = reinterpret_cast(wkWebViewPtr); + if (!wkWebView || !wkWebView->wpeView()) + return; + + WPEEventType eventType; + switch (type) { + case 0: + eventType = WPE_EVENT_TOUCH_DOWN; + break; + case 1: + eventType = WPE_EVENT_TOUCH_MOVE; + break; + case 2: + eventType = WPE_EVENT_TOUCH_UP; + break; + default: + return; + } - std::vector idsVector(pointerCount); - std::vector xsVector(pointerCount); - std::vector ysVector(pointerCount); - env->GetIntArrayRegion(ids, 0, pointerCount, idsVector.data()); - env->GetFloatArrayRegion(xs, 0, pointerCount, xsVector.data()); - env->GetFloatArrayRegion(ys, 0, pointerCount, ysVector.data()); - - auto* touchPoints = new wpe_input_touch_event_raw[pointerCount]; - for (int i = 0; i < pointerCount; ++i) { - touchPoints[i].type = touchEventType; - touchPoints[i].time = static_cast(time); - touchPoints[i].id = idsVector[i]; - touchPoints[i].x = static_cast(std::round(xsVector[i])); - touchPoints[i].y = static_cast(std::round(ysVector[i])); - } + std::vector idsVector(pointerCount); + std::vector xsVector(pointerCount); + std::vector ysVector(pointerCount); + env->GetIntArrayRegion(ids, 0, pointerCount, idsVector.data()); + env->GetFloatArrayRegion(xs, 0, pointerCount, xsVector.data()); + env->GetFloatArrayRegion(ys, 0, pointerCount, ysVector.data()); - wpe_input_touch_event touchEvent { - .touchpoints = touchPoints, - .touchpoints_length = static_cast(pointerCount), - .type = touchEventType, - .id = touchPoints[0].id, // Use the first touchpoint's ID - .time = static_cast(time), - .modifiers = 0, // Set modifiers if any - }; + const float scale = safeDeviceDensity(wkWebView->deviceScale()); - wpe_view_backend_dispatch_touch_event( - WPEAndroidViewBackend_getWPEViewBackend(wkWebView->m_viewBackend), &touchEvent); + for (int i = 0; i < pointerCount; ++i) { + const double logicalX = static_cast(xsVector[i]) / scale; + const double logicalY = static_cast(ysVector[i]) / scale; - delete[] touchPoints; + auto* event = wpe_event_touch_new(eventType, WPE_VIEW(wkWebView->wpeView()), WPE_INPUT_SOURCE_TOUCHSCREEN, + static_cast(time), static_cast(0), static_cast(idsVector[i]), logicalX, + logicalY); + wpe_view_android_dispatch_event(wkWebView->wpeView(), event); + wpe_event_unref(event); } } @@ -680,30 +835,46 @@ void JNIWKWebViewCache::nativeSetInputMethodContent( { Logging::logDebug("WKWebView::nativeSetInputMethodContent(0x%08X) [tid %d]", unicodeChar, gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && wkWebView->m_renderer) { - static constexpr size_t GUNICHAR_UTF8_BUFFER_SIZE = 8; - char utf8Content[GUNICHAR_UTF8_BUFFER_SIZE] = {}; - g_unichar_to_utf8(static_cast(unicodeChar), utf8Content); - wkWebView->m_inputMethodContext.setContent(utf8Content); + if (wkWebView == nullptr) { + Logging::logError("WKWebView::nativeSetInputMethodContent - wkWebView is null"); + return; + } + if (wkWebView->wpeView() == nullptr) { + Logging::logError("WKWebView::nativeSetInputMethodContent - wpeView is null"); + return; + } + auto* context = wpe_input_method_context_android_get_for_view(WPE_VIEW(wkWebView->wpeView())); + if (context == nullptr) { + Logging::logError( + "WKWebView::nativeSetInputMethodContent - context is null for view %p", WPE_VIEW(wkWebView->wpeView())); + return; } + static constexpr size_t GUNICHAR_UTF8_BUFFER_SIZE = 8; + char utf8Content[GUNICHAR_UTF8_BUFFER_SIZE] = {}; + g_unichar_to_utf8(static_cast(unicodeChar), utf8Content); + wpe_input_method_context_android_commit_text(context, utf8Content); } void JNIWKWebViewCache::nativeDeleteInputMethodContent( - JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr, jint offset) noexcept + JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr, jint offset, jint count) noexcept { - Logging::logDebug("WKWebView::nativeDeleteInputMethodContent(%d) [tid %d]", offset, gettid()); + Logging::logDebug("WKWebView::nativeDeleteInputMethodContent(%d, %d) [tid %d]", offset, count, gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && wkWebView->m_renderer) - wkWebView->m_inputMethodContext.deleteContent(offset); + if (wkWebView != nullptr && wkWebView->wpeView() != nullptr) { + auto* context = wpe_input_method_context_android_get_for_view(WPE_VIEW(wkWebView->wpeView())); + if (context != nullptr) + wpe_input_method_context_android_delete_surrounding(context, offset, static_cast(count)); + } } void JNIWKWebViewCache::nativeRequestExitFullscreenMode(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr) noexcept { Logging::logDebug("WKWebView::nativeRequestExitFullscreenMode() [tid %d]", gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) - if ((wkWebView != nullptr) && (wkWebView->m_viewBackend != nullptr)) { - wpe_view_backend_dispatch_request_exit_fullscreen( - WPEAndroidViewBackend_getWPEViewBackend(wkWebView->m_viewBackend)); + if (wkWebView != nullptr && wkWebView->wpeView() != nullptr) { + auto* toplevel = wpe_view_get_toplevel(WPE_VIEW(wkWebView->wpeView())); + if (toplevel != nullptr) + wpe_toplevel_unfullscreen(toplevel); } } @@ -771,6 +942,29 @@ void JNIWKWebViewCache::nativeTriggerSslErrorHandler( } } +void JNIWKWebViewCache::nativeFocusIn(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr) noexcept +{ + Logging::logDebug("WKWebView::nativeFocusIn() [tid %d]", gettid()); + auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) + if ((wkWebView != nullptr) && wkWebView->wpeView()) + wpe_view_focus_in(WPE_VIEW(wkWebView->wpeView())); +} + +void JNIWKWebViewCache::nativeFocusOut(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr) noexcept +{ + Logging::logDebug("WKWebView::nativeFocusOut() [tid %d]", gettid()); + auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) + if ((wkWebView != nullptr) && wkWebView->wpeView()) + wpe_view_focus_out(WPE_VIEW(wkWebView->wpeView())); +} + +void JNIWKWebViewCache::nativeOnKeyEvent(JNIEnv* /*env*/, jobject /*obj*/, jlong /*wkWebViewPtr*/, jlong /*time*/, + jint /*action*/, jint /*keyCode*/, jint /*unicodeChar*/, jint /*modifiers*/) noexcept +{ + // Keyboard event handling will be implemented in a subsequent commit + Logging::logDebug("WKWebView::nativeOnKeyEvent() - not yet implemented [tid %d]", gettid()); +} + /*********************************************************************************************************************** * Native WKWebView class implementation **********************************************************************************************************************/ @@ -780,32 +974,76 @@ void WKWebView::configureJNIMappings() { getJNIPageCache(); } WKWebView::WKWebView(JNIEnv* env, JNIWKWebView jniWKWebView, WKWebContext* wkWebContext, int width, int height, float deviceScale, bool headless) : m_webViewJavaInstance(JNI::createTypedProtectedRef(env, jniWKWebView, true)) - , m_inputMethodContext(this) , m_isHeadless(headless) , m_deviceScale(deviceScale) { - const uint32_t uWidth = std::max(0, width); - const uint32_t uHeight = std::max(0, height); + const uint32_t physicalWidth = std::max(0, width); + const uint32_t physicalHeight = std::max(0, height); - m_viewBackend = WPEAndroidViewBackend_create(uWidth, uHeight); + const float scale = safeDeviceDensity(deviceScale); + const uint32_t logicalWidth = static_cast(std::round(physicalWidth / scale)); + const uint32_t logicalHeight = static_cast(std::round(physicalHeight / scale)); - if (!m_isHeadless) - m_renderer = std::make_shared(m_viewBackend, uWidth, uHeight); + Logging::logDebug("WKWebView: physical=%ux%u, logical=%ux%u, scale=%.2f", physicalWidth, physicalHeight, + logicalWidth, logicalHeight, scale); - WPEViewBackend* wpeBackend = WPEAndroidViewBackend_getWPEViewBackend(m_viewBackend); - WebKitWebViewBackend* viewBackend = webkit_web_view_backend_new( - wpeBackend, reinterpret_cast(WPEAndroidViewBackend_destroy), m_viewBackend); + // Create WPE Platform display for the UI process + m_wpeDisplay = wpe_display_get_primary(); + if (!m_wpeDisplay) { + m_wpeDisplay = wpe_display_android_new(); + wpe_display_set_primary(m_wpeDisplay); + + GError* error = nullptr; + if (!wpe_display_connect(m_wpeDisplay, &error)) { + Logging::logError("Failed to connect WPE display: %s", error ? error->message : "unknown error"); + g_clear_error(&error); + } + } else { + // Take ownership of borrowed reference from wpe_display_get_primary() + g_object_ref(m_wpeDisplay); + } + + // Create renderer if not headless + if (!m_isHeadless) + m_renderer = std::make_shared(physicalWidth, physicalHeight); gboolean const automationMode = wkWebContext->automationMode() ? TRUE : FALSE; - m_webView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "backend", viewBackend, "web-context", + // Create WebKitWebView with WPE Platform + m_webView = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "display", m_wpeDisplay, "web-context", wkWebContext->webContext(), "is-controlled-by-automation", automationMode, nullptr)); - webkit_web_view_set_input_method_context(m_webView, m_inputMethodContext.webKitInputMethodContext()); + + // Get the WPEViewAndroid that WebKit created + m_wpeView = WPE_VIEW_ANDROID(webkit_web_view_get_wpe_view(m_webView)); + if (m_wpeView == nullptr) { + Logging::logError("Failed to get WPEViewAndroid from WebKitWebView!"); + } + + // Set up the renderer on the WPE view + if (m_wpeView && m_renderer) { + wpe_view_android_set_renderer(m_wpeView, m_renderer); + } + + // Set initial size and scale + if (m_wpeView) { + wpe_view_android_resize(m_wpeView, logicalWidth, logicalHeight); + wpe_view_android_set_scale(m_wpeView, scale); + + // Set up focus callbacks + wpe_input_method_context_android_set_focus_callbacks_for_view( + WPE_VIEW(m_wpeView), onInputMethodFocusIn, onInputMethodFocusOut, this); + } m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "close", G_CALLBACK(JNIWKWebViewCache::onClose), this)); m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "load-changed", G_CALLBACK(JNIWKWebViewCache::onLoadChanged), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "notify::is-loading", G_CALLBACK(JNIWKWebViewCache::onIsLoadingChanged), this)); + m_signalHandlers.push_back( + g_signal_connect_swapped(m_webView, "load-failed", G_CALLBACK(JNIWKWebViewCache::onLoadFailed), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "web-process-terminated", G_CALLBACK(JNIWKWebViewCache::onWebProcessTerminated), this)); m_signalHandlers.push_back(g_signal_connect_swapped( m_webView, "notify::estimated-load-progress", G_CALLBACK(JNIWKWebViewCache::onEstimatedLoadProgress), this)); m_signalHandlers.push_back( @@ -818,14 +1056,18 @@ WKWebView::WKWebView(JNIEnv* env, JNIWKWebView jniWKWebView, WKWebContext* wkWeb g_signal_connect_swapped(m_webView, "decide-policy", G_CALLBACK(JNIWKWebViewCache::onDecidePolicy), this)); m_signalHandlers.push_back(g_signal_connect_swapped( m_webView, "load-failed-with-tls-errors", G_CALLBACK(JNIWKWebViewCache::onReceivedSslError), this)); - - wpe_view_backend_set_fullscreen_handler(wpeBackend, - reinterpret_cast(JNIWKWebViewCache::onFullscreenRequest), this); - - WPEAndroidViewBackend_setCommitBufferHandler(m_viewBackend, this, handleCommitBuffer); - - wpe_view_backend_dispatch_set_device_scale_factor( - WPEAndroidViewBackend_getWPEViewBackend(m_viewBackend), deviceScale); + m_signalHandlers.push_back(g_signal_connect_swapped(m_webView, "enter-fullscreen", + G_CALLBACK(+[](WKWebView* wkWebView, WebKitWebView*) -> gboolean { + JNIWKWebViewCache::onFullscreenRequest(wkWebView, true); + return TRUE; + }), + this)); + m_signalHandlers.push_back(g_signal_connect_swapped(m_webView, "leave-fullscreen", + G_CALLBACK(+[](WKWebView* wkWebView, WebKitWebView*) -> gboolean { + JNIWKWebViewCache::onFullscreenRequest(wkWebView, false); + return TRUE; + }), + this)); } void WKWebView::close() noexcept @@ -834,7 +1076,10 @@ void WKWebView::close() noexcept // Ensure that renderer is destroyed first so that all pending commits will be cleared before page is gone m_renderer.reset(); - webkit_web_view_set_input_method_context(m_webView, nullptr); + if (m_wpeView != nullptr) { + wpe_input_method_context_android_set_focus_callbacks_for_view( + WPE_VIEW(m_wpeView), nullptr, nullptr, nullptr); + } for (auto& handler : m_signalHandlers) g_signal_handler_disconnect(m_webView, handler); @@ -842,10 +1087,15 @@ void WKWebView::close() noexcept webkit_web_view_try_close(m_webView); - m_viewBackend = nullptr; + m_wpeView = nullptr; g_object_unref(m_webView); m_webView = nullptr; } + + if (m_wpeDisplay != nullptr) { + g_object_unref(m_wpeDisplay); + m_wpeDisplay = nullptr; + } } void WKWebView::onInputMethodContextIn() noexcept @@ -857,25 +1107,3 @@ void WKWebView::onInputMethodContextOut() noexcept { getJNIPageCache().onInputMethodContextOut(m_webViewJavaInstance.get()); } - -void WKWebView::commitBuffer(WPEAndroidBuffer* buffer, int fenceFD) noexcept // NOLINT(bugprone-exception-escape) -{ - auto scopedFenceFD = std::make_shared(fenceFD); - if (m_viewBackend != nullptr) { - if (m_isHeadless) { - WPEAndroidViewBackend_dispatchReleaseBuffer(m_viewBackend, buffer); - WPEAndroidViewBackend_dispatchFrameComplete(m_viewBackend); - } else if (m_renderer) { - auto scopedBuffer = std::make_shared(buffer); - - if (m_isFullscreenRequested && (scopedBuffer->width() == m_renderer->width()) - && (scopedBuffer->height() == m_renderer->height())) { - Logging::logDebug("Fullscreen ready"); - m_isFullscreenRequested = false; - wpe_view_backend_dispatch_did_enter_fullscreen(WPEAndroidViewBackend_getWPEViewBackend(m_viewBackend)); - } - - m_renderer->commitBuffer(scopedBuffer, scopedFenceFD); - } - } -} diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.h b/wpeview/src/main/cpp/Runtime/WKWebView.h index db1cfbf57..3d0cddcf3 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.h +++ b/wpeview/src/main/cpp/Runtime/WKWebView.h @@ -22,18 +22,21 @@ #pragma once -#include "InputMethodContext.h" #include "JNI/JNI.h" #include "Renderer.h" +#include #include DECLARE_JNI_CLASS_SIGNATURE(JNIWKWebView, "org/wpewebkit/wpe/WKWebView"); -struct WPEAndroidViewBackend; +using WPEDisplay = struct _WPEDisplay; +using WPEViewAndroid = struct _WPEViewAndroid; +using WebKitWebView = struct _WebKitWebView; class WKWebContext; +class RendererSurfaceControl; -class WKWebView final : public InputMethodContextObserver { +class WKWebView final { public: static void configureJNIMappings(); @@ -42,17 +45,16 @@ class WKWebView final : public InputMethodContextObserver { WKWebView(const WKWebView&) = delete; WKWebView& operator=(const WKWebView&) = delete; - ~WKWebView() override { close(); } + ~WKWebView() { close(); } void close() noexcept; float deviceScale() const noexcept { return m_deviceScale; } WebKitWebView* webView() const noexcept { return m_webView; } + WPEViewAndroid* wpeView() const noexcept { return m_wpeView; } - void onInputMethodContextIn() noexcept override; - void onInputMethodContextOut() noexcept override; - - void commitBuffer(WPEAndroidBuffer* buffer, int fenceFD) noexcept; // NOLINT(bugprone-exception-escape) + void onInputMethodContextIn() noexcept; + void onInputMethodContextOut() noexcept; private: friend class JNIWKWebViewCache; @@ -61,10 +63,10 @@ class WKWebView final : public InputMethodContextObserver { float deviceScale, bool headless); JNI::ProtectedType m_webViewJavaInstance; - InputMethodContext m_inputMethodContext; - std::shared_ptr m_renderer; + std::shared_ptr m_renderer; - WPEAndroidViewBackend* m_viewBackend = nullptr; + WPEDisplay* m_wpeDisplay = nullptr; + WPEViewAndroid* m_wpeView = nullptr; WebKitWebView* m_webView = nullptr; std::vector m_signalHandlers; bool m_isFullscreenRequested = false; diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp new file mode 100644 index 000000000..5ee4e3b9b --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp @@ -0,0 +1,217 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "WPEViewAndroid.h" + +#include "Logging.h" +#include "RendererSurfaceControl.h" +#include "ScopedFD.h" +#include "WPEDisplayAndroid.h" + +#include + +struct _WPEViewAndroid { + WPEView parent; +}; + +typedef struct { + std::shared_ptr renderer; +} WPEViewAndroidPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE(WPEViewAndroid, wpe_view_android, WPE_TYPE_VIEW) + +static void wpeViewAndroidConstructed(GObject* object) +{ + G_OBJECT_CLASS(wpe_view_android_parent_class)->constructed(object); + + Logging::logDebug("WPEViewAndroid::constructed(%p)", object); +} + +static void wpeViewAndroidDispose(GObject* object) +{ + Logging::logDebug("WPEViewAndroid::dispose(%p)", object); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(object))); + + priv->renderer.reset(); + + G_OBJECT_CLASS(wpe_view_android_parent_class)->dispose(object); +} + +static gboolean wpeViewAndroidRenderBuffer( + WPEView* view, WPEBuffer* buffer, const WPERectangle* /*damageRects*/, guint nDamageRects, GError** error) +{ + Logging::logDebug("WPEViewAndroid::render_buffer(%p, %p, %u rects)", view, buffer, nDamageRects); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + if (!WPE_IS_BUFFER_ANDROID(buffer)) { + g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Buffer is not a WPEBufferAndroid"); + return FALSE; + } + + auto* bufferAndroid = WPE_BUFFER_ANDROID(buffer); + AHardwareBuffer* ahb = wpe_buffer_android_get_hardware_buffer(bufferAndroid); + if (ahb == nullptr) { + g_set_error_literal( + error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Failed to get AHardwareBuffer from WPEBufferAndroid"); + return FALSE; + } + + if (priv->renderer) { + // Commit buffer to SurfaceControl for display with GPU rendering fence. + // Buffer ownership and lifecycle is managed by RendererSurfaceControl. + int renderingFence = wpe_buffer_take_rendering_fence(buffer); + auto fenceFD = std::make_shared(renderingFence); + priv->renderer->commitBuffer(ahb, bufferAndroid, fenceFD); + } else { + // Headless rendering: signal that the buffer is available for reuse. + wpe_view_buffer_released(view, buffer); + wpe_view_buffer_rendered(view, buffer); + } + + Logging::logDebug("WPEViewAndroid: buffer committed successfully"); + + return TRUE; +} + +static void wpe_view_android_class_init(WPEViewAndroidClass* klass) +{ + GObjectClass* objectClass = G_OBJECT_CLASS(klass); + WPEViewClass* viewClass = WPE_VIEW_CLASS(klass); + + objectClass->constructed = wpeViewAndroidConstructed; + objectClass->dispose = wpeViewAndroidDispose; + viewClass->render_buffer = wpeViewAndroidRenderBuffer; +} + +static void wpe_view_android_init(WPEViewAndroid* view) { Logging::logDebug("WPEViewAndroid::init(%p)", view); } + +WPEView* wpe_view_android_new(WPEDisplay* display) +{ + return WPE_VIEW(g_object_new(WPE_TYPE_VIEW_ANDROID, "display", display, nullptr)); +} + +void wpe_view_android_resize(WPEViewAndroid* view, int width, int height) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::resize(%p, %d, %d)", view, width, height); + wpe_view_resized(WPE_VIEW(view), width, height); +} + +void wpe_view_android_dispatch_event(WPEViewAndroid* view, WPEEvent* event) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + g_return_if_fail(event != nullptr); + + Logging::logDebug("WPEViewAndroid::dispatch_event(%p, %p)", view, event); + wpe_view_event(WPE_VIEW(view), event); +} + +void wpe_view_android_set_scale(WPEViewAndroid* view, double scale) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::set_scale(%p, %f)", view, scale); + + auto* toplevel = wpe_view_get_toplevel(WPE_VIEW(view)); + if (toplevel != nullptr) { + wpe_toplevel_scale_changed(toplevel, scale); + } +} + +void wpe_view_android_set_toplevel_state(WPEViewAndroid* view, WPEToplevelState state) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::set_toplevel_state(%p, %u)", view, static_cast(state)); + + auto* toplevel = wpe_view_get_toplevel(WPE_VIEW(view)); + if (toplevel != nullptr) { + wpe_toplevel_state_changed(toplevel, state); + } +} + +void wpe_view_android_set_renderer(WPEViewAndroid* view, const std::shared_ptr& renderer) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::set_renderer(%p, %p)", view, renderer.get()); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + priv->renderer = renderer; + + if (renderer) { + // Give renderer direct access to WPEView for buffer lifecycle callbacks + renderer->setWPEView(WPE_VIEW(view)); + } +} + +void wpe_view_android_on_surface_created(WPEViewAndroid* view, ANativeWindow* window) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::on_surface_created(%p, %p)", view, window); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + if (priv->renderer) { + priv->renderer->onSurfaceCreated(window); + } +} + +void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint32_t width, uint32_t height) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::on_surface_changed(%p, %d, %u, %u)", view, format, width, height); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + if (priv->renderer) { + priv->renderer->onSurfaceChanged(format, width, height); + } +} + +void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::on_surface_redraw_needed(%p)", view); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + if (priv->renderer) { + priv->renderer->onSurfaceRedrawNeeded(); + } +} + +void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + Logging::logDebug("WPEViewAndroid::on_surface_destroyed(%p)", view); + + auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + + if (priv->renderer) { + priv->renderer->onSurfaceDestroyed(); + } +} diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h new file mode 100644 index 000000000..ebb4afed7 --- /dev/null +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define WPE_TYPE_VIEW_ANDROID (wpe_view_android_get_type()) +G_DECLARE_FINAL_TYPE(WPEViewAndroid, wpe_view_android, WPE, VIEW_ANDROID, WPEView) + +G_END_DECLS + +#include + +struct ANativeWindow; +class RendererSurfaceControl; + +WPEView* wpe_view_android_new(WPEDisplay* display); +void wpe_view_android_resize(WPEViewAndroid* view, int width, int height); +void wpe_view_android_dispatch_event(WPEViewAndroid* view, WPEEvent* event); +void wpe_view_android_set_scale(WPEViewAndroid* view, double scale); +void wpe_view_android_set_toplevel_state(WPEViewAndroid* view, WPEToplevelState state); +void wpe_view_android_set_renderer(WPEViewAndroid* view, const std::shared_ptr& renderer); +void wpe_view_android_on_surface_created(WPEViewAndroid* view, struct ANativeWindow* window); +void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint32_t width, uint32_t height); +void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view); +void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view); diff --git a/wpeview/src/main/cpp/Service/EntryPoint.cpp b/wpeview/src/main/cpp/Service/EntryPoint.cpp index 458cc62bb..496d903cf 100644 --- a/wpeview/src/main/cpp/Service/EntryPoint.cpp +++ b/wpeview/src/main/cpp/Service/EntryPoint.cpp @@ -20,6 +20,9 @@ #include "Environment.h" #include "Init.h" #include "Logging.h" +#include "WPEDisplayAndroid.h" + +#include #include #include @@ -91,6 +94,27 @@ void initializeNativeMain(JNIEnv* /*env*/, jclass /*klass*/, jlong pid, jint typ argv[3] = arg3String; } + // Initialize WPE Platform Display for WebProcess and NetworkProcess + if (processType == ProcessType::WebProcess || processType == ProcessType::NetworkProcess) { + Logging::logDebug("Initializing WPE Display for %s", processName[static_cast(processType)]); + + auto* display = wpe_display_get_primary(); + if (!display) { + display = wpe_display_android_new(); + wpe_display_set_primary(display); + + GError* error = nullptr; + if (!wpe_display_connect(display, &error)) { + Logging::logError("Failed to connect WPE display: %s", error ? error->message : "unknown error"); + g_clear_error(&error); + } else { + Logging::logDebug("WPE Display initialized successfully"); + } + } else { + Logging::logDebug("WPE Display already initialized"); + } + } + (*entrypoint)(numArgs, argv); delete[] argv; } diff --git a/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java b/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java index 14ff6830b..72e84efa7 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java +++ b/wpeview/src/main/java/org/wpewebkit/wpe/WKWebView.java @@ -33,6 +33,7 @@ import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; +import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ScaleGestureDetector; @@ -92,6 +93,14 @@ public final class WKWebView { public static final int WEBKIT_TLS_ERRORS_POLICY_IGNORE = 0; public static final int WEBKIT_TLS_ERRORS_POLICY_FAIL = 1; + public static final int WEBKIT_WEB_PROCESS_CRASHED = 0; + public static final int WEBKIT_WEB_PROCESS_EXCEEDED_MEMORY_LIMIT = 1; + public static final int WEBKIT_WEB_PROCESS_TERMINATED_BY_API = 2; + + public static final int WEBKIT_MEDIA_CAPTURE_STATE_NONE = 0; + public static final int WEBKIT_MEDIA_CAPTURE_STATE_ACTIVE = 1; + public static final int WEBKIT_MEDIA_CAPTURE_STATE_MUTED = 2; + protected long nativePtr = 0; public long getNativePtr() { return nativePtr; } @@ -113,6 +122,7 @@ public final class WKWebView { private boolean canGoBack = true; private boolean canGoForward = true; protected boolean ignoreTouchEvents = false; + private boolean isInputFieldFocused = false; private static int kHeadlessWidth = 1080; private static int kHeadlessHeight = 2274; @@ -235,9 +245,22 @@ public void loadHtml(@NonNull String content, @Nullable String baseUri) { public void reload() { nativeReload(nativePtr); } + public boolean isMuted() { return nativeIsMuted(nativePtr); } + + public void setMuted(boolean muted) { nativeSetMuted(nativePtr, muted); } + public void setInputMethodContent(int unicodeChar) { nativeSetInputMethodContent(nativePtr, unicodeChar); } - public void deleteInputMethodContent(int offset) { nativeDeleteInputMethodContent(nativePtr, offset); } + public void deleteInputMethodContent(int offset, int count) { + nativeDeleteInputMethodContent(nativePtr, offset, count); + } + + public void onKeyEvent(@NonNull KeyEvent event) { + nativeOnKeyEvent(nativePtr, event.getEventTime(), event.getAction(), event.getKeyCode(), event.getUnicodeChar(), + event.getMetaState()); + } + + public boolean isInputFieldFocused() { return isInputFieldFocused; } public void evaluateJavascript(@NonNull String script, @Nullable WKCallback callback) { nativeEvaluateJavascript(nativePtr, script, callback); @@ -298,7 +321,20 @@ public boolean onScale(ScaleGestureDetector detector) { } private final class PageSurfaceView extends SurfaceView { - public PageSurfaceView(Context context) { super(context); } + public PageSurfaceView(Context context) { + super(context); + setFocusable(true); + setFocusableInTouchMode(true); + setOnFocusChangeListener((view, hasFocus) -> { + if (nativePtr != 0) { + if (hasFocus) { + nativeFocusIn(nativePtr); + } else { + nativeFocusOut(nativePtr); + } + } + }); + } @Override @SuppressLint("ClickableViewAccessibility") @@ -468,6 +504,33 @@ private void onLoadChanged(int loadEvent) { } } + @Keep + private boolean shouldOverrideUrlLoading(@NonNull String url, boolean isRedirect, boolean isUserGesture) { + if (wpeViewClient != null) + return wpeViewClient.shouldOverrideUrlLoading(wpeView, url, isRedirect, isUserGesture); + return false; + } + + @Keep + private void onIsLoadingChanged(boolean isLoading) { + if (wpeViewClient != null) + wpeViewClient.onLoadingStateChanged(wpeView, isLoading); + } + + @Keep + private boolean onLoadFailed(int loadEvent, @NonNull String failingUri, int errorCode, @NonNull String errorDomain, + @NonNull String errorMessage) { + if (wpeViewClient != null) + return wpeViewClient.onLoadFailed(wpeView, failingUri, errorCode, errorDomain, errorMessage); + return false; + } + + @Keep + private void onWebProcessTerminated(int reason) { + if (wpeViewClient != null) + wpeViewClient.onRenderProcessGone(wpeView, reason); + } + @Keep private void onClose() { if (wpeChromeClient != null) @@ -483,6 +546,8 @@ private void onEstimatedLoadProgress(double progress) { @Keep private void onUriChanged(@NonNull String uri) { this.uri = uri; + if (wpeChromeClient != null) + wpeChromeClient.onUriChanged(wpeView, uri); } @Keep @@ -494,6 +559,36 @@ private void onTitleChanged(@NonNull String title, boolean canGoBack, boolean ca wpeChromeClient.onReceivedTitle(wpeView, title); } + @Keep + private void onIsPlayingAudioChanged(boolean isPlayingAudio) { + if (wpeChromeClient != null) + wpeChromeClient.onAudioStateChanged(wpeView, isPlayingAudio); + } + + @Keep + private void onIsMutedChanged(boolean isMuted) { + if (wpeChromeClient != null) + wpeChromeClient.onMutedStateChanged(wpeView, isMuted); + } + + @Keep + private void onCameraCaptureStateChanged(int state) { + if (wpeChromeClient != null) + wpeChromeClient.onCameraCaptureStateChanged(wpeView, state); + } + + @Keep + private void onMicrophoneCaptureStateChanged(int state) { + if (wpeChromeClient != null) + wpeChromeClient.onMicrophoneCaptureStateChanged(wpeView, state); + } + + @Keep + private void onDisplayCaptureStateChanged(int state) { + if (wpeChromeClient != null) + wpeChromeClient.onDisplayCaptureStateChanged(wpeView, state); + } + @SuppressLint("StringFormatInvalid") @Keep private boolean onScriptDialog(long nativeDialogPtr, int dialogType, @NonNull String url, @NonNull String message, @@ -559,6 +654,7 @@ private boolean onScriptDialog(long nativeDialogPtr, int dialogType, @NonNull St @Keep private void onInputMethodContextIn() { + isInputFieldFocused = true; WeakReference weakRefecence = new WeakReference<>(wpeView); Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> { @@ -575,6 +671,7 @@ private void onInputMethodContextIn() { @Keep private void onInputMethodContextOut() { + isInputFieldFocused = false; WeakReference weakRefecence = new WeakReference<>(wpeView); Handler handler = new Handler(Looper.getMainLooper()); handler.post(() -> { @@ -672,6 +769,9 @@ private boolean onReceivedSslError(@NonNull String failingURI, @NonNull String c @Keep private void onEnterFullscreenMode() { Log.d(LOGTAG, "onEnterFullscreenMode()"); + if (wpeChromeClient != null) { + wpeChromeClient.onFullscreenModeChanged(wpeView, true); + } if ((surfaceView != null) && (wpeChromeClient != null)) { wpeView.removeView(surfaceView); @@ -690,6 +790,9 @@ private void onEnterFullscreenMode() { @Keep private void onExitFullscreenMode() { Log.d(LOGTAG, "onExitFullscreenMode()"); + if (wpeChromeClient != null) { + wpeChromeClient.onFullscreenModeChanged(wpeView, false); + } if ((customView != null) && (surfaceView != null) && (wpeChromeClient != null)) { customView.removeView(surfaceView); wpeView.addView(surfaceView); @@ -734,10 +837,12 @@ private void onReceivedHttpError( // WPEResourceRequest private native void nativeSurfaceRedrawNeeded(long nativePtr); private native void nativeSurfaceDestroyed(long nativePtr); private native void nativeSetZoomLevel(long nativePtr, double zoomLevel); + private native boolean nativeIsMuted(long nativePtr); + private native void nativeSetMuted(long nativePtr, boolean muted); private native void nativeOnTouchEvent(long nativePtr, long time, int type, int pointerCount, int[] ids, float[] xs, float[] ys); private native void nativeSetInputMethodContent(long nativePtr, int unicodeChar); - private native void nativeDeleteInputMethodContent(long nativePtr, int offset); + private native void nativeDeleteInputMethodContent(long nativePtr, int offset, int count); private native void nativeRequestExitFullscreenMode(long nativePtr); private native void nativeEvaluateJavascript(long nativePtr, String script, WKCallback callback); private native void nativeScriptDialogClose(long nativeDialogPtr); @@ -745,4 +850,8 @@ private native void nativeOnTouchEvent(long nativePtr, long time, int type, int private native void nativeSetTLSErrorsPolicy(long nativePtr, int policy); protected static native void nativeTriggerSslErrorHandler(long nativeHandlerPtr, boolean acceptCertificate); + private native void nativeFocusIn(long nativePtr); + private native void nativeFocusOut(long nativePtr); + private native void nativeOnKeyEvent(long nativePtr, long time, int action, int keyCode, int unicodeChar, + int modifiers); } diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEChromeClient.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEChromeClient.java index 31fcbf5b4..0b8c95f7a 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEChromeClient.java +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEChromeClient.java @@ -53,6 +53,70 @@ default void onProgressChanged(@NonNull WPEView view, int progress) {} */ default void onReceivedTitle(@NonNull WPEView view, @NonNull String title) {} + /** + * Notify the host application that the URI has changed. + * @param view The WPEView that initiated the callback. + * @param uri The new URI. + */ + default void onUriChanged(@NonNull WPEView view, @NonNull String uri) {} + + /** + * Notify the host application that the audio playback state has changed. + * This is called when the WebView starts or stops playing audio. + * @param view The WPEView that initiated the callback. + * @param isPlayingAudio {@code true} if the WebView is currently playing audio, + * {@code false} otherwise. + */ + default void onAudioStateChanged(@NonNull WPEView view, boolean isPlayingAudio) {} + + /** + * Notify the host application that the muted state has changed. + * This is called when the WebView is muted or unmuted. + * @param view The WPEView that initiated the callback. + * @param isMuted {@code true} if the WebView is currently muted, + * {@code false} otherwise. + */ + default void onMutedStateChanged(@NonNull WPEView view, boolean isMuted) {} + + /** + * Notify the host application that the camera capture state has changed. + * This is called when a page starts or stops capturing from the camera. + * @param view The WPEView that initiated the callback. + * @param state The capture state. One of: + *
    + *
  • {@code 0} - MEDIA_CAPTURE_STATE_NONE: Camera capture is not active.
  • + *
  • {@code 1} - MEDIA_CAPTURE_STATE_ACTIVE: Camera capture is active.
  • + *
  • {@code 2} - MEDIA_CAPTURE_STATE_MUTED: Camera capture is muted.
  • + *
+ */ + default void onCameraCaptureStateChanged(@NonNull WPEView view, int state) {} + + /** + * Notify the host application that the microphone capture state has changed. + * This is called when a page starts or stops capturing from the microphone. + * @param view The WPEView that initiated the callback. + * @param state The capture state. One of: + *
    + *
  • {@code 0} - MEDIA_CAPTURE_STATE_NONE: Microphone capture is not active.
  • + *
  • {@code 1} - MEDIA_CAPTURE_STATE_ACTIVE: Microphone capture is active.
  • + *
  • {@code 2} - MEDIA_CAPTURE_STATE_MUTED: Microphone capture is muted.
  • + *
+ */ + default void onMicrophoneCaptureStateChanged(@NonNull WPEView view, int state) {} + + /** + * Notify the host application that the display capture state has changed. + * This is called when a page starts or stops screen sharing. + * @param view The WPEView that initiated the callback. + * @param state The capture state. One of: + *
    + *
  • {@code 0} - MEDIA_CAPTURE_STATE_NONE: Display capture is not active.
  • + *
  • {@code 1} - MEDIA_CAPTURE_STATE_ACTIVE: Display capture is active.
  • + *
  • {@code 2} - MEDIA_CAPTURE_STATE_MUTED: Display capture is muted.
  • + *
+ */ + default void onDisplayCaptureStateChanged(@NonNull WPEView view, int state) {} + /** * A callback interface used by the host application to notify * the current page that its custom view has been dismissed. @@ -78,6 +142,20 @@ default void onShowCustomView(@NonNull View view, @NonNull WPEChromeClient.Custo */ default void onHideCustomView() {} + /** + * Notify the host application that the fullscreen mode has changed. + * This is called when the WebView enters or exits fullscreen mode. + *

+ * This callback provides a simpler alternative to {@link #onShowCustomView} + * and {@link #onHideCustomView} when you only need to track fullscreen state + * without custom view management. + * + * @param view The WPEView that initiated the callback. + * @param isFullscreen {@code true} if the WebView is entering fullscreen mode, + * {@code false} if exiting fullscreen mode. + */ + default void onFullscreenModeChanged(@NonNull WPEView view, boolean isFullscreen) {} + /** * Notify the host application that the web page wants to display a * JavaScript {@code alert()} dialog. diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEContext.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEContext.java index 7e872fb6e..f1470b9db 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEContext.java +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEContext.java @@ -45,7 +45,7 @@ public void setClient(@Nullable WPEContext.Client client) { return null; }); } else { - setClient(null); + this.context.setClient(null); } } WKWebContext getWebContext() { return context; } diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java index 657683acd..42c06b479 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java @@ -157,6 +157,18 @@ public static void enableRemoteInspector(int inspectorPort, boolean useHttpInspe */ public void reload() { wkWebView.reload(); } + /** + * Returns whether this WebView is muted. + * @return {@code true} if the WebView is muted, {@code false} otherwise. + */ + public boolean isMuted() { return wkWebView.isMuted(); } + + /** + * Sets whether this WebView should be muted. + * @param muted {@code true} to mute the WebView, {@code false} to unmute. + */ + public void setMuted(boolean muted) { wkWebView.setMuted(muted); } + /** * Gets loading progress for the current page. * @return the loading progress for the current page (between 0 and 100). @@ -278,18 +290,6 @@ public void evaluateJavascript(@NonNull String script, @Nullable WPECallbackNote: Do not call {@link WPEView#loadUrl(String)} with the same URL + * from within this method. Doing so triggers an infinite loop. + * + * @param view The WPEView that is initiating the callback. + * @param url The URL to be loaded. + * @param isRedirect {@code true} if the navigation was triggered by a redirect. + * @param isUserGesture {@code true} if the navigation was triggered by a user gesture. + * @return {@code true} to cancel the current load, {@code false} to continue. + */ + public boolean shouldOverrideUrlLoading(@NonNull WPEView view, @NonNull String url, boolean isRedirect, + boolean isUserGesture) { + return false; + } + /** * Notify the host application that a page has started loading. This method * is called once for each main frame load so a page with iframes or @@ -45,6 +64,52 @@ public void onPageStarted(@NonNull WPEView view, @NonNull String url) {} */ public void onPageFinished(@NonNull WPEView view, @NonNull String url) {} + /** + * Notify the host application that the loading state has changed. + * This is called whenever the WebView starts or stops loading content. + * @param view The WPEView that is initiating the callback. + * @param isLoading {@code true} if the WebView is currently loading, {@code false} otherwise. + */ + public void onLoadingStateChanged(@NonNull WPEView view, boolean isLoading) {} + + /** + * Notify the host application that a page failed to load. + *

+ * This callback is invoked when an error prevents a page from loading. + * Common errors include network failures, DNS resolution failures, + * and policy errors. + *

+ * Note that {@link #onPageFinished} will still be called after this callback. + * + * @param view The WPEView that is initiating the callback. + * @param failingUri The URI that failed to load. + * @param errorCode The error code from WebKit. + * @param errorDomain The error domain (e.g., "WebKitNetworkError", "WebKitPolicyError"). + * @param errorMessage A human-readable description of the error. + * @return {@code true} if the error was handled and the default error page + * should not be shown; {@code false} to show the default error page. + */ + public boolean onLoadFailed(@NonNull WPEView view, @NonNull String failingUri, int errorCode, + @NonNull String errorDomain, @NonNull String errorMessage) { + return false; + } + + /** + * Notify the host application that the renderer process has exited. + *

+ * Multiple {@link WPEView} instances may be associated with a single render process. + * The application should handle this by destroying all associated WebViews. + * + * @param view The WPEView that initiated the callback. + * @param terminationReason The reason for the termination. One of: + *

    + *
  • {@code 0} - WEB_PROCESS_CRASHED: The web process crashed.
  • + *
  • {@code 1} - WEB_PROCESS_EXCEEDED_MEMORY_LIMIT: The web process exceeded memory limits.
  • + *
  • {@code 2} - WEB_PROCESS_TERMINATED_BY_API: The web process was terminated by API call.
  • + *
+ */ + public void onRenderProcessGone(@NonNull WPEView view, int terminationReason) {} + /** * Notify the host application that the internal SurfaceView has been created * and it's ready to render to it's surface. From ed7fd804a2e537f5fd4828990ae3f5025a7acca5 Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Wed, 21 Jan 2026 16:45:01 +0900 Subject: [PATCH 3/7] Address review feedback --- .../src/main/cpp/Common/WPEDisplayAndroid.cpp | 84 +++++-------- .../Common/WPEInputMethodContextAndroid.cpp | 108 ++++++----------- .../main/cpp/Common/WPEToplevelAndroid.cpp | 39 +----- .../src/main/cpp/Runtime/WPEViewAndroid.cpp | 113 ++++++++++-------- wpeview/src/main/cpp/Runtime/WPEViewAndroid.h | 8 ++ 5 files changed, 143 insertions(+), 209 deletions(-) diff --git a/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp index 4ba77d972..5dfad594e 100644 --- a/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp +++ b/wpeview/src/main/cpp/Common/WPEDisplayAndroid.cpp @@ -25,32 +25,27 @@ #include #include +#include struct _WPEDisplayAndroid { WPEDisplay parent; -}; - -typedef struct { - gpointer eglDisplay; + EGLDisplay eglDisplay; WPEToplevel* toplevel; -} WPEDisplayAndroidPrivate; +}; -G_DEFINE_TYPE_WITH_PRIVATE(WPEDisplayAndroid, wpe_display_android, WPE_TYPE_DISPLAY) +G_DEFINE_FINAL_TYPE(WPEDisplayAndroid, wpe_display_android, WPE_TYPE_DISPLAY) static void wpeDisplayAndroidDispose(GObject* object) { Logging::logDebug("WPEDisplayAndroid::dispose(%p)", object); - auto* priv - = static_cast(wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(object))); + auto* display = WPE_DISPLAY_ANDROID(object); - // Clean up toplevel - g_clear_object(&priv->toplevel); + g_clear_object(&display->toplevel); - // Clean up EGL display if initialized - if (priv->eglDisplay != nullptr) { - eglTerminate(static_cast(priv->eglDisplay)); - priv->eglDisplay = nullptr; + if (display->eglDisplay != EGL_NO_DISPLAY) { + eglTerminate(display->eglDisplay); + display->eglDisplay = EGL_NO_DISPLAY; } G_OBJECT_CLASS(wpe_display_android_parent_class)->dispose(object); @@ -67,11 +62,9 @@ static WPEView* wpeDisplayAndroidCreateView(WPEDisplay* display) Logging::logDebug("WPEDisplayAndroid::create_view(%p)", display); auto* view = wpe_view_android_new(display); - auto* priv = static_cast( - wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); - if (priv->toplevel) { - wpe_view_set_toplevel(view, priv->toplevel); - } + auto* displayAndroid = WPE_DISPLAY_ANDROID(display); + if (displayAndroid->toplevel) + wpe_view_set_toplevel(view, displayAndroid->toplevel); return view; } @@ -80,17 +73,14 @@ static gpointer wpeDisplayAndroidGetEGLDisplay(WPEDisplay* display, GError** err { Logging::logDebug("WPEDisplayAndroid::get_egl_display(%p)", display); - auto* priv = static_cast( - wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + auto* displayAndroid = WPE_DISPLAY_ANDROID(display); - if (priv->eglDisplay != nullptr) { - return priv->eglDisplay; - } + if (displayAndroid->eglDisplay != EGL_NO_DISPLAY) + return displayAndroid->eglDisplay; EGLDisplay eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (eglDisplay == EGL_NO_DISPLAY) { - EGLint eglError = eglGetError(); - Logging::logError("WPEDisplayAndroid::get_egl_display - eglGetDisplay failed with error 0x%x", eglError); + Logging::logError("WPEDisplayAndroid::get_egl_display - eglGetDisplay failed with error 0x%04X", eglGetError()); g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "Failed to get EGL display"); return nullptr; } @@ -98,19 +88,18 @@ static gpointer wpeDisplayAndroidGetEGLDisplay(WPEDisplay* display, GError** err EGLint major = 0; EGLint minor = 0; if (!eglInitialize(eglDisplay, &major, &minor)) { - EGLint eglError = eglGetError(); - Logging::logError("WPEDisplayAndroid::get_egl_display - eglInitialize failed with error 0x%x", eglError); + Logging::logError("WPEDisplayAndroid::get_egl_display - eglInitialize failed with error 0x%04X", eglGetError()); g_set_error_literal(error, WPE_DISPLAY_ERROR, WPE_DISPLAY_ERROR_CONNECTION_FAILED, "Failed to initialize EGL"); return nullptr; } Logging::logDebug("EGL initialized: version %d.%d", major, minor); - priv->eglDisplay = eglDisplay; - return priv->eglDisplay; + displayAndroid->eglDisplay = eglDisplay; + return displayAndroid->eglDisplay; } -static gboolean wpeDisplayAndroidUseExplicitSync(WPEDisplay* /*display*/) { return TRUE; } +static gboolean wpeDisplayAndroidUseExplicitSync(WPEDisplay*) { return TRUE; } static WPEInputMethodContext* wpeDisplayAndroidCreateInputMethodContext(WPEDisplay* display, WPEView* view) { @@ -118,24 +107,20 @@ static WPEInputMethodContext* wpeDisplayAndroidCreateInputMethodContext(WPEDispl return wpe_input_method_context_android_new(view); } -static WPEBufferFormats* wpeDisplayAndroidGetPreferredBufferFormats(WPEDisplay* /*display*/) +static WPEBufferFormats* wpeDisplayAndroidGetPreferredBufferFormats(WPEDisplay*) { - static const struct { - uint32_t fourcc; - uint64_t modifier; - } formats[] = { - {0x34324152, 0}, // DRM_FORMAT_RGBA8888 - {0x34325852, 0}, // DRM_FORMAT_RGBX8888 - {0x34324752, 0}, // DRM_FORMAT_RGB888 - {0x36314752, 0}, // DRM_FORMAT_RGB565 + static constexpr uint32_t formats[] = { + DRM_FORMAT_RGBA8888, + DRM_FORMAT_RGBX8888, + DRM_FORMAT_RGB888, + DRM_FORMAT_RGB565, }; auto* builder = wpe_buffer_formats_builder_new(nullptr); wpe_buffer_formats_builder_append_group(builder, nullptr, WPE_BUFFER_FORMAT_USAGE_RENDERING); - for (const auto& format : formats) { - wpe_buffer_formats_builder_append_format(builder, format.fourcc, format.modifier); - } + for (auto format : formats) + wpe_buffer_formats_builder_append_format(builder, format, 0); return wpe_buffer_formats_builder_end(builder); } @@ -158,16 +143,13 @@ static void wpe_display_android_init(WPEDisplayAndroid* display) { Logging::logDebug("WPEDisplayAndroid::init(%p)", display); - auto* priv = static_cast( - wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); + display->eglDisplay = EGL_NO_DISPLAY; - // Set available input devices for Android auto inputDevices = static_cast( WPE_AVAILABLE_INPUT_DEVICE_TOUCHSCREEN | WPE_AVAILABLE_INPUT_DEVICE_KEYBOARD); wpe_display_set_available_input_devices(WPE_DISPLAY(display), inputDevices); - // Create the toplevel for this display - priv->toplevel = wpe_toplevel_android_new(WPE_DISPLAY(display)); + display->toplevel = wpe_toplevel_android_new(WPE_DISPLAY(display)); } WPEDisplay* wpe_display_android_new(void) { return WPE_DISPLAY(g_object_new(WPE_TYPE_DISPLAY_ANDROID, nullptr)); } @@ -175,9 +157,5 @@ WPEDisplay* wpe_display_android_new(void) { return WPE_DISPLAY(g_object_new(WPE_ WPEToplevel* wpe_display_android_get_toplevel(WPEDisplay* display) { g_return_val_if_fail(WPE_IS_DISPLAY_ANDROID(display), nullptr); - - auto* priv = static_cast( - wpe_display_android_get_instance_private(WPE_DISPLAY_ANDROID(display))); - - return priv->toplevel; + return WPE_DISPLAY_ANDROID(display)->toplevel; } diff --git a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp index 213117ada..dc5e54968 100644 --- a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp +++ b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp @@ -19,52 +19,32 @@ #include "WPEInputMethodContextAndroid.h" #include "Logging.h" - -#include - -// Map from WPEView to WPEInputMethodContext for lookup from JNI -static std::unordered_map s_contextMap; - -// Map from WPEView to focus callbacks (registered before context is created) -struct FocusCallbacks { - WPEInputMethodContextAndroidFocusCallback focusInCallback = nullptr; - WPEInputMethodContextAndroidFocusCallback focusOutCallback = nullptr; - void* userData = nullptr; -}; -static std::unordered_map s_pendingCallbacks; +#include "WPEViewAndroid.h" struct _WPEInputMethodContextAndroid { WPEInputMethodContext parent; -}; - -typedef struct { WPEInputMethodContextAndroidFocusCallback focusInCallback; WPEInputMethodContextAndroidFocusCallback focusOutCallback; void* callbackUserData; -} WPEInputMethodContextAndroidPrivate; - -G_DEFINE_TYPE_WITH_PRIVATE( - WPEInputMethodContextAndroid, wpe_input_method_context_android, WPE_TYPE_INPUT_METHOD_CONTEXT) +}; -static inline WPEInputMethodContextAndroidPrivate* getPrivate(WPEInputMethodContextAndroid* context) -{ - return static_cast( - wpe_input_method_context_android_get_instance_private(context)); -} +G_DEFINE_FINAL_TYPE(WPEInputMethodContextAndroid, wpe_input_method_context_android, WPE_TYPE_INPUT_METHOD_CONTEXT) static void wpeInputMethodContextAndroidDispose(GObject* object) { auto* context = WPE_INPUT_METHOD_CONTEXT(object); auto* view = wpe_input_method_context_get_view(context); if (view != nullptr) { - Logging::logDebug("WPEInputMethodContextAndroid::dispose - removing from map for view %p", view); - s_contextMap.erase(view); + Logging::logDebug("WPEInputMethodContextAndroid::dispose view=%p", view); + auto* viewAndroid = WPE_VIEW_ANDROID(view); + if (wpe_view_android_get_input_method_context(viewAndroid) == context) + wpe_view_android_set_input_method_context(viewAndroid, nullptr); } G_OBJECT_CLASS(wpe_input_method_context_android_parent_class)->dispose(object); } static void wpeInputMethodContextAndroidGetPreeditString( - WPEInputMethodContext* /*context*/, char** text, GList** underlines, guint* cursorOffset) + WPEInputMethodContext*, char** text, GList** underlines, guint* cursorOffset) { // No preedit support for now - all text is committed immediately if (text != nullptr) @@ -78,22 +58,22 @@ static void wpeInputMethodContextAndroidGetPreeditString( static void wpeInputMethodContextAndroidFocusIn(WPEInputMethodContext* context) { auto* view = wpe_input_method_context_get_view(context); - Logging::logDebug("WPEInputMethodContextAndroid::focus_in(%p) view=%p callback=%p", context, view, - getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context))->focusInCallback); - auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); - if (priv->focusInCallback != nullptr) - priv->focusInCallback(priv->callbackUserData); + auto* imContext = WPE_INPUT_METHOD_CONTEXT_ANDROID(context); + Logging::logDebug( + "WPEInputMethodContextAndroid::focus_in(%p) view=%p callback=%p", context, view, imContext->focusInCallback); + if (imContext->focusInCallback != nullptr) + imContext->focusInCallback(imContext->callbackUserData); } static void wpeInputMethodContextAndroidFocusOut(WPEInputMethodContext* context) { Logging::logDebug("WPEInputMethodContextAndroid::focus_out(%p)", context); - auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); - if (priv->focusOutCallback != nullptr) - priv->focusOutCallback(priv->callbackUserData); + auto* imContext = WPE_INPUT_METHOD_CONTEXT_ANDROID(context); + if (imContext->focusOutCallback != nullptr) + imContext->focusOutCallback(imContext->callbackUserData); } -static void wpeInputMethodContextAndroidReset(WPEInputMethodContext* /*context*/) +static void wpeInputMethodContextAndroidReset(WPEInputMethodContext*) { // No state to reset } @@ -121,30 +101,20 @@ WPEInputMethodContext* wpe_input_method_context_android_new(WPEView* view) auto* context = WPE_INPUT_METHOD_CONTEXT(g_object_new(WPE_TYPE_INPUT_METHOD_CONTEXT_ANDROID, "view", view, nullptr)); - // Store in map for later lookup from JNI - s_contextMap[view] = context; - Logging::logDebug("WPEInputMethodContextAndroid::new - added to map, now has %zu entries", s_contextMap.size()); - - // Check if there are pending callbacks registered before this context was created - auto it = s_pendingCallbacks.find(view); - if (it != s_pendingCallbacks.end()) { - auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); - priv->focusInCallback = it->second.focusInCallback; - priv->focusOutCallback = it->second.focusOutCallback; - priv->callbackUserData = it->second.userData; - s_pendingCallbacks.erase(it); - Logging::logDebug("WPEInputMethodContextAndroid::new - applied pending callbacks"); - } + auto* viewAndroid = WPE_VIEW_ANDROID(view); + wpe_view_android_set_input_method_context(viewAndroid, context); + Logging::logDebug("WPEInputMethodContextAndroid::new - stored in view=%p", view); + + wpe_view_android_apply_pending_focus_callbacks(viewAndroid, context); return context; } WPEInputMethodContext* wpe_input_method_context_android_get_for_view(WPEView* view) { - auto it = s_contextMap.find(view); - if (it != s_contextMap.end()) - return it->second; - return nullptr; + if (!WPE_IS_VIEW_ANDROID(view)) + return nullptr; + return wpe_view_android_get_input_method_context(WPE_VIEW_ANDROID(view)); } void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* context, @@ -152,10 +122,10 @@ void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) { g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); - auto* priv = getPrivate(WPE_INPUT_METHOD_CONTEXT_ANDROID(context)); - priv->focusInCallback = focusInCallback; - priv->focusOutCallback = focusOutCallback; - priv->callbackUserData = userData; + auto* imContext = WPE_INPUT_METHOD_CONTEXT_ANDROID(context); + imContext->focusInCallback = focusInCallback; + imContext->focusOutCallback = focusOutCallback; + imContext->callbackUserData = userData; Logging::logDebug("WPEInputMethodContextAndroid::set_focus_callbacks(%p, %p, %p, %p)", context, focusInCallback, focusOutCallback, userData); } @@ -164,17 +134,15 @@ void wpe_input_method_context_android_set_focus_callbacks_for_view(WPEView* view WPEInputMethodContextAndroidFocusCallback focusInCallback, WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) { - g_return_if_fail(view != nullptr); + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + auto* viewAndroid = WPE_VIEW_ANDROID(view); - // Check if context already exists - auto* context = wpe_input_method_context_android_get_for_view(view); - if (context != nullptr) { + if (auto* context = wpe_view_android_get_input_method_context(viewAndroid)) { wpe_input_method_context_android_set_focus_callbacks(context, focusInCallback, focusOutCallback, userData); return; } - // Context doesn't exist yet - store callbacks for later - s_pendingCallbacks[view] = {focusInCallback, focusOutCallback, userData}; + wpe_view_android_set_pending_focus_callbacks(viewAndroid, focusInCallback, focusOutCallback, userData); Logging::logDebug("WPEInputMethodContextAndroid::set_focus_callbacks_for_view(%p) - stored as pending", view); } @@ -184,15 +152,13 @@ void wpe_input_method_context_android_commit_text(WPEInputMethodContext* context auto* view = wpe_input_method_context_get_view(context); Logging::logDebug("WPEInputMethodContextAndroid::commit_text(%p, '%s') view=%p", context, text, view); - // Check if this context is in our map (sanity check) - auto* mapContext = wpe_input_method_context_android_get_for_view(view); - if (mapContext != context) { - Logging::logError( - "WPEInputMethodContextAndroid::commit_text - context mismatch! map=%p, this=%p", mapContext, context); + auto* viewContext = wpe_input_method_context_android_get_for_view(view); + if (viewContext != context) { + Logging::logError("WPEInputMethodContextAndroid::commit_text - mismatch view=%p this=%p", viewContext, context); } g_signal_emit_by_name(context, "committed", text); - Logging::logDebug("WPEInputMethodContextAndroid::commit_text - signal emitted"); + Logging::logDebug("WPEInputMethodContextAndroid::commit_text signal emitted"); } void wpe_input_method_context_android_delete_surrounding(WPEInputMethodContext* context, int offset, unsigned int count) diff --git a/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp index ba4e01bf6..822afed19 100644 --- a/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp +++ b/wpeview/src/main/cpp/Common/WPEToplevelAndroid.cpp @@ -20,22 +20,11 @@ #include "Logging.h" -/** - * WPEToplevelAndroid: - * - * Android implementation of #WPEToplevel for the WPE Platform API. - * This provides a minimal toplevel implementation for Android applications. - */ - struct _WPEToplevelAndroid { WPEToplevel parent; }; -typedef struct { - // Reserved for future use -} WPEToplevelAndroidPrivate; - -G_DEFINE_TYPE_WITH_PRIVATE(WPEToplevelAndroid, wpe_toplevel_android, WPE_TYPE_TOPLEVEL) +G_DEFINE_FINAL_TYPE(WPEToplevelAndroid, wpe_toplevel_android, WPE_TYPE_TOPLEVEL) static void wpeToplevelAndroidConstructed(GObject* object) { @@ -50,17 +39,6 @@ static void wpeToplevelAndroidConstructed(GObject* object) wpe_toplevel_state_changed(toplevel, WPE_TOPLEVEL_STATE_ACTIVE); } -static void wpeToplevelAndroidSetTitle(WPEToplevel* toplevel, const char* title) -{ - Logging::logDebug("WPEToplevelAndroid::set_title(%p, %s)", toplevel, title ? title : "(null)"); -} - -static WPEScreen* wpeToplevelAndroidGetScreen(WPEToplevel* toplevel) -{ - Logging::logDebug("WPEToplevelAndroid::get_screen(%p)", toplevel); - return nullptr; -} - static gboolean wpeToplevelAndroidResize(WPEToplevel* toplevel, int width, int height) { Logging::logDebug("WPEToplevelAndroid::resize(%p, %d, %d)", toplevel, width, height); @@ -121,11 +99,8 @@ static gboolean wpeToplevelAndroidSetMinimized(WPEToplevel* toplevel) static WPEBufferFormats* wpeToplevelAndroidGetPreferredBufferFormats(WPEToplevel* toplevel) { - auto* display = wpe_toplevel_get_display(toplevel); - - if (display) { + if (auto* display = wpe_toplevel_get_display(toplevel)) return wpe_display_get_preferred_buffer_formats(display); - } return nullptr; } @@ -135,8 +110,6 @@ static void wpe_toplevel_android_class_init(WPEToplevelAndroidClass* toplevelAnd objectClass->constructed = wpeToplevelAndroidConstructed; WPEToplevelClass* toplevelClass = WPE_TOPLEVEL_CLASS(toplevelAndroidClass); - toplevelClass->set_title = wpeToplevelAndroidSetTitle; - toplevelClass->get_screen = wpeToplevelAndroidGetScreen; toplevelClass->resize = wpeToplevelAndroidResize; toplevelClass->set_fullscreen = wpeToplevelAndroidSetFullscreen; toplevelClass->set_maximized = wpeToplevelAndroidSetMaximized; @@ -149,14 +122,6 @@ static void wpe_toplevel_android_init(WPEToplevelAndroid* toplevel) Logging::logDebug("WPEToplevelAndroid::init(%p)", toplevel); } -/** - * wpe_toplevel_android_new: - * @display: a #WPEDisplay - * - * Create a new #WPEToplevel on @display. - * - * Returns: (transfer full): a #WPEToplevel - */ WPEToplevel* wpe_toplevel_android_new(WPEDisplay* display) { g_return_val_if_fail(WPE_IS_DISPLAY(display), nullptr); diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp index 5ee4e3b9b..b12853522 100644 --- a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp @@ -22,43 +22,40 @@ #include "RendererSurfaceControl.h" #include "ScopedFD.h" #include "WPEDisplayAndroid.h" +#include "WPEInputMethodContextAndroid.h" #include struct _WPEViewAndroid { WPEView parent; -}; - -typedef struct { std::shared_ptr renderer; -} WPEViewAndroidPrivate; -G_DEFINE_TYPE_WITH_PRIVATE(WPEViewAndroid, wpe_view_android, WPE_TYPE_VIEW) - -static void wpeViewAndroidConstructed(GObject* object) -{ - G_OBJECT_CLASS(wpe_view_android_parent_class)->constructed(object); + WPEInputMethodContext* inputMethodContext; + struct { + WPEInputMethodContextAndroidFocusCallback focusInCallback; + WPEInputMethodContextAndroidFocusCallback focusOutCallback; + void* userData; + } pendingFocusCallbacks; +}; - Logging::logDebug("WPEViewAndroid::constructed(%p)", object); -} +G_DEFINE_FINAL_TYPE(WPEViewAndroid, wpe_view_android, WPE_TYPE_VIEW) static void wpeViewAndroidDispose(GObject* object) { Logging::logDebug("WPEViewAndroid::dispose(%p)", object); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(object))); - - priv->renderer.reset(); + auto* view = WPE_VIEW_ANDROID(object); + view->renderer.reset(); G_OBJECT_CLASS(wpe_view_android_parent_class)->dispose(object); } static gboolean wpeViewAndroidRenderBuffer( - WPEView* view, WPEBuffer* buffer, const WPERectangle* /*damageRects*/, guint nDamageRects, GError** error) + WPEView* view, WPEBuffer* buffer, const WPERectangle*, guint nDamageRects, GError** error) { Logging::logDebug("WPEViewAndroid::render_buffer(%p, %p, %u rects)", view, buffer, nDamageRects); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + auto* viewAndroid = WPE_VIEW_ANDROID(view); if (!WPE_IS_BUFFER_ANDROID(buffer)) { g_set_error_literal(error, WPE_VIEW_ERROR, WPE_VIEW_ERROR_RENDER_FAILED, "Buffer is not a WPEBufferAndroid"); @@ -73,14 +70,11 @@ static gboolean wpeViewAndroidRenderBuffer( return FALSE; } - if (priv->renderer) { - // Commit buffer to SurfaceControl for display with GPU rendering fence. - // Buffer ownership and lifecycle is managed by RendererSurfaceControl. - int renderingFence = wpe_buffer_take_rendering_fence(buffer); + if (viewAndroid->renderer) { + const int renderingFence = wpe_buffer_take_rendering_fence(buffer); auto fenceFD = std::make_shared(renderingFence); - priv->renderer->commitBuffer(ahb, bufferAndroid, fenceFD); + viewAndroid->renderer->commitBuffer(ahb, bufferAndroid, fenceFD); } else { - // Headless rendering: signal that the buffer is available for reuse. wpe_view_buffer_released(view, buffer); wpe_view_buffer_rendered(view, buffer); } @@ -95,12 +89,16 @@ static void wpe_view_android_class_init(WPEViewAndroidClass* klass) GObjectClass* objectClass = G_OBJECT_CLASS(klass); WPEViewClass* viewClass = WPE_VIEW_CLASS(klass); - objectClass->constructed = wpeViewAndroidConstructed; objectClass->dispose = wpeViewAndroidDispose; viewClass->render_buffer = wpeViewAndroidRenderBuffer; } -static void wpe_view_android_init(WPEViewAndroid* view) { Logging::logDebug("WPEViewAndroid::init(%p)", view); } +static void wpe_view_android_init(WPEViewAndroid* view) +{ + Logging::logDebug("WPEViewAndroid::init(%p)", view); + view->inputMethodContext = nullptr; + view->pendingFocusCallbacks = {}; +} WPEView* wpe_view_android_new(WPEDisplay* display) { @@ -154,14 +152,10 @@ void wpe_view_android_set_renderer(WPEViewAndroid* view, const std::shared_ptr(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); - - priv->renderer = renderer; + view->renderer = renderer; - if (renderer) { - // Give renderer direct access to WPEView for buffer lifecycle callbacks + if (renderer) renderer->setWPEView(WPE_VIEW(view)); - } } void wpe_view_android_on_surface_created(WPEViewAndroid* view, ANativeWindow* window) @@ -170,11 +164,8 @@ void wpe_view_android_on_surface_created(WPEViewAndroid* view, ANativeWindow* wi Logging::logDebug("WPEViewAndroid::on_surface_created(%p, %p)", view, window); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); - - if (priv->renderer) { - priv->renderer->onSurfaceCreated(window); - } + if (view->renderer) + view->renderer->onSurfaceCreated(window); } void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint32_t width, uint32_t height) @@ -183,11 +174,8 @@ void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint3 Logging::logDebug("WPEViewAndroid::on_surface_changed(%p, %d, %u, %u)", view, format, width, height); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); - - if (priv->renderer) { - priv->renderer->onSurfaceChanged(format, width, height); - } + if (view->renderer) + view->renderer->onSurfaceChanged(format, width, height); } void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view) @@ -196,11 +184,8 @@ void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view) Logging::logDebug("WPEViewAndroid::on_surface_redraw_needed(%p)", view); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); - - if (priv->renderer) { - priv->renderer->onSurfaceRedrawNeeded(); - } + if (view->renderer) + view->renderer->onSurfaceRedrawNeeded(); } void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view) @@ -209,9 +194,41 @@ void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view) Logging::logDebug("WPEViewAndroid::on_surface_destroyed(%p)", view); - auto* priv = static_cast(wpe_view_android_get_instance_private(WPE_VIEW_ANDROID(view))); + if (view->renderer) + view->renderer->onSurfaceDestroyed(); +} + +WPEInputMethodContext* wpe_view_android_get_input_method_context(WPEViewAndroid* view) +{ + g_return_val_if_fail(WPE_IS_VIEW_ANDROID(view), nullptr); + return view->inputMethodContext; +} + +void wpe_view_android_set_input_method_context(WPEViewAndroid* view, WPEInputMethodContext* context) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + view->inputMethodContext = context; +} + +void wpe_view_android_set_pending_focus_callbacks(WPEViewAndroid* view, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + view->pendingFocusCallbacks.focusInCallback = focusInCallback; + view->pendingFocusCallbacks.focusOutCallback = focusOutCallback; + view->pendingFocusCallbacks.userData = userData; +} + +void wpe_view_android_apply_pending_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContext* context) +{ + g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); - if (priv->renderer) { - priv->renderer->onSurfaceDestroyed(); + if (view->pendingFocusCallbacks.focusInCallback || view->pendingFocusCallbacks.focusOutCallback) { + wpe_input_method_context_android_set_focus_callbacks(context, view->pendingFocusCallbacks.focusInCallback, + view->pendingFocusCallbacks.focusOutCallback, view->pendingFocusCallbacks.userData); + view->pendingFocusCallbacks = {}; + Logging::logDebug("WPEViewAndroid: applied pending focus callbacks"); } } diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h index ebb4afed7..815520d65 100644 --- a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h @@ -26,6 +26,8 @@ G_BEGIN_DECLS #define WPE_TYPE_VIEW_ANDROID (wpe_view_android_get_type()) G_DECLARE_FINAL_TYPE(WPEViewAndroid, wpe_view_android, WPE, VIEW_ANDROID, WPEView) +typedef void (*WPEInputMethodContextAndroidFocusCallback)(void* userData); + G_END_DECLS #include @@ -43,3 +45,9 @@ void wpe_view_android_on_surface_created(WPEViewAndroid* view, struct ANativeWin void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint32_t width, uint32_t height); void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view); void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view); +void wpe_view_android_set_input_method_context(WPEViewAndroid* view, WPEInputMethodContext* context); +void wpe_view_android_set_pending_focus_callbacks(WPEViewAndroid* view, + WPEInputMethodContextAndroidFocusCallback focusInCallback, + WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); +void wpe_view_android_apply_pending_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContext* context); +WPEInputMethodContext* wpe_view_android_get_input_method_context(WPEViewAndroid* view); From 01b41168a15d04a46ddb35c42e34270c3132a564 Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Wed, 21 Jan 2026 17:05:03 +0900 Subject: [PATCH 4/7] Simplify input method management --- .../Common/WPEInputMethodContextAndroid.cpp | 41 +------------------ .../cpp/Common/WPEInputMethodContextAndroid.h | 13 ------ wpeview/src/main/cpp/Runtime/WKWebView.cpp | 17 +++----- .../src/main/cpp/Runtime/WPEViewAndroid.cpp | 29 ++++++------- wpeview/src/main/cpp/Runtime/WPEViewAndroid.h | 5 +-- 5 files changed, 25 insertions(+), 80 deletions(-) diff --git a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp index dc5e54968..f50d85c38 100644 --- a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp +++ b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.cpp @@ -100,23 +100,10 @@ WPEInputMethodContext* wpe_input_method_context_android_new(WPEView* view) Logging::logDebug("WPEInputMethodContextAndroid::new(%p)", view); auto* context = WPE_INPUT_METHOD_CONTEXT(g_object_new(WPE_TYPE_INPUT_METHOD_CONTEXT_ANDROID, "view", view, nullptr)); - - auto* viewAndroid = WPE_VIEW_ANDROID(view); - wpe_view_android_set_input_method_context(viewAndroid, context); - Logging::logDebug("WPEInputMethodContextAndroid::new - stored in view=%p", view); - - wpe_view_android_apply_pending_focus_callbacks(viewAndroid, context); - + wpe_view_android_set_input_method_context(WPE_VIEW_ANDROID(view), context); return context; } -WPEInputMethodContext* wpe_input_method_context_android_get_for_view(WPEView* view) -{ - if (!WPE_IS_VIEW_ANDROID(view)) - return nullptr; - return wpe_view_android_get_input_method_context(WPE_VIEW_ANDROID(view)); -} - void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* context, WPEInputMethodContextAndroidFocusCallback focusInCallback, WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) @@ -130,35 +117,11 @@ void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* focusOutCallback, userData); } -void wpe_input_method_context_android_set_focus_callbacks_for_view(WPEView* view, - WPEInputMethodContextAndroidFocusCallback focusInCallback, - WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) -{ - g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); - auto* viewAndroid = WPE_VIEW_ANDROID(view); - - if (auto* context = wpe_view_android_get_input_method_context(viewAndroid)) { - wpe_input_method_context_android_set_focus_callbacks(context, focusInCallback, focusOutCallback, userData); - return; - } - - wpe_view_android_set_pending_focus_callbacks(viewAndroid, focusInCallback, focusOutCallback, userData); - Logging::logDebug("WPEInputMethodContextAndroid::set_focus_callbacks_for_view(%p) - stored as pending", view); -} - void wpe_input_method_context_android_commit_text(WPEInputMethodContext* context, const char* text) { g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); - auto* view = wpe_input_method_context_get_view(context); - Logging::logDebug("WPEInputMethodContextAndroid::commit_text(%p, '%s') view=%p", context, text, view); - - auto* viewContext = wpe_input_method_context_android_get_for_view(view); - if (viewContext != context) { - Logging::logError("WPEInputMethodContextAndroid::commit_text - mismatch view=%p this=%p", viewContext, context); - } - + Logging::logDebug("WPEInputMethodContextAndroid::commit_text(%p, '%s')", context, text); g_signal_emit_by_name(context, "committed", text); - Logging::logDebug("WPEInputMethodContextAndroid::commit_text signal emitted"); } void wpe_input_method_context_android_delete_surrounding(WPEInputMethodContext* context, int offset, unsigned int count) diff --git a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h index 28dbd0815..557ccf056 100644 --- a/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h +++ b/wpeview/src/main/cpp/Common/WPEInputMethodContextAndroid.h @@ -32,22 +32,9 @@ typedef void (*WPEInputMethodContextAndroidFocusCallback)(void* userData); WPE_API WPEInputMethodContext* wpe_input_method_context_android_new(WPEView* view); -// Get the WPEInputMethodContext for a given view (for JNI lookup) -WPE_API WPEInputMethodContext* wpe_input_method_context_android_get_for_view(WPEView* view); - -// Set callbacks for focus in/out events (called by WebKit when input field gains/loses focus) -// Use this version if you have the context WPE_API void wpe_input_method_context_android_set_focus_callbacks(WPEInputMethodContext* context, WPEInputMethodContextAndroidFocusCallback focusInCallback, WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); - -// Register callbacks by view - use this when context may not exist yet -// Callbacks will be applied when the context is created -WPE_API void wpe_input_method_context_android_set_focus_callbacks_for_view(WPEView* view, - WPEInputMethodContextAndroidFocusCallback focusInCallback, - WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); - -// Methods for Java to call via JNI to commit text and delete surrounding text WPE_API void wpe_input_method_context_android_commit_text(WPEInputMethodContext* context, const char* text); WPE_API void wpe_input_method_context_android_delete_surrounding( WPEInputMethodContext* context, int offset, unsigned int count); diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.cpp b/wpeview/src/main/cpp/Runtime/WKWebView.cpp index e3563fa47..b67cd9e68 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.cpp +++ b/wpeview/src/main/cpp/Runtime/WKWebView.cpp @@ -843,10 +843,9 @@ void JNIWKWebViewCache::nativeSetInputMethodContent( Logging::logError("WKWebView::nativeSetInputMethodContent - wpeView is null"); return; } - auto* context = wpe_input_method_context_android_get_for_view(WPE_VIEW(wkWebView->wpeView())); + auto* context = wpe_view_android_get_input_method_context(wkWebView->wpeView()); if (context == nullptr) { - Logging::logError( - "WKWebView::nativeSetInputMethodContent - context is null for view %p", WPE_VIEW(wkWebView->wpeView())); + Logging::logError("WKWebView::nativeSetInputMethodContent - null context for view=%p", wkWebView->wpeView()); return; } static constexpr size_t GUNICHAR_UTF8_BUFFER_SIZE = 8; @@ -861,7 +860,7 @@ void JNIWKWebViewCache::nativeDeleteInputMethodContent( Logging::logDebug("WKWebView::nativeDeleteInputMethodContent(%d, %d) [tid %d]", offset, count, gettid()); auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) if (wkWebView != nullptr && wkWebView->wpeView() != nullptr) { - auto* context = wpe_input_method_context_android_get_for_view(WPE_VIEW(wkWebView->wpeView())); + auto* context = wpe_view_android_get_input_method_context(wkWebView->wpeView()); if (context != nullptr) wpe_input_method_context_android_delete_surrounding(context, offset, static_cast(count)); } @@ -1029,9 +1028,7 @@ WKWebView::WKWebView(JNIEnv* env, JNIWKWebView jniWKWebView, WKWebContext* wkWeb wpe_view_android_resize(m_wpeView, logicalWidth, logicalHeight); wpe_view_android_set_scale(m_wpeView, scale); - // Set up focus callbacks - wpe_input_method_context_android_set_focus_callbacks_for_view( - WPE_VIEW(m_wpeView), onInputMethodFocusIn, onInputMethodFocusOut, this); + wpe_view_android_set_input_method_focus_callbacks(m_wpeView, onInputMethodFocusIn, onInputMethodFocusOut, this); } m_signalHandlers.push_back( @@ -1076,10 +1073,8 @@ void WKWebView::close() noexcept // Ensure that renderer is destroyed first so that all pending commits will be cleared before page is gone m_renderer.reset(); - if (m_wpeView != nullptr) { - wpe_input_method_context_android_set_focus_callbacks_for_view( - WPE_VIEW(m_wpeView), nullptr, nullptr, nullptr); - } + if (m_wpeView != nullptr) + wpe_view_android_set_input_method_focus_callbacks(m_wpeView, nullptr, nullptr, nullptr); for (auto& handler : m_signalHandlers) g_signal_handler_disconnect(m_webView, handler); diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp index b12853522..f1674135f 100644 --- a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.cpp @@ -208,27 +208,28 @@ void wpe_view_android_set_input_method_context(WPEViewAndroid* view, WPEInputMet { g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); view->inputMethodContext = context; + + if (context && (view->pendingFocusCallbacks.focusInCallback || view->pendingFocusCallbacks.focusOutCallback)) { + wpe_input_method_context_android_set_focus_callbacks(context, view->pendingFocusCallbacks.focusInCallback, + view->pendingFocusCallbacks.focusOutCallback, view->pendingFocusCallbacks.userData); + view->pendingFocusCallbacks = {}; + Logging::logDebug("WPEViewAndroid::set_input_method_context(%p) - applied pending focus callbacks", view); + } } -void wpe_view_android_set_pending_focus_callbacks(WPEViewAndroid* view, +void wpe_view_android_set_input_method_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContextAndroidFocusCallback focusInCallback, WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData) { g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); + + if (auto* context = view->inputMethodContext) { + wpe_input_method_context_android_set_focus_callbacks(context, focusInCallback, focusOutCallback, userData); + return; + } + view->pendingFocusCallbacks.focusInCallback = focusInCallback; view->pendingFocusCallbacks.focusOutCallback = focusOutCallback; view->pendingFocusCallbacks.userData = userData; -} - -void wpe_view_android_apply_pending_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContext* context) -{ - g_return_if_fail(WPE_IS_VIEW_ANDROID(view)); - g_return_if_fail(WPE_IS_INPUT_METHOD_CONTEXT_ANDROID(context)); - - if (view->pendingFocusCallbacks.focusInCallback || view->pendingFocusCallbacks.focusOutCallback) { - wpe_input_method_context_android_set_focus_callbacks(context, view->pendingFocusCallbacks.focusInCallback, - view->pendingFocusCallbacks.focusOutCallback, view->pendingFocusCallbacks.userData); - view->pendingFocusCallbacks = {}; - Logging::logDebug("WPEViewAndroid: applied pending focus callbacks"); - } + Logging::logDebug("WPEViewAndroid::set_input_method_focus_callbacks(%p) - stored as pending", view); } diff --git a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h index 815520d65..5776feadb 100644 --- a/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h +++ b/wpeview/src/main/cpp/Runtime/WPEViewAndroid.h @@ -45,9 +45,8 @@ void wpe_view_android_on_surface_created(WPEViewAndroid* view, struct ANativeWin void wpe_view_android_on_surface_changed(WPEViewAndroid* view, int format, uint32_t width, uint32_t height); void wpe_view_android_on_surface_redraw_needed(WPEViewAndroid* view); void wpe_view_android_on_surface_destroyed(WPEViewAndroid* view); +WPEInputMethodContext* wpe_view_android_get_input_method_context(WPEViewAndroid* view); void wpe_view_android_set_input_method_context(WPEViewAndroid* view, WPEInputMethodContext* context); -void wpe_view_android_set_pending_focus_callbacks(WPEViewAndroid* view, +void wpe_view_android_set_input_method_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContextAndroidFocusCallback focusInCallback, WPEInputMethodContextAndroidFocusCallback focusOutCallback, void* userData); -void wpe_view_android_apply_pending_focus_callbacks(WPEViewAndroid* view, WPEInputMethodContext* context); -WPEInputMethodContext* wpe_view_android_get_input_method_context(WPEViewAndroid* view); From 6ac56694fd9b6babb982f256643ca518bda6d35a Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Fri, 2 Jan 2026 22:16:36 +0800 Subject: [PATCH 5/7] Implement keyboard and text input via WPE Platform API Add full keyboard support for web content using WPE's input method context: - AndroidKeyMap.h: Maps Android keycodes to XKB keysyms - WPEInputConnection: Android InputConnection for soft keyboard - Support for text composition, deletion, and hardware key events The input method context integrates with WPE's focus tracking to show/hide the soft keyboard when input fields gain or lose focus. --- wpeview/src/main/cpp/Common/AndroidKeyMap.h | 343 ++++++++++++++++++ wpeview/src/main/cpp/Runtime/WKWebView.cpp | 28 +- .../wpewebkit/wpeview/WPEInputConnection.java | 101 ++++++ .../java/org/wpewebkit/wpeview/WPEView.java | 26 +- 4 files changed, 492 insertions(+), 6 deletions(-) create mode 100644 wpeview/src/main/cpp/Common/AndroidKeyMap.h create mode 100644 wpeview/src/main/java/org/wpewebkit/wpeview/WPEInputConnection.java diff --git a/wpeview/src/main/cpp/Common/AndroidKeyMap.h b/wpeview/src/main/cpp/Common/AndroidKeyMap.h new file mode 100644 index 000000000..98b8c9a30 --- /dev/null +++ b/wpeview/src/main/cpp/Common/AndroidKeyMap.h @@ -0,0 +1,343 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +/** + * Android to XKB keycode/keysym mapping utilities. + * + * WPE Platform expects XKB-style keycodes and keysyms for keyboard events, but Android + * uses its own keycode system (AKEYCODE_*). This header provides the necessary mappings: + * + * - androidToXkbKeycode(): Converts Android keycodes to XKB keycodes (Linux evdev + 8) + * - androidToKeysym(): Converts Android keycode + unicode to XKB keysym + * - androidToWpeModifiers(): Converts Android meta state to WPE modifier flags + */ + +#include +#include +#include +#include +#include + +/** + * Convert Android keycode to XKB keycode (Linux input code + 8). + * Returns 0 if the keycode is not mapped. + */ +inline uint32_t androidToXkbKeycode(int androidKeyCode) +{ + // Map Android keycodes to Linux input codes, then add 8 for XKB compatibility + switch (androidKeyCode) { + // Letters A-Z (Android 29-54 -> Linux 30-55) + case AKEYCODE_A: + return KEY_A + 8; + case AKEYCODE_B: + return KEY_B + 8; + case AKEYCODE_C: + return KEY_C + 8; + case AKEYCODE_D: + return KEY_D + 8; + case AKEYCODE_E: + return KEY_E + 8; + case AKEYCODE_F: + return KEY_F + 8; + case AKEYCODE_G: + return KEY_G + 8; + case AKEYCODE_H: + return KEY_H + 8; + case AKEYCODE_I: + return KEY_I + 8; + case AKEYCODE_J: + return KEY_J + 8; + case AKEYCODE_K: + return KEY_K + 8; + case AKEYCODE_L: + return KEY_L + 8; + case AKEYCODE_M: + return KEY_M + 8; + case AKEYCODE_N: + return KEY_N + 8; + case AKEYCODE_O: + return KEY_O + 8; + case AKEYCODE_P: + return KEY_P + 8; + case AKEYCODE_Q: + return KEY_Q + 8; + case AKEYCODE_R: + return KEY_R + 8; + case AKEYCODE_S: + return KEY_S + 8; + case AKEYCODE_T: + return KEY_T + 8; + case AKEYCODE_U: + return KEY_U + 8; + case AKEYCODE_V: + return KEY_V + 8; + case AKEYCODE_W: + return KEY_W + 8; + case AKEYCODE_X: + return KEY_X + 8; + case AKEYCODE_Y: + return KEY_Y + 8; + case AKEYCODE_Z: + return KEY_Z + 8; + + // Numbers 0-9 + case AKEYCODE_0: + return KEY_0 + 8; + case AKEYCODE_1: + return KEY_1 + 8; + case AKEYCODE_2: + return KEY_2 + 8; + case AKEYCODE_3: + return KEY_3 + 8; + case AKEYCODE_4: + return KEY_4 + 8; + case AKEYCODE_5: + return KEY_5 + 8; + case AKEYCODE_6: + return KEY_6 + 8; + case AKEYCODE_7: + return KEY_7 + 8; + case AKEYCODE_8: + return KEY_8 + 8; + case AKEYCODE_9: + return KEY_9 + 8; + + // Special keys + case AKEYCODE_ENTER: + return KEY_ENTER + 8; + case AKEYCODE_DEL: + return KEY_BACKSPACE + 8; + case AKEYCODE_FORWARD_DEL: + return KEY_DELETE + 8; + case AKEYCODE_ESCAPE: + return KEY_ESC + 8; + case AKEYCODE_TAB: + return KEY_TAB + 8; + case AKEYCODE_SPACE: + return KEY_SPACE + 8; + + // Navigation + case AKEYCODE_DPAD_UP: + return KEY_UP + 8; + case AKEYCODE_DPAD_DOWN: + return KEY_DOWN + 8; + case AKEYCODE_DPAD_LEFT: + return KEY_LEFT + 8; + case AKEYCODE_DPAD_RIGHT: + return KEY_RIGHT + 8; + case AKEYCODE_MOVE_HOME: + return KEY_HOME + 8; + case AKEYCODE_MOVE_END: + return KEY_END + 8; + case AKEYCODE_PAGE_UP: + return KEY_PAGEUP + 8; + case AKEYCODE_PAGE_DOWN: + return KEY_PAGEDOWN + 8; + case AKEYCODE_INSERT: + return KEY_INSERT + 8; + + // Modifiers + case AKEYCODE_SHIFT_LEFT: + return KEY_LEFTSHIFT + 8; + case AKEYCODE_SHIFT_RIGHT: + return KEY_RIGHTSHIFT + 8; + case AKEYCODE_CTRL_LEFT: + return KEY_LEFTCTRL + 8; + case AKEYCODE_CTRL_RIGHT: + return KEY_RIGHTCTRL + 8; + case AKEYCODE_ALT_LEFT: + return KEY_LEFTALT + 8; + case AKEYCODE_ALT_RIGHT: + return KEY_RIGHTALT + 8; + case AKEYCODE_META_LEFT: + return KEY_LEFTMETA + 8; + case AKEYCODE_META_RIGHT: + return KEY_RIGHTMETA + 8; + case AKEYCODE_CAPS_LOCK: + return KEY_CAPSLOCK + 8; + + // Function keys + case AKEYCODE_F1: + return KEY_F1 + 8; + case AKEYCODE_F2: + return KEY_F2 + 8; + case AKEYCODE_F3: + return KEY_F3 + 8; + case AKEYCODE_F4: + return KEY_F4 + 8; + case AKEYCODE_F5: + return KEY_F5 + 8; + case AKEYCODE_F6: + return KEY_F6 + 8; + case AKEYCODE_F7: + return KEY_F7 + 8; + case AKEYCODE_F8: + return KEY_F8 + 8; + case AKEYCODE_F9: + return KEY_F9 + 8; + case AKEYCODE_F10: + return KEY_F10 + 8; + case AKEYCODE_F11: + return KEY_F11 + 8; + case AKEYCODE_F12: + return KEY_F12 + 8; + + // Punctuation and symbols + case AKEYCODE_MINUS: + return KEY_MINUS + 8; + case AKEYCODE_EQUALS: + return KEY_EQUAL + 8; + case AKEYCODE_LEFT_BRACKET: + return KEY_LEFTBRACE + 8; + case AKEYCODE_RIGHT_BRACKET: + return KEY_RIGHTBRACE + 8; + case AKEYCODE_BACKSLASH: + return KEY_BACKSLASH + 8; + case AKEYCODE_SEMICOLON: + return KEY_SEMICOLON + 8; + case AKEYCODE_APOSTROPHE: + return KEY_APOSTROPHE + 8; + case AKEYCODE_GRAVE: + return KEY_GRAVE + 8; + case AKEYCODE_COMMA: + return KEY_COMMA + 8; + case AKEYCODE_PERIOD: + return KEY_DOT + 8; + case AKEYCODE_SLASH: + return KEY_SLASH + 8; + + default: + return 0; + } +} + +/** + * Convert Android keycode and unicode character to XKB keysym. + * For printable ASCII characters, the unicode value equals the keysym. + * For special keys, returns the appropriate XKB keysym. + */ +inline uint32_t androidToKeysym(int androidKeyCode, int unicodeChar) +{ + // For printable ASCII characters, unicode value IS the keysym + if (unicodeChar >= 0x20 && unicodeChar <= 0x7f) + return static_cast(unicodeChar); + + // For special keys, return the XKB keysym + switch (androidKeyCode) { + case AKEYCODE_ENTER: + return XKB_KEY_Return; + case AKEYCODE_DEL: + return XKB_KEY_BackSpace; + case AKEYCODE_FORWARD_DEL: + return XKB_KEY_Delete; + case AKEYCODE_ESCAPE: + return XKB_KEY_Escape; + case AKEYCODE_TAB: + return XKB_KEY_Tab; + + case AKEYCODE_DPAD_UP: + return XKB_KEY_Up; + case AKEYCODE_DPAD_DOWN: + return XKB_KEY_Down; + case AKEYCODE_DPAD_LEFT: + return XKB_KEY_Left; + case AKEYCODE_DPAD_RIGHT: + return XKB_KEY_Right; + case AKEYCODE_MOVE_HOME: + return XKB_KEY_Home; + case AKEYCODE_MOVE_END: + return XKB_KEY_End; + case AKEYCODE_PAGE_UP: + return XKB_KEY_Page_Up; + case AKEYCODE_PAGE_DOWN: + return XKB_KEY_Page_Down; + case AKEYCODE_INSERT: + return XKB_KEY_Insert; + + case AKEYCODE_SHIFT_LEFT: + return XKB_KEY_Shift_L; + case AKEYCODE_SHIFT_RIGHT: + return XKB_KEY_Shift_R; + case AKEYCODE_CTRL_LEFT: + return XKB_KEY_Control_L; + case AKEYCODE_CTRL_RIGHT: + return XKB_KEY_Control_R; + case AKEYCODE_ALT_LEFT: + return XKB_KEY_Alt_L; + case AKEYCODE_ALT_RIGHT: + return XKB_KEY_Alt_R; + case AKEYCODE_META_LEFT: + return XKB_KEY_Meta_L; + case AKEYCODE_META_RIGHT: + return XKB_KEY_Meta_R; + case AKEYCODE_CAPS_LOCK: + return XKB_KEY_Caps_Lock; + + case AKEYCODE_F1: + return XKB_KEY_F1; + case AKEYCODE_F2: + return XKB_KEY_F2; + case AKEYCODE_F3: + return XKB_KEY_F3; + case AKEYCODE_F4: + return XKB_KEY_F4; + case AKEYCODE_F5: + return XKB_KEY_F5; + case AKEYCODE_F6: + return XKB_KEY_F6; + case AKEYCODE_F7: + return XKB_KEY_F7; + case AKEYCODE_F8: + return XKB_KEY_F8; + case AKEYCODE_F9: + return XKB_KEY_F9; + case AKEYCODE_F10: + return XKB_KEY_F10; + case AKEYCODE_F11: + return XKB_KEY_F11; + case AKEYCODE_F12: + return XKB_KEY_F12; + + default: + // For unknown keys, return the unicode char if available + if (unicodeChar > 0) + return static_cast(unicodeChar); + return 0; + } +} + +/** + * Convert Android KeyEvent meta state to WPE modifiers. + */ +inline WPEModifiers androidToWpeModifiers(int metaState) +{ + uint32_t mods = 0; + if (metaState & AMETA_CTRL_ON) + mods |= WPE_MODIFIER_KEYBOARD_CONTROL; + if (metaState & AMETA_SHIFT_ON) + mods |= WPE_MODIFIER_KEYBOARD_SHIFT; + if (metaState & AMETA_ALT_ON) + mods |= WPE_MODIFIER_KEYBOARD_ALT; + if (metaState & AMETA_META_ON) + mods |= WPE_MODIFIER_KEYBOARD_META; + if (metaState & AMETA_CAPS_LOCK_ON) + mods |= WPE_MODIFIER_KEYBOARD_CAPS_LOCK; + return static_cast(mods); +} diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.cpp b/wpeview/src/main/cpp/Runtime/WKWebView.cpp index b67cd9e68..a5b5c9868 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.cpp +++ b/wpeview/src/main/cpp/Runtime/WKWebView.cpp @@ -22,6 +22,7 @@ #include "WKWebView.h" +#include "AndroidKeyMap.h" #include "Logging.h" #include "RendererSurfaceControl.h" #include "WKCallback.h" @@ -957,11 +958,30 @@ void JNIWKWebViewCache::nativeFocusOut(JNIEnv* /*env*/, jobject /*obj*/, jlong w wpe_view_focus_out(WPE_VIEW(wkWebView->wpeView())); } -void JNIWKWebViewCache::nativeOnKeyEvent(JNIEnv* /*env*/, jobject /*obj*/, jlong /*wkWebViewPtr*/, jlong /*time*/, - jint /*action*/, jint /*keyCode*/, jint /*unicodeChar*/, jint /*modifiers*/) noexcept +void JNIWKWebViewCache::nativeOnKeyEvent(JNIEnv* /*env*/, jobject /*obj*/, jlong wkWebViewPtr, jlong time, jint action, + jint keyCode, jint unicodeChar, jint modifiers) noexcept { - // Keyboard event handling will be implemented in a subsequent commit - Logging::logDebug("WKWebView::nativeOnKeyEvent() - not yet implemented [tid %d]", gettid()); + Logging::logDebug("WKWebView::nativeOnKeyEvent(action=%d, keyCode=%d, unicode=0x%04X, mods=0x%X) [tid %d]", action, + keyCode, unicodeChar, modifiers, gettid()); + + auto* wkWebView = reinterpret_cast(wkWebViewPtr); // NOLINT(performance-no-int-to-ptr) + if (wkWebView == nullptr || wkWebView->wpeView() == nullptr) + return; + + // Map Android action to WPE event type (0 = ACTION_DOWN, 1 = ACTION_UP) + WPEEventType const eventType = (action == 0) ? WPE_EVENT_KEYBOARD_KEY_DOWN : WPE_EVENT_KEYBOARD_KEY_UP; + + // Convert keycodes and modifiers + uint32_t const xkbKeycode = androidToXkbKeycode(keyCode); + uint32_t const keyval = androidToKeysym(keyCode, unicodeChar); + WPEModifiers const wpeModifiers = androidToWpeModifiers(modifiers); + + // Create and dispatch the keyboard event + WPEEvent* event = wpe_event_keyboard_new(eventType, WPE_VIEW(wkWebView->wpeView()), WPE_INPUT_SOURCE_KEYBOARD, + static_cast(time), wpeModifiers, xkbKeycode, keyval); + + wpe_view_android_dispatch_event(wkWebView->wpeView(), event); + wpe_event_unref(event); } /*********************************************************************************************************************** diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEInputConnection.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEInputConnection.java new file mode 100644 index 000000000..b1226e44e --- /dev/null +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEInputConnection.java @@ -0,0 +1,101 @@ +/** + * Copyright (C) 2025 Igalia S.L. + * Author: Felipe Erias + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.wpewebkit.wpeview; + +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.BaseInputConnection; + +import androidx.annotation.NonNull; + +import org.wpewebkit.wpe.WKWebView; + +/** + * Bridges Android IME input to WebKit's input method system. Handles text commit, deletion, + * and special keys. Note: composition text is currently treated as committed text. + */ +public class WPEInputConnection extends BaseInputConnection { + private final WKWebView wkWebView; + + public WPEInputConnection(@NonNull View targetView, boolean fullEditor, @NonNull WKWebView wkWebView) { + super(targetView, fullEditor); + this.wkWebView = wkWebView; + } + + @Override + public boolean commitText(CharSequence text, int newCursorPosition) { + if (text == null || text.length() == 0) { + return true; + } + + // This loop iterates by char position but we need to send whole codepoints to WebKit, so when we detect a + // supplementary character (emoji, etc.) we get the codepoint at that location and skip the second char. + for (int i = 0; i < text.length(); i++) { + int codePoint = Character.codePointAt(text, i); + wkWebView.setInputMethodContent(codePoint); + + if (Character.isSupplementaryCodePoint(codePoint)) { + i++; + } + } + + return true; + } + + @Override + public boolean deleteSurroundingText(int beforeLength, int afterLength) { + // Counts Java char units (supplementary chars count as 2) + if (beforeLength > 0) { + wkWebView.deleteInputMethodContent(-beforeLength, beforeLength); + } + + if (afterLength > 0) { + wkWebView.deleteInputMethodContent(0, afterLength); + } + + return true; + } + + @Override + public boolean setComposingText(CharSequence text, int newCursorPosition) { + // TODO: Implement proper preedit support via WebKit's input method context + return commitText(text, newCursorPosition); + } + + @Override + public boolean finishComposingText() { + // Nothing to do since we treat composing text as committed + return true; + } + + @Override + public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { + // Counts code points (not char units), but we delegate to deleteSurroundingText + // since we don't track text buffer state. May not handle supplementary chars correctly. + return deleteSurroundingText(beforeLength, afterLength); + } + + @Override + public boolean sendKeyEvent(@NonNull KeyEvent event) { + // Forward key events to WebKit + wkWebView.onKeyEvent(event); + return true; + } +} diff --git a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java index 42c06b479..09d6d6f9a 100644 --- a/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java +++ b/wpeview/src/main/java/org/wpewebkit/wpeview/WPEView.java @@ -25,9 +25,9 @@ import android.content.Context; import android.util.AttributeSet; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.widget.FrameLayout; import androidx.annotation.NonNull; @@ -290,6 +290,28 @@ public void evaluateJavascript(@NonNull String script, @Nullable WPECallback Date: Mon, 5 Jan 2026 14:01:15 +0800 Subject: [PATCH 6/7] Connect media and capture state signals WebKit notifications for media playback and capture state changes: - notify::is-playing-audio: Track when audio starts/stops playing - notify::is-muted: Track mute state changes - notify::camera-capture-state: Track camera capture activity - notify::microphone-capture-state: Track microphone capture activity - notify::display-capture-state: Track screen sharing activity --- wpeview/src/main/cpp/Runtime/WKWebView.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/wpeview/src/main/cpp/Runtime/WKWebView.cpp b/wpeview/src/main/cpp/Runtime/WKWebView.cpp index a5b5c9868..30c479a59 100644 --- a/wpeview/src/main/cpp/Runtime/WKWebView.cpp +++ b/wpeview/src/main/cpp/Runtime/WKWebView.cpp @@ -1067,6 +1067,16 @@ WKWebView::WKWebView(JNIEnv* env, JNIWKWebView jniWKWebView, WKWebContext* wkWeb g_signal_connect_swapped(m_webView, "notify::uri", G_CALLBACK(JNIWKWebViewCache::onUriChanged), this)); m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "notify::title", G_CALLBACK(JNIWKWebViewCache::onTitleChanged), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "notify::is-playing-audio", G_CALLBACK(JNIWKWebViewCache::onIsPlayingAudioChanged), this)); + m_signalHandlers.push_back( + g_signal_connect_swapped(m_webView, "notify::is-muted", G_CALLBACK(JNIWKWebViewCache::onIsMutedChanged), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "notify::camera-capture-state", G_CALLBACK(JNIWKWebViewCache::onCameraCaptureStateChanged), this)); + m_signalHandlers.push_back(g_signal_connect_swapped(m_webView, "notify::microphone-capture-state", + G_CALLBACK(JNIWKWebViewCache::onMicrophoneCaptureStateChanged), this)); + m_signalHandlers.push_back(g_signal_connect_swapped( + m_webView, "notify::display-capture-state", G_CALLBACK(JNIWKWebViewCache::onDisplayCaptureStateChanged), this)); m_signalHandlers.push_back( g_signal_connect_swapped(m_webView, "script-dialog", G_CALLBACK(JNIWKWebViewCache::onScriptDialog), this)); m_signalHandlers.push_back( From ea0500b403e1d343d1b89d721ff819de6175ce04 Mon Sep 17 00:00:00 2001 From: Felipe Erias Date: Fri, 2 Jan 2026 22:19:52 +0800 Subject: [PATCH 7/7] MiniBrowser: Improve navigation and UI polish Navigation improvements: - Update URL bar when page URI changes - Handle device back button to navigate browser history - Default to HTTPS when scheme is not specified - Clear focus from URL bar after navigation UI improvements: - Move progress bar above toolbar - Update UI style and colors --- .../tools/minibrowser/BrowserFragment.kt | 18 ++++++++++++++-- .../tools/minibrowser/MainActivity.kt | 13 ++++++++++++ .../org/wpewebkit/tools/minibrowser/Utils.kt | 21 +++++++++---------- .../res/drawable/address_view_background.xml | 4 ++-- .../res/drawable/ic_baseline_more_vert_24.xml | 2 +- .../res/drawable/ic_outline_settings_24.xml | 2 +- .../res/drawable/ic_round_arrow_back_24.xml | 2 +- .../drawable/ic_round_arrow_forward_24.xml | 2 +- .../main/res/drawable/ic_round_refresh_24.xml | 2 +- .../src/main/res/layout/fragment_browser.xml | 19 +++++++++-------- 10 files changed, 56 insertions(+), 29 deletions(-) diff --git a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/BrowserFragment.kt b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/BrowserFragment.kt index bc2b42d88..3e1dc4eb4 100644 --- a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/BrowserFragment.kt +++ b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/BrowserFragment.kt @@ -79,6 +79,8 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) { if (actionId == EditorInfo.IME_ACTION_DONE) { hideKeyboard() onCommit(binding.toolbarEditText.text.toString()) + binding.toolbarEditText.clearFocus() + selectedTab().webview.requestFocus() true } else { false @@ -163,6 +165,18 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) { } fullscreenView = null } + + override fun onUriChanged(view: WPEView, uri: String) { + super.onUriChanged(view, uri) + binding.toolbarEditText.setText(uri) + } + } + } + + selectedTab.webview.wpeViewClient = object : WPEViewClient() { + override fun onPageStarted(view: WPEView, url: String) { + super.onPageStarted(view, url) + binding.toolbarEditText.setText(url) } } } @@ -204,7 +218,7 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) { private fun onCommit(text: String) { val url: String = if ((text.contains(".") || text.contains(":")) && !text.contains(" ")) { - Utils.normalizeAddress(text) + normalizeAddress(text) } else { SEARCH_URI_BASE + text } @@ -217,7 +231,7 @@ class BrowserFragment : Fragment(R.layout.fragment_browser) { manager.hideSoftInputFromWindow(requireView().windowToken, 0) } - private fun selectedTab() : Tab { + internal fun selectedTab() : Tab { // For now assume we always have at least one tab val selectedTabId = browserViewModel.browserState.value.selectedTabId return browserViewModel.findTab(selectedTabId!!) diff --git a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/MainActivity.kt b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/MainActivity.kt index d8298b49e..0edab2ab7 100644 --- a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/MainActivity.kt +++ b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/MainActivity.kt @@ -53,4 +53,17 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { super.onConfigurationChanged(newConfig) Log.d(TAG, "onConfigurationChanged") } + + @Deprecated("Deprecated in superclass") + override fun onBackPressed() { + val currentFragment = navHost.childFragmentManager.fragments.firstOrNull() + if (currentFragment is BrowserFragment) { + val webView = currentFragment.selectedTab().webview + if (webView.canGoBack()) { + webView.goBack() + return + } + } + super.onBackPressed() + } } diff --git a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/Utils.kt b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/Utils.kt index 60ccb0535..5c541e6e1 100644 --- a/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/Utils.kt +++ b/tools/minibrowser/src/main/java/org/wpewebkit/tools/minibrowser/Utils.kt @@ -34,19 +34,18 @@ fun View.requestApplyStandardInsets() { requestApplyInsetsWhenAttached() } -object Utils { - private fun addressHasWebScheme(address: String) : Boolean { +// Ensures that the address has an HTTP/HTTPS scheme, adding HTTPS if missing +fun normalizeAddress(address: String): String { + // Returns true if a scheme exists and is "http" or "https" (case-insensitive) + val hasWebScheme = try { val uri = URI(address) - return uri.scheme?.let { - it == "http" - } ?: false + uri.scheme?.lowercase() in listOf("http", "https") + } catch (_: Exception) { + false } - fun normalizeAddress(address: String) : String { - return if (!addressHasWebScheme(address)) { - return "http://$address" - } else { - address - } + if (hasWebScheme) { + return address } + return "https://$address" } diff --git a/tools/minibrowser/src/main/res/drawable/address_view_background.xml b/tools/minibrowser/src/main/res/drawable/address_view_background.xml index 8f878dad5..4efab6270 100644 --- a/tools/minibrowser/src/main/res/drawable/address_view_background.xml +++ b/tools/minibrowser/src/main/res/drawable/address_view_background.xml @@ -1,5 +1,5 @@ - - + + diff --git a/tools/minibrowser/src/main/res/drawable/ic_baseline_more_vert_24.xml b/tools/minibrowser/src/main/res/drawable/ic_baseline_more_vert_24.xml index 39fbab5f7..b93355a59 100644 --- a/tools/minibrowser/src/main/res/drawable/ic_baseline_more_vert_24.xml +++ b/tools/minibrowser/src/main/res/drawable/ic_baseline_more_vert_24.xml @@ -1,4 +1,4 @@ - diff --git a/tools/minibrowser/src/main/res/drawable/ic_outline_settings_24.xml b/tools/minibrowser/src/main/res/drawable/ic_outline_settings_24.xml index c624d1973..0bd63eb72 100644 --- a/tools/minibrowser/src/main/res/drawable/ic_outline_settings_24.xml +++ b/tools/minibrowser/src/main/res/drawable/ic_outline_settings_24.xml @@ -1,4 +1,4 @@ - diff --git a/tools/minibrowser/src/main/res/drawable/ic_round_arrow_back_24.xml b/tools/minibrowser/src/main/res/drawable/ic_round_arrow_back_24.xml index 509d0e8a1..cec5fa096 100644 --- a/tools/minibrowser/src/main/res/drawable/ic_round_arrow_back_24.xml +++ b/tools/minibrowser/src/main/res/drawable/ic_round_arrow_back_24.xml @@ -1,5 +1,5 @@ diff --git a/tools/minibrowser/src/main/res/drawable/ic_round_arrow_forward_24.xml b/tools/minibrowser/src/main/res/drawable/ic_round_arrow_forward_24.xml index bdd7da8c2..c5bec9a07 100644 --- a/tools/minibrowser/src/main/res/drawable/ic_round_arrow_forward_24.xml +++ b/tools/minibrowser/src/main/res/drawable/ic_round_arrow_forward_24.xml @@ -1,5 +1,5 @@ diff --git a/tools/minibrowser/src/main/res/drawable/ic_round_refresh_24.xml b/tools/minibrowser/src/main/res/drawable/ic_round_refresh_24.xml index f4359556f..af083ca36 100644 --- a/tools/minibrowser/src/main/res/drawable/ic_round_refresh_24.xml +++ b/tools/minibrowser/src/main/res/drawable/ic_round_refresh_24.xml @@ -1,4 +1,4 @@ - diff --git a/tools/minibrowser/src/main/res/layout/fragment_browser.xml b/tools/minibrowser/src/main/res/layout/fragment_browser.xml index d0d2151de..00473d51c 100644 --- a/tools/minibrowser/src/main/res/layout/fragment_browser.xml +++ b/tools/minibrowser/src/main/res/layout/fragment_browser.xml @@ -12,13 +12,13 @@ android:id="@+id/tab_container_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_above="@id/page_progress" /> + android:layout_above="@id/toolbar" /> @@ -27,29 +27,30 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?android:actionBarSize" - android:layout_alignParentBottom="true" - android:background="?android:statusBarColor"> + android:layout_alignParentBottom="true"> + android:focusable="true"> +