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.
This commit is contained in:
Andrew Kaster 2023-09-13 23:22:44 -06:00 committed by Andrew Kaster
parent bd131c0bf8
commit a58ee0ecd2
8 changed files with 235 additions and 43 deletions

View file

@ -5,35 +5,34 @@
*/
#include "ALooperEventLoopImplementation.h"
#include <AK/Format.h>
#include <AK/OwnPtr.h>
#include <Ladybird/Utilities.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <android/log.h>
#include <jni.h>
OwnPtr<Core::EventLoop> s_main_event_loop;
RefPtr<Core::Timer> 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<Core::EventLoop>();
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

View file

@ -4,17 +4,101 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <android/log.h>
#include <AK/LexicalPath.h>
#include <Ladybird/FontPlugin.h>
#include <Ladybird/HelperProcess.h>
#include <Ladybird/ImageCodecPlugin.h>
#include <Ladybird/Utilities.h>
#include <LibAudio/Loader.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/LocalServer.h>
#include <LibCore/System.h>
#include <LibIPC/ConnectionFromClient.h>
#include <LibJS/Bytecode/Interpreter.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Loader/FrameLoader.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/Platform/AudioCodecPluginAgnostic.h>
#include <LibWeb/Platform/EventLoopPluginSerenity.h>
#include <LibWebView/RequestServerAdapter.h>
#include <LibWebView/WebSocketClientAdapter.h>
#include <WebContent/ConnectionFromClient.h>
#include <WebContent/PageHost.h>
#include <jni.h>
#include <unistd.h>
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<Web::ResourceLoaderConnectorRequest> start_request(DeprecatedString const&, AK::URL const&, HashMap<DeprecatedString, DeprecatedString> const&, ReadonlyBytes, Core::ProxyData const&) override
{
return nullptr;
}
};
ErrorOr<int> 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<NullResourceConnector>());
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<bool> 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);
}

View file

@ -10,7 +10,6 @@
#include <Userland/Libraries/LibWeb/Crypto/Crypto.h>
#include <Userland/Libraries/LibWebView/ViewImplementation.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <jni.h>
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<WebView::WebContentClient> 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<jlong>(new WebViewImplementationNative(ref));
__android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "New WebViewImplementationNative at %p", reinterpret_cast<void*>(instance));
return instance;
}
@ -178,7 +197,6 @@ Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv* e
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
env->DeleteGlobalRef(impl->java_instance());
delete impl;
__android_log_print(ANDROID_LOG_DEBUG, "Ladybird", "Destroyed WebViewImplementationNative at %p", reinterpret_cast<void*>(instance));
}
extern "C" JNIEXPORT void JNICALL
@ -202,3 +220,20 @@ Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNI
auto* impl = reinterpret_cast<WebViewImplementationNative*>(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<WebViewImplementationNative*>(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<WebViewImplementationNative*>(instance);
impl->set_device_pixel_ratio(ratio);
}

View file

@ -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

View file

@ -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<ParcelFileDescriptor>("IPC_SOCKET")!!
val fdSocket = bundle.getParcelable<ParcelFileDescriptor>("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")

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {
/*