diff --git a/Ladybird/Android/src/main/AndroidManifest.xml b/Ladybird/Android/src/main/AndroidManifest.xml index a4fb6fed93..c48a1e683a 100644 --- a/Ladybird/Android/src/main/AndroidManifest.xml +++ b/Ladybird/Android/src/main/AndroidManifest.xml @@ -1,41 +1,54 @@ - - + + + + + + - + + - - - - - - - - + android:screenOrientation="unspecified"> + + + + + + + + + + diff --git a/Ladybird/Android/src/main/cpp/WebContentService.cpp b/Ladybird/Android/src/main/cpp/WebContentService.cpp new file mode 100644 index 0000000000..bcb3de600d --- /dev/null +++ b/Ladybird/Android/src/main/cpp/WebContentService.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_WebContentService_nativeHandleTransferSockets(JNIEnv*, jobject /* thiz */, jint ipc_socket, jint fd_passing_socket) +{ + __android_log_print(ANDROID_LOG_INFO, "WebContent", "New binding received, sockets %d and %d", ipc_socket, fd_passing_socket); + ::close(ipc_socket); + ::close(fd_passing_socket); +} diff --git a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp index c26f8dd83c..740020a490 100644 --- a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp +++ b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp @@ -4,9 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include +#include +#include +#include +#include #include #include #include @@ -23,13 +24,75 @@ Gfx::BitmapFormat to_gfx_bitmap_format(i32 f) } } +class JavaEnvironment { +public: + JavaEnvironment(JavaVM* vm) + : m_vm(vm) + { + auto ret = m_vm->GetEnv(reinterpret_cast(&m_env), JNI_VERSION_1_6); + if (ret == JNI_EDETACHED) { + ret = m_vm->AttachCurrentThread(&m_env, nullptr); + VERIFY(ret == JNI_OK); + m_did_attach_thread = true; + } else if (ret == JNI_EVERSION) { + VERIFY_NOT_REACHED(); + } else { + VERIFY(ret == JNI_OK); + } + + VERIFY(m_env != nullptr); + } + + ~JavaEnvironment() + { + if (m_did_attach_thread) + m_vm->DetachCurrentThread(); + } + + JNIEnv* get() const { return m_env; } + +private: + JavaVM* m_vm = nullptr; + JNIEnv* m_env = nullptr; + bool m_did_attach_thread = false; +}; + class WebViewImplementationNative : public WebView::ViewImplementation { public: + WebViewImplementationNative(jobject thiz) + : m_java_instance(thiz) + { + // NOTE: m_java_instance's global ref is controlled by the JNI bindings + create_client(WebView::EnableCallgrindProfiling::No); + } + virtual Gfx::IntRect viewport_rect() const override { return m_viewport_rect; } virtual Gfx::IntPoint to_content_position(Gfx::IntPoint p) const override { return p; } virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint p) const override { return p; } virtual void update_zoom() override { } + NonnullRefPtr bind_web_content_client(); + + virtual void create_client(WebView::EnableCallgrindProfiling) override + { + m_client_state = {}; + + auto new_client = bind_web_content_client(); + + m_client_state.client = new_client; + m_client_state.client->on_web_content_process_crash = [] { + __android_log_print(ANDROID_LOG_ERROR, "Ladybird", "WebContent crashed!"); + // FIXME: launch a new client + }; + + m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid()); + client().async_set_window_handle(m_client_state.client_handle); + + client().async_set_device_pixels_per_css_pixel(m_device_pixel_ratio); + + // FIXME: update_palette, update system fonts + } + void paint_into_bitmap(void* android_bitmap_raw, AndroidBitmapInfo const& info) { // Software bitmaps only for now! @@ -64,40 +127,89 @@ public: static jclass global_class_reference; static jfieldID instance_pointer_field; + static jmethodID bind_webcontent_method; + static JavaVM* global_vm; + + jobject java_instance() const { return m_java_instance; } private: + jobject m_java_instance = nullptr; Gfx::IntRect m_viewport_rect; }; jclass WebViewImplementationNative::global_class_reference; jfieldID WebViewImplementationNative::instance_pointer_field; +jmethodID WebViewImplementationNative::bind_webcontent_method; +JavaVM* WebViewImplementationNative::global_vm; + +NonnullRefPtr WebViewImplementationNative::bind_web_content_client() +{ + JavaEnvironment env(WebViewImplementationNative::global_vm); + + int socket_fds[2] {}; + MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds)); + + int ui_fd = socket_fds[0]; + int wc_fd = socket_fds[1]; + + int fd_passing_socket_fds[2] {}; + MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, fd_passing_socket_fds)); + + int ui_fd_passing_fd = fd_passing_socket_fds[0]; + int wc_fd_passing_fd = fd_passing_socket_fds[1]; + + // NOTE: The java object takes ownership of the socket fds + env.get()->CallVoidMethod(m_java_instance, bind_webcontent_method, wc_fd, wc_fd_passing_fd); + + auto socket = MUST(Core::LocalSocket::adopt_fd(ui_fd)); + MUST(socket->set_blocking(true)); + + auto new_client = make_ref_counted(move(socket), *this); + new_client->set_fd_passing_socket(MUST(Core::LocalSocket::adopt_fd(ui_fd_passing_fd))); + + return new_client; +} + } extern "C" JNIEXPORT void JNICALL Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassInit(JNIEnv* env, jobject /* thiz */) { + auto ret = env->GetJavaVM(&WebViewImplementationNative::global_vm); + if (ret != 0) + TODO(); + auto local_class = env->FindClass("org/serenityos/ladybird/WebViewImplementation"); if (!local_class) TODO(); WebViewImplementationNative::global_class_reference = reinterpret_cast(env->NewGlobalRef(local_class)); + env->DeleteLocalRef(local_class); auto field = env->GetFieldID(WebViewImplementationNative::global_class_reference, "nativeInstance", "J"); if (!field) TODO(); WebViewImplementationNative::instance_pointer_field = field; + + auto method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "bindWebContentService", "(II)V"); + if (!method) + TODO(); + WebViewImplementationNative::bind_webcontent_method = method; } extern "C" JNIEXPORT jlong JNICALL -Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv*, jobject /* thiz */) +Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv* env, jobject thiz) { - auto instance = reinterpret_cast(new WebViewImplementationNative); + auto ref = env->NewGlobalRef(thiz); + auto instance = reinterpret_cast(new WebViewImplementationNative(ref)); __android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "New WebViewImplementationNative at %p", reinterpret_cast(instance)); return instance; } extern "C" JNIEXPORT void JNICALL -Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv*, jobject /* thiz */, jlong instance) +Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv* env, jobject /* thiz */, jlong instance) { - delete reinterpret_cast(instance); + auto* impl = reinterpret_cast(instance); + env->DeleteGlobalRef(impl->java_instance()); + delete impl; __android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "Destroyed WebViewImplementationNative at %p", reinterpret_cast(instance)); } diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt new file mode 100644 index 0000000000..5b2625cf63 --- /dev/null +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +package org.serenityos.ladybird + +import android.app.Service +import android.content.Intent +import android.util.Log +import android.os.ParcelFileDescriptor +import android.os.Handler +import android.os.IBinder +import android.os.Message +import android.os.Messenger + +const val MSG_TRANSFER_SOCKETS = 1 + +class WebContentService : Service() { + private val TAG = "WebContentService" + + override fun onCreate() { + super.onCreate() + Log.i(TAG, "Creating Service") + } + + override fun onDestroy() { + super.onDestroy() + Log.i(TAG, "Destroying Service") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i(TAG, "Start command received") + return super.onStartCommand(intent, flags, startId) + } + + private fun handleTransferSockets(msg: Message) { + val bundle = msg.data + // FIXME: Handle garbage messages from wierd clients + val ipcSocket = bundle.getParcelable("IPC_SOCKET")!! + val fdSocket = bundle.getParcelable("FD_PASSING_SOCKET")!! + nativeHandleTransferSockets(ipcSocket.detachFd(), fdSocket.detachFd()) + } + + private external fun nativeHandleTransferSockets(ipcSocket: Int, fdPassingSocket: Int) + + internal class IncomingHandler( + context: WebContentService, + private val owner: WebContentService = context + ) : Handler() { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_TRANSFER_SOCKETS -> this.owner.handleTransferSockets(msg) + else -> super.handleMessage(msg) + } + } + } + + override fun onBind(p0: Intent?): IBinder? { + // FIXME: Check the intent to make sure it's legit + return Messenger(IncomingHandler(this)).binder + } + + companion object { + init { + System.loadLibrary("webcontent") + } + } +} diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentServiceConnection.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentServiceConnection.kt new file mode 100644 index 0000000000..b75a4c0184 --- /dev/null +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentServiceConnection.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +package org.serenityos.ladybird + +import android.content.ComponentName +import android.content.ServiceConnection +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.ParcelFileDescriptor + +class WebContentServiceConnection(private var ipcFd: Int, private var fdPassingFd: Int) : + ServiceConnection { + var boundToWebContent: Boolean = false + var onDisconnect: () -> Unit = {} + private var webContentService: Messenger? = null + + override fun onServiceConnected(className: ComponentName, svc: IBinder) { + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + webContentService = Messenger(svc) + boundToWebContent = true + + var msg = Message.obtain(null, MSG_TRANSFER_SOCKETS) + msg.data.putParcelable("IPC_SOCKET", ParcelFileDescriptor.adoptFd(ipcFd)) + msg.data.putParcelable("FD_PASSING_SOCKET", ParcelFileDescriptor.adoptFd(fdPassingFd)) + webContentService!!.send(msg) + } + + override fun onServiceDisconnected(className: ComponentName) { + // This is called when the connection with the service has been + // unexpectedly disconnected; that is, its process crashed. + webContentService = null + boundToWebContent = false + + // Notify owner that the service is dead + onDisconnect() + } +} diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt index f60bff6979..7a638ff400 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2023, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + package org.serenityos.ladybird import android.content.Context @@ -8,7 +14,7 @@ import android.view.View // FIXME: This should (eventually) implement NestedScrollingChild3 and ScrollingView class WebView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) { - private val viewImpl = WebViewImplementation() + private val viewImpl = WebViewImplementation(context) private lateinit var contentBitmap: Bitmap fun dispose() { diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt index b3561027b5..da8816ca09 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt @@ -6,13 +6,18 @@ package org.serenityos.ladybird +import android.content.Context +import android.content.Intent import android.graphics.Bitmap import android.util.Log /** * Wrapper around WebView::ViewImplementation for use by Kotlin */ -class WebViewImplementation { +class WebViewImplementation( + context: Context, + private var appContext: Context = context.applicationContext +) { // Instance Pointer to native object, very unsafe :) private var nativeInstance = nativeObjectInit() @@ -36,6 +41,20 @@ class WebViewImplementation { nativeSetViewportGeometry(nativeInstance, w, h) } + fun bindWebContentService(ipcFd: Int, fdPassingFd: Int) { + var connector = WebContentServiceConnection(ipcFd, fdPassingFd) + connector.onDisconnect = { + // FIXME: Notify impl that service is dead and might need restarted + Log.e("WebContentView", "WebContent Died! :(") + } + // FIXME: Unbind this at some point maybe + appContext.bindService( + Intent(appContext, WebContentService::class.java), + connector, + Context.BIND_AUTO_CREATE + ) + } + private external fun nativeObjectInit(): Long private external fun nativeObjectDispose(instance: Long) diff --git a/Ladybird/WebContent/CMakeLists.txt b/Ladybird/WebContent/CMakeLists.txt index d28dcbca60..5654bdc5f4 100644 --- a/Ladybird/WebContent/CMakeLists.txt +++ b/Ladybird/WebContent/CMakeLists.txt @@ -29,8 +29,11 @@ if (ENABLE_QT) target_link_libraries(WebContent PRIVATE Qt::Core Qt::Network Qt::Multimedia) target_compile_definitions(WebContent PRIVATE HAVE_QT=1) else() - # FIXME: Remove when chromes are upstreamed - add_library(webcontent STATIC ${WEBCONTENT_SOURCES}) + set(LIB_TYPE STATIC) + if (ANDROID) + set(LIB_TYPE SHARED) + endif() + add_library(webcontent ${LIB_TYPE} ${WEBCONTENT_SOURCES}) target_include_directories(webcontent PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/) target_include_directories(webcontent PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..) target_link_libraries(webcontent PRIVATE LibAudio LibCore LibFileSystem LibGfx LibIPC LibJS LibMain LibWeb LibWebSocket LibProtocol LibWebView) @@ -49,6 +52,11 @@ else() ${WEBCONTENT_SOURCE_DIR}/WebDriverConnection.h ) + if (ANDROID) + target_sources(webcontent PRIVATE ../Android/src/main/cpp/WebContentService.cpp) + target_link_libraries(webcontent PRIVATE log) + endif() + add_executable(WebContent main.cpp) target_link_libraries(WebContent PRIVATE webcontent) endif() diff --git a/Ladybird/cmake/InstallRules.cmake b/Ladybird/cmake/InstallRules.cmake index 237cab15a6..c428b17fbc 100644 --- a/Ladybird/cmake/InstallRules.cmake +++ b/Ladybird/cmake/InstallRules.cmake @@ -38,7 +38,10 @@ list(REMOVE_ITEM all_required_lagom_libraries ladybird) # Install webcontent impl library if it exists if (TARGET webcontent) - list(APPEND all_required_lagom_libraries webcontent) + get_target_property(target_type webcontent TYPE) + if ("${target_type}" STREQUAL STATIC_LIBRARY) + list(APPEND all_required_lagom_libraries webcontent) + endif() endif() install(TARGETS ${all_required_lagom_libraries}