Ladybird: Add WebContentService for Android port

This will let us spawn a new process for an Android Service to handle
all our WebContent needs. The ServiceConnection is manged by each
WebView. The lifecycle of the Service is not quite clear yet, but each
bindService call will get a unique Messenger that can be used to
transfer the WebContent side of the LibIPC socketpair we use in other
ports.
This commit is contained in:
Andrew Kaster 2023-09-06 16:42:16 -06:00 committed by Andrew Kaster
parent ffc0046d74
commit 6952de73dc
Notes: sideshowbarker 2024-07-17 08:34:29 +09:00
9 changed files with 334 additions and 40 deletions

View file

@ -1,41 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
android:versionCode="001"
android:versionName="head">
<supports-screens android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"/>
<application
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
android:versionCode="001"
android:versionName="head">
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true" />
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
<application
android:allowBackup="true"
android:allowNativeHeapPointerTagging="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:fullBackupOnly="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Ladybird"
tools:targetApi="33"
android:hardwareAccelerated="true"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:allowNativeHeapPointerTagging="false"
android:allowBackup="true"
android:fullBackupOnly="false"
android:enableOnBackInvokedCallback="true">
<activity
tools:targetApi="33">
<activity
android:name=".LadybirdActivity"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="unspecified"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.extract_android_style" android:value="minimal"/>
</activity>
</application>
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
</activity>
<service
android:name=".WebContentService"
android:exported="false"
android:process=":WebContent"/>
</application>
</manifest>

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <android/log.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);
}

View file

@ -4,9 +4,10 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibGfx/Painter.h>
#include <LibWebView/ViewImplementation.h>
#include <Userland/Libraries/LibGfx/Bitmap.h>
#include <Userland/Libraries/LibGfx/Painter.h>
#include <Userland/Libraries/LibWeb/Crypto/Crypto.h>
#include <Userland/Libraries/LibWebView/ViewImplementation.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <jni.h>
@ -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<void**>(&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<WebView::WebContentClient> 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<WebView::WebContentClient> 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<WebView::WebContentClient>(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<jclass>(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<jlong>(new WebViewImplementationNative);
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;
}
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<WebViewImplementationNative*>(instance);
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));
}

View file

@ -0,0 +1,70 @@
/**
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* 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<ParcelFileDescriptor>("IPC_SOCKET")!!
val fdSocket = bundle.getParcelable<ParcelFileDescriptor>("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")
}
}
}

View file

@ -0,0 +1,46 @@
/**
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* 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()
}
}

View file

@ -1,3 +1,9 @@
/**
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
*
* 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() {

View file

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

View file

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

View file

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