From a58ee0ecd2d1547fe7ecfc4c257716dae86590b9 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Wed, 13 Sep 2023 23:22:44 -0600 Subject: [PATCH] Ladybird/Android: Implement enough of WebContent to load local files The local files can only be loaded by calling loadURL on the WebView, but it's a start. --- .../Android/src/main/cpp/LadybirdActivity.cpp | 17 ++- .../src/main/cpp/WebContentService.cpp | 102 ++++++++++++++++-- .../main/cpp/WebViewImplementationNative.cpp | 43 +++++++- .../serenityos/ladybird/LadybirdActivity.kt | 18 +++- .../serenityos/ladybird/WebContentService.kt | 27 ++++- .../ladybird/WebContentServiceConnection.kt | 12 ++- .../java/org/serenityos/ladybird/WebView.kt | 15 ++- .../ladybird/WebViewImplementation.kt | 44 +++++--- 8 files changed, 235 insertions(+), 43 deletions(-) diff --git a/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp b/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp index 29722f75b0..97c499243d 100644 --- a/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp +++ b/Ladybird/Android/src/main/cpp/LadybirdActivity.cpp @@ -5,35 +5,34 @@ */ #include "ALooperEventLoopImplementation.h" +#include #include #include #include #include -#include #include OwnPtr s_main_event_loop; -RefPtr s_timer; extern "C" JNIEXPORT void JNICALL -Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir, jobject timer_service) +Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir, jstring tag_name, jobject timer_service) { char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr); s_serenity_resource_root = raw_resource_dir; - __android_log_print(ANDROID_LOG_INFO, "Ladybird", "Serenity resource dir is %s", s_serenity_resource_root.characters()); env->ReleaseStringUTFChars(resource_dir, raw_resource_dir); + char const* raw_tag_name = env->GetStringUTFChars(tag_name, nullptr); + AK::set_log_tag_name(raw_tag_name); + env->ReleaseStringUTFChars(tag_name, raw_tag_name); + + dbgln("Set resource dir to {}", s_serenity_resource_root); + jobject timer_service_ref = env->NewGlobalRef(timer_service); JavaVM* vm = nullptr; jint ret = env->GetJavaVM(&vm); VERIFY(ret == 0); Core::EventLoopManager::install(*new Ladybird::ALooperEventLoopManager(vm, timer_service_ref)); s_main_event_loop = make(); - - s_timer = MUST(Core::Timer::create_repeating(1000, [] { - __android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "EventLoop is alive!"); - })); - s_timer->start(); } extern "C" JNIEXPORT void JNICALL diff --git a/Ladybird/Android/src/main/cpp/WebContentService.cpp b/Ladybird/Android/src/main/cpp/WebContentService.cpp index 5e0e85e383..ae2e9b2778 100644 --- a/Ladybird/Android/src/main/cpp/WebContentService.cpp +++ b/Ladybird/Android/src/main/cpp/WebContentService.cpp @@ -4,17 +4,101 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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); +class NullResourceConnector : public Web::ResourceLoaderConnector { + virtual void prefetch_dns(AK::URL const&) override { } + virtual void preconnect(AK::URL const&) override { } - // FIXME: Create a new thread to start WebContent processing - // Make sure to create IPC sockets *in that thread*! + virtual RefPtr start_request(DeprecatedString const&, AK::URL const&, HashMap const&, ReadonlyBytes, Core::ProxyData const&) override + { + return nullptr; + } +}; + +ErrorOr web_content_main(int ipc_socket, int fd_passing_socket) +{ + Core::EventLoop event_loop; + + Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity); + Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPlugin); + + Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) { + (void)loader; + return Error::from_string_literal("Don't know how to initialize audio in this configuration!"); + }); + + Web::FrameLoader::set_default_favicon_path(DeprecatedString::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root)); + + Web::ResourceLoader::initialize(make_ref_counted()); + + bool is_layout_test_mode = false; + + Web::HTML::Window::set_internals_object_exposed(is_layout_test_mode); + Web::Platform::FontPlugin::install(*new Ladybird::FontPlugin(is_layout_test_mode)); + + Web::FrameLoader::set_resource_directory_url(DeprecatedString::formatted("file://{}/res", s_serenity_resource_root)); + Web::FrameLoader::set_error_page_url(DeprecatedString::formatted("file://{}/res/html/error.html", s_serenity_resource_root)); + Web::FrameLoader::set_directory_page_url(DeprecatedString::formatted("file://{}/res/html/directory.html", s_serenity_resource_root)); + + TRY(Web::Bindings::initialize_main_thread_vm()); + + auto webcontent_socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket)); + auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(move(webcontent_socket))); + webcontent_client->set_fd_passing_socket(TRY(Core::LocalSocket::adopt_fd(fd_passing_socket))); + + return event_loop.exec(); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_WebContentService_nativeThreadLoop(JNIEnv*, jobject /* thiz */, jint ipc_socket, jint fd_passing_socket) +{ + dbgln("New binding received, sockets {} and {}", ipc_socket, fd_passing_socket); + auto ret = web_content_main(ipc_socket, fd_passing_socket); + if (ret.is_error()) { + warnln("Runtime Error: {}", ret.release_error()); + } else { + outln("Thread exited with code {}", ret.release_value()); + } +} + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_WebContentService_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir, jstring tag_name) +{ + static Atomic s_initialized_flag { false }; + if (s_initialized_flag.exchange(true) == true) { + // Skip initializing if someone else already started the process at some point in the past + return; + } + + char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr); + s_serenity_resource_root = raw_resource_dir; + env->ReleaseStringUTFChars(resource_dir, raw_resource_dir); + + char const* raw_tag_name = env->GetStringUTFChars(tag_name, nullptr); + AK::set_log_tag_name(raw_tag_name); + env->ReleaseStringUTFChars(tag_name, raw_tag_name); } diff --git a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp index 23c4fe7220..5d1e6fc340 100644 --- a/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp +++ b/Ladybird/Android/src/main/cpp/WebViewImplementationNative.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include namespace { @@ -32,6 +31,11 @@ public: { // NOTE: m_java_instance's global ref is controlled by the JNI bindings create_client(WebView::EnableCallgrindProfiling::No); + + on_ready_to_paint = [this]() { + JavaEnvironment env(global_vm); + env.get()->CallVoidMethod(m_java_instance, invalidate_layout_method); + }; } virtual Gfx::IntRect viewport_rect() const override { return m_viewport_rect; } @@ -49,7 +53,7 @@ public: m_client_state.client = new_client; m_client_state.client->on_web_content_process_crash = [] { - __android_log_print(ANDROID_LOG_ERROR, "Ladybird", "WebContent crashed!"); + warnln("WebContent crashed!"); // FIXME: launch a new client }; @@ -91,11 +95,21 @@ public: void set_viewport_geometry(int w, int h) { m_viewport_rect = { { 0, 0 }, { w, h } }; + client().async_set_viewport_rect(m_viewport_rect); + request_repaint(); + handle_resize(); + } + + void set_device_pixel_ratio(float f) + { + m_device_pixel_ratio = f; + client().async_set_device_pixels_per_css_pixel(m_device_pixel_ratio); } static jclass global_class_reference; static jfieldID instance_pointer_field; static jmethodID bind_webcontent_method; + static jmethodID invalidate_layout_method; static JavaVM* global_vm; jobject java_instance() const { return m_java_instance; } @@ -107,6 +121,7 @@ private: jclass WebViewImplementationNative::global_class_reference; jfieldID WebViewImplementationNative::instance_pointer_field; jmethodID WebViewImplementationNative::bind_webcontent_method; +jmethodID WebViewImplementationNative::invalidate_layout_method; JavaVM* WebViewImplementationNative::global_vm; NonnullRefPtr WebViewImplementationNative::bind_web_content_client() @@ -161,6 +176,11 @@ Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassIni if (!method) TODO(); WebViewImplementationNative::bind_webcontent_method = method; + + method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "invalidateLayout", "()V"); + if (!method) + TODO(); + WebViewImplementationNative::invalidate_layout_method = method; } extern "C" JNIEXPORT jlong JNICALL @@ -168,7 +188,6 @@ Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv* env, { 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; } @@ -178,7 +197,6 @@ Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv* e 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)); } extern "C" JNIEXPORT void JNICALL @@ -202,3 +220,20 @@ Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNI auto* impl = reinterpret_cast(instance); impl->set_viewport_geometry(w, h); } + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_WebViewImplementation_nativeLoadURL(JNIEnv* env, jobject /* thiz */, jlong instance, jstring url) +{ + auto* impl = reinterpret_cast(instance); + char const* raw_url = env->GetStringUTFChars(url, nullptr); + auto ak_url = AK::URL::create_with_url_or_path(StringView { raw_url, strlen(raw_url) }); + env->ReleaseStringUTFChars(url, raw_url); + impl->load(ak_url); +} + +extern "C" JNIEXPORT void JNICALL +Java_org_serenityos_ladybird_WebViewImplementation_nativeSetDevicePixelRatio(JNIEnv*, jobject /* thiz */, jlong instance, jfloat ratio) +{ + auto* impl = reinterpret_cast(instance); + impl->set_device_pixel_ratio(ratio); +} diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt index 79e287a712..de5eeb5deb 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/LadybirdActivity.kt @@ -9,6 +9,7 @@ package org.serenityos.ladybird import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import org.serenityos.ladybird.databinding.ActivityMainBinding +import kotlin.io.path.Path class LadybirdActivity : AppCompatActivity() { @@ -19,18 +20,27 @@ class LadybirdActivity : AppCompatActivity() { super.onCreate(savedInstanceState) resourceDir = TransferAssets.transferAssets(this) - initNativeCode(resourceDir, timerService) + initNativeCode(resourceDir, "Ladybird", timerService) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) view = binding.webView + view.initialize(resourceDir) mainExecutor.execute { callNativeEventLoopForever() } } + override fun onStart() { + super.onStart() + + // FIXME: This is not the right place to load the homepage :^) + val initialURL = Path(resourceDir, "res/html/misc/welcome.html").toUri().toURL() + view.loadURL(initialURL) + } + override fun onDestroy() { view.dispose() super.onDestroy() @@ -43,7 +53,11 @@ class LadybirdActivity : AppCompatActivity() { * A native method that is implemented by the 'ladybird' native library, * which is packaged with this application. */ - private external fun initNativeCode(resourceDir: String, timerService: TimerExecutorService) + private external fun initNativeCode( + resourceDir: String, + tag: String, + timerService: TimerExecutorService + ) // FIXME: Instead of doing this, can we push a message to the message queue of the java Looper // when an event is pushed to the main thread, and use that to clear out the diff --git a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt index 5b2625cf63..f8230d14b3 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentService.kt @@ -14,12 +14,17 @@ import android.os.Handler import android.os.IBinder import android.os.Message import android.os.Messenger +import java.util.concurrent.Executors -const val MSG_TRANSFER_SOCKETS = 1 +const val MSG_SET_RESOURCE_ROOT = 1 +const val MSG_TRANSFER_SOCKETS = 2 class WebContentService : Service() { private val TAG = "WebContentService" + private val threadPool = Executors.newCachedThreadPool() + private lateinit var resourceDir: String + override fun onCreate() { super.onCreate() Log.i(TAG, "Creating Service") @@ -40,10 +45,15 @@ class WebContentService : Service() { // 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()) + createThread(ipcSocket, fdSocket) } - private external fun nativeHandleTransferSockets(ipcSocket: Int, fdPassingSocket: Int) + private fun handleSetResourceRoot(msg: Message) { + // FIXME: Handle this being already set, not being present, etc + resourceDir = msg.data.getString("PATH")!! + + initNativeCode(resourceDir, TAG) + } internal class IncomingHandler( context: WebContentService, @@ -52,6 +62,7 @@ class WebContentService : Service() { override fun handleMessage(msg: Message) { when (msg.what) { MSG_TRANSFER_SOCKETS -> this.owner.handleTransferSockets(msg) + MSG_SET_RESOURCE_ROOT -> this.owner.handleSetResourceRoot(msg) else -> super.handleMessage(msg) } } @@ -62,6 +73,16 @@ class WebContentService : Service() { return Messenger(IncomingHandler(this)).binder } + private external fun nativeThreadLoop(ipcSocket: Int, fdPassingSocket: Int) + + private fun createThread(ipcSocket: ParcelFileDescriptor, fdSocket: ParcelFileDescriptor) { + threadPool.execute { + nativeThreadLoop(ipcSocket.detachFd(), fdSocket.detachFd()) + } + } + + private external fun initNativeCode(resourceDir: String, tagName: String); + 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 index b75a4c0184..ddb5f1c35d 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentServiceConnection.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebContentServiceConnection.kt @@ -13,7 +13,11 @@ import android.os.Message import android.os.Messenger import android.os.ParcelFileDescriptor -class WebContentServiceConnection(private var ipcFd: Int, private var fdPassingFd: Int) : +class WebContentServiceConnection( + private var ipcFd: Int, + private var fdPassingFd: Int, + private var resourceDir: String +) : ServiceConnection { var boundToWebContent: Boolean = false var onDisconnect: () -> Unit = {} @@ -28,7 +32,11 @@ class WebContentServiceConnection(private var ipcFd: Int, private var fdPassingF webContentService = Messenger(svc) boundToWebContent = true - var msg = Message.obtain(null, MSG_TRANSFER_SOCKETS) + val init = Message.obtain(null, MSG_SET_RESOURCE_ROOT) + init.data.putString("PATH", resourceDir) + webContentService!!.send(init) + + val 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) 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 7a638ff400..02a2200d56 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebView.kt @@ -11,19 +11,32 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.util.AttributeSet import android.view.View +import java.net.URL // FIXME: This should (eventually) implement NestedScrollingChild3 and ScrollingView class WebView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) { - private val viewImpl = WebViewImplementation(context) + private val viewImpl = WebViewImplementation(this) private lateinit var contentBitmap: Bitmap + fun initialize(resourceDir: String) { + viewImpl.initialize(resourceDir) + } + fun dispose() { viewImpl.dispose() } + fun loadURL(url: URL) { + viewImpl.loadURL(url) + } + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) contentBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + + val pixelDensity = context.resources.displayMetrics.density + viewImpl.setDevicePixelRatio(pixelDensity) + // FIXME: Account for scroll offset when view supports scrolling viewImpl.setViewportGeometry(w, h) } 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 da8816ca09..cc399f7c1d 100644 --- a/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt +++ b/Ladybird/Android/src/main/java/org/serenityos/ladybird/WebViewImplementation.kt @@ -8,24 +8,24 @@ package org.serenityos.ladybird import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.graphics.Bitmap import android.util.Log +import android.view.View +import java.net.URL /** * Wrapper around WebView::ViewImplementation for use by Kotlin */ -class WebViewImplementation( - context: Context, - private var appContext: Context = context.applicationContext -) { +class WebViewImplementation(private val view: WebView) { // Instance Pointer to native object, very unsafe :) - private var nativeInstance = nativeObjectInit() + private var nativeInstance: Long = 0 + private lateinit var resourceDir: String + private lateinit var connection: ServiceConnection - init { - Log.d( - "Ladybird", - "New WebViewImplementation (Kotlin) with nativeInstance ${this.nativeInstance}" - ) + fun initialize(resourceDir: String) { + this.resourceDir = resourceDir + nativeInstance = nativeObjectInit() } fun dispose() { @@ -33,6 +33,10 @@ class WebViewImplementation( nativeInstance = 0 } + fun loadURL(url: URL) { + nativeLoadURL(nativeInstance, url.toString()) + } + fun drawIntoBitmap(bitmap: Bitmap) { nativeDrawIntoBitmap(nativeInstance, bitmap) } @@ -41,25 +45,39 @@ class WebViewImplementation( nativeSetViewportGeometry(nativeInstance, w, h) } + fun setDevicePixelRatio(ratio: Float) { + nativeSetDevicePixelRatio(nativeInstance, ratio) + } + + // Functions called from native code fun bindWebContentService(ipcFd: Int, fdPassingFd: Int) { - var connector = WebContentServiceConnection(ipcFd, fdPassingFd) + val connector = WebContentServiceConnection(ipcFd, fdPassingFd, resourceDir) 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), + view.context.bindService( + Intent(view.context, WebContentService::class.java), connector, Context.BIND_AUTO_CREATE ) + connection = connector } + fun invalidateLayout() { + view.requestLayout() + view.invalidate() + } + + // Functions implemented in native code private external fun nativeObjectInit(): Long private external fun nativeObjectDispose(instance: Long) private external fun nativeDrawIntoBitmap(instance: Long, bitmap: Bitmap) private external fun nativeSetViewportGeometry(instance: Long, w: Int, h: Int) + private external fun nativeSetDevicePixelRatio(instance: Long, ratio: Float) + private external fun nativeLoadURL(instance: Long, url: String) companion object { /*