Everywhere: Move the Ladybird folder to UI
Author: https://github.com/trflynn89
Commit: db47cc41f8
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2256
Reviewed-by: https://github.com/sideshowbarker
|
@ -0,0 +1,40 @@
|
|||
package org.serenityos.ladybird
|
||||
|
||||
import androidx.test.ext.junit.rules.activityScenarioRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmokeTest {
|
||||
|
||||
@get:Rule
|
||||
var activityScenarioRule = activityScenarioRule<LadybirdActivity>()
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("org.serenityos.ladybird", appContext.packageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadWebView() {
|
||||
// We can actually load a web view, and it is visible
|
||||
onView(withId(R.id.web_view)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
66
UI/Android/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?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" />
|
||||
|
||||
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<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">
|
||||
<activity
|
||||
android:name=".LadybirdActivity"
|
||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
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:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":WebContent" />
|
||||
<service
|
||||
android:name=".RequestServerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":RequestServer" />
|
||||
<service
|
||||
android:name=".ImageDecoderService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":ImageDecoder" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
262
UI/Android/src/main/cpp/ALooperEventLoopImplementation.cpp
Normal file
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <android/log.h>
|
||||
#include <android/looper.h>
|
||||
#include <fcntl.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
EventLoopThreadData& EventLoopThreadData::the()
|
||||
{
|
||||
static thread_local EventLoopThreadData s_thread_data { {}, {}, &Core::ThreadEventQueue::current() };
|
||||
return s_thread_data;
|
||||
}
|
||||
|
||||
static ALooperEventLoopImplementation& current_impl()
|
||||
{
|
||||
return verify_cast<ALooperEventLoopImplementation>(Core::EventLoop::current().impl());
|
||||
}
|
||||
|
||||
static int looper_callback(int fd, int events, void* data);
|
||||
|
||||
ALooperEventLoopManager::ALooperEventLoopManager(jobject timer_service)
|
||||
: m_timer_service(timer_service)
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
|
||||
jclass timer_class = env.get()->FindClass("org/serenityos/ladybird/TimerExecutorService$Timer");
|
||||
if (!timer_class)
|
||||
TODO();
|
||||
m_timer_class = reinterpret_cast<jclass>(env.get()->NewGlobalRef(timer_class));
|
||||
env.get()->DeleteLocalRef(timer_class);
|
||||
|
||||
m_timer_constructor = env.get()->GetMethodID(m_timer_class, "<init>", "(J)V");
|
||||
if (!m_timer_constructor)
|
||||
TODO();
|
||||
|
||||
jclass timer_service_class = env.get()->GetObjectClass(m_timer_service);
|
||||
|
||||
m_register_timer = env.get()->GetMethodID(timer_service_class, "registerTimer", "(Lorg/serenityos/ladybird/TimerExecutorService$Timer;ZJ)J");
|
||||
if (!m_register_timer)
|
||||
TODO();
|
||||
|
||||
m_unregister_timer = env.get()->GetMethodID(timer_service_class, "unregisterTimer", "(J)V");
|
||||
if (!m_unregister_timer)
|
||||
TODO();
|
||||
env.get()->DeleteLocalRef(timer_service_class);
|
||||
|
||||
auto ret = pipe2(m_pipe, O_CLOEXEC | O_NONBLOCK);
|
||||
VERIFY(ret == 0);
|
||||
|
||||
m_main_looper = ALooper_forThread();
|
||||
VERIFY(m_main_looper);
|
||||
ALooper_acquire(m_main_looper);
|
||||
|
||||
ret = ALooper_addFd(m_main_looper, m_pipe[0], ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, &looper_callback, this);
|
||||
VERIFY(ret == 1);
|
||||
}
|
||||
|
||||
ALooperEventLoopManager::~ALooperEventLoopManager()
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
|
||||
env.get()->DeleteGlobalRef(m_timer_service);
|
||||
env.get()->DeleteGlobalRef(m_timer_class);
|
||||
|
||||
ALooper_removeFd(m_main_looper, m_pipe[0]);
|
||||
ALooper_release(m_main_looper);
|
||||
|
||||
::close(m_pipe[0]);
|
||||
::close(m_pipe[1]);
|
||||
}
|
||||
|
||||
NonnullOwnPtr<Core::EventLoopImplementation> ALooperEventLoopManager::make_implementation()
|
||||
{
|
||||
return ALooperEventLoopImplementation::create();
|
||||
}
|
||||
|
||||
intptr_t ALooperEventLoopManager::register_timer(Core::EventReceiver& receiver, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible visibility)
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
auto& thread_data = EventLoopThreadData::the();
|
||||
|
||||
auto timer = env.get()->NewObject(m_timer_class, m_timer_constructor, reinterpret_cast<long>(¤t_impl()));
|
||||
|
||||
long millis = milliseconds;
|
||||
long timer_id = env.get()->CallLongMethod(m_timer_service, m_register_timer, timer, !should_reload, millis);
|
||||
|
||||
// FIXME: Is there a race condition here? Maybe we should take a lock on the timers...
|
||||
thread_data.timers.set(timer_id, { receiver.make_weak_ptr(), visibility });
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::unregister_timer(intptr_t timer_id)
|
||||
{
|
||||
if (auto timer = EventLoopThreadData::the().timers.take(timer_id); timer.has_value()) {
|
||||
JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(m_timer_service, m_unregister_timer, timer_id);
|
||||
}
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
EventLoopThreadData::the().notifiers.set(¬ifier);
|
||||
current_impl().register_notifier(notifier);
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
EventLoopThreadData::the().notifiers.remove(¬ifier);
|
||||
current_impl().unregister_notifier(notifier);
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::did_post_event()
|
||||
{
|
||||
int msg = 0xCAFEBABE;
|
||||
(void)write(m_pipe[1], &msg, sizeof(msg));
|
||||
}
|
||||
|
||||
int looper_callback(int fd, int events, void* data)
|
||||
{
|
||||
auto& manager = *static_cast<ALooperEventLoopManager*>(data);
|
||||
|
||||
if (events & ALOOPER_EVENT_INPUT) {
|
||||
int msg = 0;
|
||||
while (read(fd, &msg, sizeof(msg)) == sizeof(msg)) {
|
||||
// Do nothing, we don't actually care what the message was, just that it was posted
|
||||
}
|
||||
manager.on_did_post_event();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
ALooperEventLoopImplementation::ALooperEventLoopImplementation()
|
||||
: m_event_loop(ALooper_prepare(0))
|
||||
, m_thread_data(&EventLoopThreadData::the())
|
||||
{
|
||||
ALooper_acquire(m_event_loop);
|
||||
}
|
||||
|
||||
ALooperEventLoopImplementation::~ALooperEventLoopImplementation()
|
||||
{
|
||||
ALooper_release(m_event_loop);
|
||||
}
|
||||
|
||||
EventLoopThreadData& ALooperEventLoopImplementation::thread_data()
|
||||
{
|
||||
return *m_thread_data;
|
||||
}
|
||||
|
||||
int ALooperEventLoopImplementation::exec()
|
||||
{
|
||||
while (!m_exit_requested.load(MemoryOrder::memory_order_acquire))
|
||||
pump(PumpMode::WaitForEvents);
|
||||
return m_exit_code;
|
||||
}
|
||||
|
||||
size_t ALooperEventLoopImplementation::pump(Core::EventLoopImplementation::PumpMode mode)
|
||||
{
|
||||
auto num_events = Core::ThreadEventQueue::current().process();
|
||||
|
||||
int timeout_ms = mode == Core::EventLoopImplementation::PumpMode::WaitForEvents ? -1 : 0;
|
||||
int ret;
|
||||
do {
|
||||
ret = ALooper_pollOnce(timeout_ms, nullptr, nullptr, nullptr);
|
||||
} while (ret == ALOOPER_POLL_CALLBACK);
|
||||
|
||||
// We don't expect any non-callback FDs to be ready
|
||||
VERIFY(ret <= 0);
|
||||
|
||||
if (ret == ALOOPER_POLL_ERROR)
|
||||
m_exit_requested.store(true, MemoryOrder::memory_order_release);
|
||||
|
||||
num_events += Core::ThreadEventQueue::current().process();
|
||||
return num_events;
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::quit(int code)
|
||||
{
|
||||
m_exit_code = code;
|
||||
m_exit_requested.store(true, MemoryOrder::memory_order_release);
|
||||
wake();
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::wake()
|
||||
{
|
||||
ALooper_wake(m_event_loop);
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&& event)
|
||||
{
|
||||
m_thread_event_queue.post_event(receiver, move(event));
|
||||
|
||||
if (&m_thread_event_queue != &Core::ThreadEventQueue::current())
|
||||
wake();
|
||||
}
|
||||
|
||||
static int notifier_callback(int fd, int events, void* data)
|
||||
{
|
||||
auto& notifier = *static_cast<Core::Notifier*>(data);
|
||||
|
||||
VERIFY(fd == notifier.fd());
|
||||
|
||||
Core::NotificationType type = Core::NotificationType::None;
|
||||
if (events & ALOOPER_EVENT_INPUT)
|
||||
type |= Core::NotificationType::Read;
|
||||
if (events & ALOOPER_EVENT_OUTPUT)
|
||||
type |= Core::NotificationType::Write;
|
||||
if (events & ALOOPER_EVENT_HANGUP)
|
||||
type |= Core::NotificationType::HangUp;
|
||||
if (events & ALOOPER_EVENT_ERROR)
|
||||
type |= Core::NotificationType::Error;
|
||||
|
||||
Core::NotifierActivationEvent event(notifier.fd(), type);
|
||||
notifier.dispatch_event(event);
|
||||
|
||||
// Wake up from ALooper_pollAll, and service this event on the event queue
|
||||
current_impl().wake();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
auto event_flags = 0;
|
||||
switch (notifier.type()) {
|
||||
case Core::Notifier::Type::Read:
|
||||
event_flags = ALOOPER_EVENT_INPUT;
|
||||
break;
|
||||
case Core::Notifier::Type::Write:
|
||||
event_flags = ALOOPER_EVENT_OUTPUT;
|
||||
break;
|
||||
case Core::Notifier::Type::Error:
|
||||
event_flags = ALOOPER_EVENT_ERROR;
|
||||
break;
|
||||
case Core::Notifier::Type::HangUp:
|
||||
event_flags = ALOOPER_EVENT_HANGUP;
|
||||
break;
|
||||
case Core::Notifier::Type::None:
|
||||
TODO();
|
||||
}
|
||||
|
||||
auto ret = ALooper_addFd(m_event_loop, notifier.fd(), ALOOPER_POLL_CALLBACK, event_flags, ¬ifier_callback, ¬ifier);
|
||||
VERIFY(ret == 1);
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
ALooper_removeFd(m_event_loop, notifier.fd());
|
||||
}
|
||||
|
||||
}
|
96
UI/Android/src/main/cpp/ALooperEventLoopImplementation.h
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Atomic.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibCore/EventLoopImplementation.h>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" struct ALooper;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class ALooperEventLoopManager : public Core::EventLoopManager {
|
||||
public:
|
||||
ALooperEventLoopManager(jobject timer_service);
|
||||
virtual ~ALooperEventLoopManager() override;
|
||||
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
|
||||
|
||||
virtual intptr_t register_timer(Core::EventReceiver&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
|
||||
virtual void unregister_timer(intptr_t timer_id) override;
|
||||
|
||||
virtual void register_notifier(Core::Notifier&) override;
|
||||
virtual void unregister_notifier(Core::Notifier&) override;
|
||||
|
||||
virtual void did_post_event() override;
|
||||
|
||||
Function<void()> on_did_post_event;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual int register_signal(int, Function<void(int)>) override { return 0; }
|
||||
virtual void unregister_signal(int) override { }
|
||||
|
||||
private:
|
||||
int m_pipe[2] = {};
|
||||
ALooper* m_main_looper { nullptr };
|
||||
jobject m_timer_service { nullptr };
|
||||
jmethodID m_register_timer { nullptr };
|
||||
jmethodID m_unregister_timer { nullptr };
|
||||
jclass m_timer_class { nullptr };
|
||||
jmethodID m_timer_constructor { nullptr };
|
||||
};
|
||||
|
||||
struct TimerData {
|
||||
WeakPtr<Core::EventReceiver> receiver;
|
||||
Core::TimerShouldFireWhenNotVisible visibility;
|
||||
};
|
||||
|
||||
struct EventLoopThreadData {
|
||||
static EventLoopThreadData& the();
|
||||
|
||||
HashMap<long, TimerData> timers;
|
||||
HashTable<Core::Notifier*> notifiers;
|
||||
Core::ThreadEventQueue* thread_queue = nullptr;
|
||||
};
|
||||
|
||||
class ALooperEventLoopImplementation : public Core::EventLoopImplementation {
|
||||
public:
|
||||
static NonnullOwnPtr<ALooperEventLoopImplementation> create() { return adopt_own(*new ALooperEventLoopImplementation); }
|
||||
|
||||
virtual ~ALooperEventLoopImplementation() override;
|
||||
|
||||
virtual int exec() override;
|
||||
virtual size_t pump(PumpMode) override;
|
||||
virtual void quit(int) override;
|
||||
virtual void wake() override;
|
||||
virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&&) override;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual void unquit() override { }
|
||||
virtual bool was_exit_requested() const override { return false; }
|
||||
virtual void notify_forked_and_in_child() override { }
|
||||
|
||||
EventLoopThreadData& thread_data();
|
||||
|
||||
private:
|
||||
friend class ALooperEventLoopManager;
|
||||
|
||||
ALooperEventLoopImplementation();
|
||||
|
||||
void register_notifier(Core::Notifier&);
|
||||
void unregister_notifier(Core::Notifier&);
|
||||
|
||||
ALooper* m_event_loop { nullptr };
|
||||
int m_exit_code { 0 };
|
||||
Atomic<bool> m_exit_requested { false };
|
||||
EventLoopThreadData* m_thread_data { nullptr };
|
||||
};
|
||||
|
||||
}
|
22
UI/Android/src/main/cpp/ImageDecoderService.cpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <ImageDecoder/ConnectionFromClient.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibIPC/SingleServer.h>
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto client = TRY(ImageDecoder::ConnectionFromClient::try_create(move(socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
16
UI/Android/src/main/cpp/JNIHelpers.cpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "JNIHelpers.h"
|
||||
#include <AK/Utf16View.h>
|
||||
|
||||
namespace Ladybird {
|
||||
jstring JavaEnvironment::jstring_from_ak_string(String const& str)
|
||||
{
|
||||
auto as_utf16 = MUST(AK::utf8_to_utf16(str.code_points()));
|
||||
return m_env->NewString(as_utf16.data(), as_utf16.size());
|
||||
}
|
||||
}
|
49
UI/Android/src/main/cpp/JNIHelpers.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/String.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
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; }
|
||||
|
||||
jstring jstring_from_ak_string(String const& str);
|
||||
|
||||
private:
|
||||
JavaVM* m_vm = nullptr;
|
||||
JNIEnv* m_env = nullptr;
|
||||
bool m_did_attach_thread = false;
|
||||
};
|
||||
}
|
||||
extern JavaVM* global_vm;
|
258
UI/Android/src/main/cpp/LadybirdActivity.cpp
Normal file
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Format.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibArchive/TarStream.h>
|
||||
#include <LibCore/DirIterator.h>
|
||||
#include <LibCore/Directory.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <jni.h>
|
||||
|
||||
static ErrorOr<void> extract_tar_archive(String archive_file, ByteString output_directory);
|
||||
|
||||
JavaVM* global_vm;
|
||||
static OwnPtr<WebView::Application> s_application;
|
||||
static OwnPtr<Core::EventLoop> s_main_event_loop;
|
||||
static jobject s_java_instance;
|
||||
static jmethodID s_schedule_event_loop_method;
|
||||
|
||||
struct Application : public WebView::Application {
|
||||
WEB_VIEW_APPLICATION(Application);
|
||||
};
|
||||
|
||||
Application::Application(Badge<WebView::Application>, Main::Arguments&)
|
||||
{
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv*, jobject, jstring, jstring, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
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_ladybird_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);
|
||||
|
||||
dbgln("Set resource dir to {}", s_ladybird_resource_root);
|
||||
|
||||
auto file_or_error = Core::System::open(MUST(String::formatted("{}/res/icons/48x48/app-browser.png", s_ladybird_resource_root)), O_RDONLY);
|
||||
if (file_or_error.is_error()) {
|
||||
dbgln("No resource files, extracting assets...");
|
||||
MUST(extract_tar_archive(MUST(String::formatted("{}/ladybird-assets.tar", s_ladybird_resource_root)), s_ladybird_resource_root));
|
||||
} else {
|
||||
dbgln("Found app-browser.png, not re-extracting assets.");
|
||||
dbgln("Hopefully no developer changed the asset files and expected them to be re-extracted!");
|
||||
}
|
||||
|
||||
env->GetJavaVM(&global_vm);
|
||||
VERIFY(global_vm);
|
||||
|
||||
s_java_instance = env->NewGlobalRef(thiz);
|
||||
jclass clazz = env->GetObjectClass(s_java_instance);
|
||||
VERIFY(clazz);
|
||||
s_schedule_event_loop_method = env->GetMethodID(clazz, "scheduleEventLoop", "()V");
|
||||
VERIFY(s_schedule_event_loop_method);
|
||||
env->DeleteLocalRef(clazz);
|
||||
|
||||
jobject timer_service_ref = env->NewGlobalRef(timer_service);
|
||||
|
||||
auto* event_loop_manager = new Ladybird::ALooperEventLoopManager(timer_service_ref);
|
||||
event_loop_manager->on_did_post_event = [] {
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(s_java_instance, s_schedule_event_loop_method);
|
||||
};
|
||||
Core::EventLoopManager::install(*event_loop_manager);
|
||||
s_main_event_loop = make<Core::EventLoop>();
|
||||
|
||||
// The strings cannot be empty
|
||||
Main::Arguments arguments = {
|
||||
.argc = 0,
|
||||
.argv = nullptr,
|
||||
.strings = Span<StringView> { new StringView("ladybird"sv), 1 }
|
||||
};
|
||||
|
||||
// FIXME: We are not making use of this Application object to track our processes.
|
||||
// So, right now, the Application's ProcessManager is constantly empty.
|
||||
// (However, LibWebView depends on an Application object existing, so we do have to actually create one.)
|
||||
s_application = Application::create(arguments, "about:newtab"sv);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_execMainEventLoop(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_execMainEventLoop(JNIEnv*, jobject /* thiz */)
|
||||
{
|
||||
if (s_main_event_loop) {
|
||||
s_main_event_loop->pump(Core::EventLoop::WaitMode::PollForEvents);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_disposeNativeCode(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_disposeNativeCode(JNIEnv* env, jobject /* thiz */)
|
||||
{
|
||||
s_main_event_loop = nullptr;
|
||||
s_schedule_event_loop_method = nullptr;
|
||||
s_application = nullptr;
|
||||
env->DeleteGlobalRef(s_java_instance);
|
||||
|
||||
delete &Core::EventLoopManager::the();
|
||||
}
|
||||
|
||||
ErrorOr<void> extract_tar_archive(String archive_file, ByteString output_directory)
|
||||
{
|
||||
constexpr size_t buffer_size = 4096;
|
||||
|
||||
auto file = TRY(Core::InputBufferedFile::create(TRY(Core::File::open(archive_file, Core::File::OpenMode::Read))));
|
||||
|
||||
ByteString old_pwd = TRY(Core::System::getcwd());
|
||||
|
||||
TRY(Core::System::chdir(output_directory));
|
||||
ScopeGuard go_back = [&old_pwd] { MUST(Core::System::chdir(old_pwd)); };
|
||||
|
||||
auto tar_stream = TRY(Archive::TarInputStream::construct(move(file)));
|
||||
|
||||
HashMap<ByteString, ByteString> global_overrides;
|
||||
HashMap<ByteString, ByteString> local_overrides;
|
||||
|
||||
auto get_override = [&](StringView key) -> Optional<ByteString> {
|
||||
Optional<ByteString> maybe_local = local_overrides.get(key);
|
||||
|
||||
if (maybe_local.has_value())
|
||||
return maybe_local;
|
||||
|
||||
Optional<ByteString> maybe_global = global_overrides.get(key);
|
||||
|
||||
if (maybe_global.has_value())
|
||||
return maybe_global;
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
while (!tar_stream->finished()) {
|
||||
Archive::TarFileHeader const& header = tar_stream->header();
|
||||
|
||||
// Handle meta-entries earlier to avoid consuming the file content stream.
|
||||
if (header.content_is_like_extended_header()) {
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::GlobalExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
if (value.length() == 0)
|
||||
global_overrides.remove(key);
|
||||
else
|
||||
global_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::ExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
local_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
warnln("Unknown extended header type '{}' of {}", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
|
||||
Archive::TarFileStream file_stream = tar_stream->file_contents();
|
||||
|
||||
// Handle other header types that don't just have an effect on extraction.
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::LongName: {
|
||||
StringBuilder long_name;
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
long_name.append(reinterpret_cast<char*>(slice.data()), slice.size());
|
||||
}
|
||||
|
||||
local_overrides.set("path", long_name.to_byte_string());
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
// None of the relevant headers, so continue as normal.
|
||||
break;
|
||||
}
|
||||
|
||||
LexicalPath path = LexicalPath(header.filename());
|
||||
if (!header.prefix().is_empty())
|
||||
path = path.prepend(header.prefix());
|
||||
ByteString filename = get_override("path"sv).value_or(path.string());
|
||||
|
||||
ByteString absolute_path = TRY(FileSystem::absolute_path(filename));
|
||||
auto parent_path = LexicalPath(absolute_path).parent();
|
||||
auto header_mode = TRY(header.mode());
|
||||
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::NormalFile:
|
||||
case Archive::TarFileType::AlternateNormalFile: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
int fd = TRY(Core::System::open(absolute_path, O_CREAT | O_WRONLY, header_mode));
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
TRY(Core::System::write(fd, slice));
|
||||
}
|
||||
|
||||
TRY(Core::System::close(fd));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::SymLink: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
TRY(Core::System::symlink(header.link_name(), absolute_path));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::Directory: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
auto result_or_error = Core::System::mkdir(absolute_path, header_mode);
|
||||
if (result_or_error.is_error() && result_or_error.error().code() != EEXIST)
|
||||
return result_or_error.release_error();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// FIXME: Implement other file types
|
||||
warnln("file type '{}' of {} is not yet supported", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// Non-global headers should be cleared after every file.
|
||||
local_overrides.clear();
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
}
|
||||
return {};
|
||||
}
|
12
UI/Android/src/main/cpp/LadybirdServiceBase.h
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <jni.h>
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket);
|
53
UI/Android/src/main/cpp/LadybirdServiceBaseJNI.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/Atomic.h>
|
||||
#include <AK/Format.h>
|
||||
#include <LibCore/ResourceImplementationFile.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <jni.h>
|
||||
|
||||
JavaVM* global_vm;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_nativeThreadLoop(JNIEnv*, jobject /* thiz */, jint);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_nativeThreadLoop(JNIEnv*, jobject /* thiz */, jint ipc_socket)
|
||||
{
|
||||
auto ret = service_main(ipc_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_LadybirdServiceBase_initNativeCode(JNIEnv*, jobject /* thiz */, jstring, jstring);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_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;
|
||||
}
|
||||
|
||||
env->GetJavaVM(&global_vm);
|
||||
|
||||
char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr);
|
||||
s_ladybird_resource_root = raw_resource_dir;
|
||||
env->ReleaseStringUTFChars(resource_dir, raw_resource_dir);
|
||||
// FIXME: Use a custom Android version that uses AssetManager to load files.
|
||||
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::formatted("{}/res", s_ladybird_resource_root))));
|
||||
|
||||
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);
|
||||
}
|
47
UI/Android/src/main/cpp/RequestServerService.cpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibIPC/SingleServer.h>
|
||||
#include <LibTLS/Certificate.h>
|
||||
#include <RequestServer/ConnectionFromClient.h>
|
||||
#include <RequestServer/HttpProtocol.h>
|
||||
#include <RequestServer/HttpsProtocol.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
// FIXME: Share b/w RequestServer and WebSocket
|
||||
static ErrorOr<ByteString> find_certificates(StringView serenity_resource_root)
|
||||
{
|
||||
auto cert_path = ByteString::formatted("{}/res/ladybird/cacert.pem", serenity_resource_root);
|
||||
if (!FileSystem::exists(cert_path))
|
||||
return Error::from_string_literal("Don't know how to load certs!");
|
||||
return cert_path;
|
||||
}
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
// Ensure the certificates are read out here.
|
||||
DefaultRootCACertificates::set_default_certificate_paths(Vector { TRY(find_certificates(s_ladybird_resource_root)) });
|
||||
[[maybe_unused]] auto& certs = DefaultRootCACertificates::the();
|
||||
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
RequestServer::HttpProtocol::install();
|
||||
RequestServer::HttpsProtocol::install();
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto client = TRY(RequestServer::ConnectionFromClient::try_create(move(socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
38
UI/Android/src/main/cpp/TimerExecutorService.cpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_TimerExecutorService_00024Timer_nativeRun(JNIEnv*, jobject /* thiz */, jlong, jlong);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_TimerExecutorService_00024Timer_nativeRun(JNIEnv*, jobject /* thiz */, jlong native_data, jlong id)
|
||||
{
|
||||
static Core::EventLoop s_event_loop; // Here to exist for this thread
|
||||
|
||||
auto& event_loop_impl = *reinterpret_cast<Ladybird::ALooperEventLoopImplementation*>(native_data);
|
||||
auto& thread_data = event_loop_impl.thread_data();
|
||||
|
||||
if (auto timer_data = thread_data.timers.get(id); timer_data.has_value()) {
|
||||
auto receiver = timer_data->receiver.strong_ref();
|
||||
if (!receiver)
|
||||
return;
|
||||
|
||||
if (timer_data->visibility == Core::TimerShouldFireWhenNotVisible::No)
|
||||
if (!receiver->is_visible_for_timer_purposes())
|
||||
return;
|
||||
|
||||
event_loop_impl.post_event(*receiver, make<Core::TimerEvent>());
|
||||
}
|
||||
// Flush the event loop on this thread to keep any garbage from building up
|
||||
if (auto num_events = s_event_loop.pump(Core::EventLoop::WaitMode::PollForEvents); num_events != 0) {
|
||||
dbgln("BUG: Processed {} events on Timer thread!", num_events);
|
||||
}
|
||||
}
|
158
UI/Android/src/main/cpp/WebContentService.cpp
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebContentService.h"
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibIPC/ConnectionFromClient.h>
|
||||
#include <LibImageDecoderClient/Client.h>
|
||||
#include <LibJS/Bytecode/Interpreter.h>
|
||||
#include <LibMedia/Audio/Loader.h>
|
||||
#include <LibRequests/RequestClient.h>
|
||||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Loader/ContentFilter.h>
|
||||
#include <LibWeb/Loader/GeneratedPagesLoader.h>
|
||||
#include <LibWeb/Loader/ResourceLoader.h>
|
||||
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
|
||||
#include <LibWeb/Platform/AudioCodecPluginAgnostic.h>
|
||||
#include <LibWeb/Platform/EventLoopPluginSerenity.h>
|
||||
#include <LibWebView/RequestServerAdapter.h>
|
||||
#include <UI/FontPlugin.h>
|
||||
#include <UI/HelperProcess.h>
|
||||
#include <UI/ImageCodecPlugin.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <WebContent/ConnectionFromClient.h>
|
||||
#include <WebContent/PageHost.h>
|
||||
|
||||
static ErrorOr<NonnullRefPtr<Requests::RequestClient>> bind_request_server_service()
|
||||
{
|
||||
return bind_service<Requests::RequestClient>(&bind_request_server_java);
|
||||
}
|
||||
|
||||
static ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> bind_image_decoder_service()
|
||||
{
|
||||
return bind_service<ImageDecoderClient::Client>(&bind_image_decoder_java);
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_content_filters();
|
||||
|
||||
static ErrorOr<void> load_autoplay_allowlist();
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity);
|
||||
|
||||
auto image_decoder_client = TRY(bind_image_decoder_service());
|
||||
Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPlugin(move(image_decoder_client)));
|
||||
|
||||
Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) {
|
||||
return Web::Platform::AudioCodecPluginAgnostic::create(move(loader));
|
||||
});
|
||||
|
||||
auto request_server_client = TRY(bind_request_server_service());
|
||||
Web::ResourceLoader::initialize(TRY(WebView::RequestServerAdapter::try_create(move(request_server_client))));
|
||||
|
||||
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));
|
||||
|
||||
TRY(Web::Bindings::initialize_main_thread_vm(Web::HTML::EventLoop::Type::Window));
|
||||
|
||||
auto maybe_content_filter_error = load_content_filters();
|
||||
if (maybe_content_filter_error.is_error())
|
||||
dbgln("Failed to load content filters: {}", maybe_content_filter_error.error());
|
||||
|
||||
auto maybe_autoplay_allowlist_error = load_autoplay_allowlist();
|
||||
if (maybe_autoplay_allowlist_error.is_error())
|
||||
dbgln("Failed to load autoplay allowlist: {}", maybe_autoplay_allowlist_error.error());
|
||||
|
||||
auto webcontent_socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(move(webcontent_socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
||||
|
||||
template<typename Client>
|
||||
ErrorOr<NonnullRefPtr<Client>> bind_service(void (*bind_method)(int))
|
||||
{
|
||||
int socket_fds[2] {};
|
||||
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
|
||||
|
||||
int ui_fd = socket_fds[0];
|
||||
int server_fd = socket_fds[1];
|
||||
|
||||
// NOTE: The java object takes ownership of the socket fds
|
||||
(*bind_method)(server_fd);
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ui_fd));
|
||||
TRY(socket->set_blocking(true));
|
||||
|
||||
auto new_client = TRY(try_make_ref_counted<Client>(move(socket)));
|
||||
|
||||
return new_client;
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_content_filters()
|
||||
{
|
||||
auto file_or_error = Core::File::open(ByteString::formatted("{}/res/ladybird/default-config/BrowserContentFilters.txt", s_ladybird_resource_root), Core::File::OpenMode::Read);
|
||||
if (file_or_error.is_error())
|
||||
return file_or_error.release_error();
|
||||
|
||||
auto file = file_or_error.release_value();
|
||||
auto ad_filter_list = TRY(Core::InputBufferedFile::create(move(file)));
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
|
||||
|
||||
Vector<String> patterns;
|
||||
|
||||
while (TRY(ad_filter_list->can_read_line())) {
|
||||
auto line = TRY(ad_filter_list->read_line(buffer));
|
||||
if (line.is_empty())
|
||||
continue;
|
||||
|
||||
auto pattern = TRY(String::from_utf8(line));
|
||||
TRY(patterns.try_append(move(pattern)));
|
||||
}
|
||||
|
||||
auto& content_filter = Web::ContentFilter::the();
|
||||
TRY(content_filter.set_patterns(patterns));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_autoplay_allowlist()
|
||||
{
|
||||
auto file_or_error = Core::File::open(TRY(String::formatted("{}/res/ladybird/default-config/BrowserAutoplayAllowlist.txt", s_ladybird_resource_root)), Core::File::OpenMode::Read);
|
||||
if (file_or_error.is_error())
|
||||
return file_or_error.release_error();
|
||||
|
||||
auto file = file_or_error.release_value();
|
||||
auto allowlist = TRY(Core::InputBufferedFile::create(move(file)));
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
|
||||
|
||||
Vector<String> origins;
|
||||
|
||||
while (TRY(allowlist->can_read_line())) {
|
||||
auto line = TRY(allowlist->read_line(buffer));
|
||||
if (line.is_empty())
|
||||
continue;
|
||||
|
||||
auto domain = TRY(String::from_utf8(line));
|
||||
TRY(origins.try_append(move(domain)));
|
||||
}
|
||||
|
||||
auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the();
|
||||
TRY(autoplay_allowlist.enable_for_origins(origins));
|
||||
|
||||
return {};
|
||||
}
|
15
UI/Android/src/main/cpp/WebContentService.h
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
|
||||
template<typename Client>
|
||||
ErrorOr<NonnullRefPtr<Client>> bind_service(void (*bind_method)(int));
|
||||
|
||||
void bind_request_server_java(int ipc_socket);
|
||||
void bind_image_decoder_java(int ipc_socket);
|
52
UI/Android/src/main/cpp/WebContentServiceJNI.cpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "JNIHelpers.h"
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include "WebContentService.h"
|
||||
#include <jni.h>
|
||||
|
||||
jobject global_instance;
|
||||
jclass global_class_reference;
|
||||
jmethodID bind_request_server_method;
|
||||
jmethodID bind_image_decoder_method;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebContentService_nativeInit(JNIEnv*, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebContentService_nativeInit(JNIEnv* env, jobject thiz)
|
||||
{
|
||||
global_instance = env->NewGlobalRef(thiz);
|
||||
|
||||
auto local_class = env->FindClass("org/serenityos/ladybird/WebContentService");
|
||||
if (!local_class)
|
||||
TODO();
|
||||
global_class_reference = reinterpret_cast<jclass>(env->NewGlobalRef(local_class));
|
||||
env->DeleteLocalRef(local_class);
|
||||
|
||||
auto method = env->GetMethodID(global_class_reference, "bindRequestServer", "(I)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
bind_request_server_method = method;
|
||||
|
||||
method = env->GetMethodID(global_class_reference, "bindImageDecoder", "(I)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
bind_image_decoder_method = method;
|
||||
}
|
||||
|
||||
void bind_request_server_java(int ipc_socket)
|
||||
{
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(global_instance, bind_request_server_method, ipc_socket);
|
||||
}
|
||||
|
||||
void bind_image_decoder_java(int ipc_socket)
|
||||
{
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(global_instance, bind_image_decoder_method, ipc_socket);
|
||||
}
|
145
UI/Android/src/main/cpp/WebViewImplementationNative.cpp
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebViewImplementationNative.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/DeprecatedPainter.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <LibWebView/WebContentClient.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
static Gfx::BitmapFormat to_gfx_bitmap_format(i32 f)
|
||||
{
|
||||
switch (f) {
|
||||
case ANDROID_BITMAP_FORMAT_RGBA_8888:
|
||||
return Gfx::BitmapFormat::BGRA8888;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
WebViewImplementationNative::WebViewImplementationNative(jobject thiz)
|
||||
: m_java_instance(thiz)
|
||||
{
|
||||
// NOTE: m_java_instance's global ref is controlled by the JNI bindings
|
||||
initialize_client(CreateNewClient::Yes);
|
||||
|
||||
on_ready_to_paint = [this]() {
|
||||
JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(m_java_instance, invalidate_layout_method);
|
||||
};
|
||||
|
||||
on_load_start = [this](URL::URL const& url, bool is_redirect) {
|
||||
JavaEnvironment env(global_vm);
|
||||
auto url_string = env.jstring_from_ak_string(MUST(url.to_string()));
|
||||
env.get()->CallVoidMethod(m_java_instance, on_load_start_method, url_string, is_redirect);
|
||||
env.get()->DeleteLocalRef(url_string);
|
||||
};
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::initialize_client(WebView::ViewImplementation::CreateNewClient)
|
||||
{
|
||||
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 = [] {
|
||||
warnln("WebContent crashed!");
|
||||
// FIXME: launch a new client
|
||||
};
|
||||
|
||||
m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid());
|
||||
client().async_set_window_handle(0, m_client_state.client_handle);
|
||||
|
||||
client().async_set_device_pixels_per_css_pixel(0, m_device_pixel_ratio);
|
||||
|
||||
// FIXME: update_palette, update system fonts
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::paint_into_bitmap(void* android_bitmap_raw, AndroidBitmapInfo const& info)
|
||||
{
|
||||
// Software bitmaps only for now!
|
||||
VERIFY((info.flags & ANDROID_BITMAP_FLAGS_IS_HARDWARE) == 0);
|
||||
|
||||
auto android_bitmap = MUST(Gfx::Bitmap::create_wrapper(to_gfx_bitmap_format(info.format), Gfx::AlphaType::Premultiplied, { info.width, info.height }, info.stride, android_bitmap_raw));
|
||||
Gfx::DeprecatedPainter painter(android_bitmap);
|
||||
if (auto* bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr())
|
||||
painter.blit({ 0, 0 }, *bitmap, bitmap->rect());
|
||||
else
|
||||
painter.clear_rect(painter.clip_rect(), Gfx::Color::Magenta);
|
||||
|
||||
// Convert our internal BGRA into RGBA. This will be slowwwwwww
|
||||
// FIXME: Don't do a color format swap here.
|
||||
for (auto y = 0; y < android_bitmap->height(); ++y) {
|
||||
auto* scanline = android_bitmap->scanline(y);
|
||||
for (auto x = 0; x < android_bitmap->width(); ++x) {
|
||||
auto current_pixel = scanline[x];
|
||||
u32 alpha = (current_pixel & 0xFF000000U) >> 24;
|
||||
u32 red = (current_pixel & 0x00FF0000U) >> 16;
|
||||
u32 green = (current_pixel & 0x0000FF00U) >> 8;
|
||||
u32 blue = (current_pixel & 0x000000FFU);
|
||||
scanline[x] = (alpha << 24U) | (blue << 16U) | (green << 8U) | red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::set_viewport_geometry(int w, int h)
|
||||
{
|
||||
m_viewport_size = { w, h };
|
||||
handle_resize();
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::set_device_pixel_ratio(float f)
|
||||
{
|
||||
m_device_pixel_ratio = f;
|
||||
client().async_set_device_pixels_per_css_pixel(0, m_device_pixel_ratio);
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::mouse_event(Web::MouseEvent::Type event_type, float x, float y, float raw_x, float raw_y)
|
||||
{
|
||||
Gfx::IntPoint position = { x, y };
|
||||
Gfx::IntPoint screen_position = { raw_x, raw_y };
|
||||
auto event = Web::MouseEvent {
|
||||
event_type,
|
||||
position.to_type<Web::DevicePixels>(),
|
||||
screen_position.to_type<Web::DevicePixels>(),
|
||||
Web::UIEvents::MouseButton::Primary,
|
||||
Web::UIEvents::MouseButton::Primary,
|
||||
Web::UIEvents::KeyModifier::Mod_None,
|
||||
0,
|
||||
0,
|
||||
nullptr
|
||||
};
|
||||
|
||||
enqueue_input_event(move(event));
|
||||
}
|
||||
|
||||
NonnullRefPtr<WebView::WebContentClient> WebViewImplementationNative::bind_web_content_client()
|
||||
{
|
||||
JavaEnvironment env(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];
|
||||
|
||||
// NOTE: The java object takes ownership of the socket fds
|
||||
env.get()->CallVoidMethod(m_java_instance, bind_webcontent_method, wc_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);
|
||||
|
||||
return new_client;
|
||||
}
|
||||
}
|
45
UI/Android/src/main/cpp/WebViewImplementationNative.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
class WebViewImplementationNative : public WebView::ViewImplementation {
|
||||
public:
|
||||
WebViewImplementationNative(jobject thiz);
|
||||
|
||||
virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size; }
|
||||
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 initialize_client(CreateNewClient) override;
|
||||
|
||||
void paint_into_bitmap(void* android_bitmap_raw, AndroidBitmapInfo const& info);
|
||||
|
||||
void set_viewport_geometry(int w, int h);
|
||||
void set_device_pixel_ratio(float f);
|
||||
|
||||
void mouse_event(Web::MouseEvent::Type event_type, float x, float y, float raw_x, float raw_y);
|
||||
|
||||
static jclass global_class_reference;
|
||||
static jmethodID bind_webcontent_method;
|
||||
static jmethodID invalidate_layout_method;
|
||||
static jmethodID on_load_start_method;
|
||||
|
||||
jobject java_instance() const { return m_java_instance; }
|
||||
|
||||
private:
|
||||
jobject m_java_instance = nullptr;
|
||||
Web::DevicePixelSize m_viewport_size;
|
||||
};
|
||||
}
|
145
UI/Android/src/main/cpp/WebViewImplementationNativeJNI.cpp
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebViewImplementationNative.h"
|
||||
#include <jni.h>
|
||||
|
||||
using namespace Ladybird;
|
||||
|
||||
jclass WebViewImplementationNative::global_class_reference;
|
||||
jmethodID WebViewImplementationNative::bind_webcontent_method;
|
||||
jmethodID WebViewImplementationNative::invalidate_layout_method;
|
||||
jmethodID WebViewImplementationNative::on_load_start_method;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassInit(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassInit(JNIEnv* env, jobject /* thiz */)
|
||||
{
|
||||
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 method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "bindWebContentService", "(I)V");
|
||||
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;
|
||||
|
||||
method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "onLoadStart", "(Ljava/lang/String;Z)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
WebViewImplementationNative::on_load_start_method = method;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv*, jobject);
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv* env, jobject thiz)
|
||||
{
|
||||
auto ref = env->NewGlobalRef(thiz);
|
||||
auto instance = reinterpret_cast<jlong>(new WebViewImplementationNative(ref));
|
||||
return instance;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv*, jobject /* thiz */, jlong);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv* env, jobject /* thiz */, jlong instance)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
env->DeleteGlobalRef(impl->java_instance());
|
||||
delete impl;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeDrawIntoBitmap(JNIEnv*, jobject /* thiz */, jlong, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeDrawIntoBitmap(JNIEnv* env, jobject /* thiz */, jlong instance, jobject bitmap)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
|
||||
AndroidBitmapInfo bitmap_info = {};
|
||||
void* pixels = nullptr;
|
||||
AndroidBitmap_getInfo(env, bitmap, &bitmap_info);
|
||||
AndroidBitmap_lockPixels(env, bitmap, &pixels);
|
||||
if (pixels)
|
||||
impl->paint_into_bitmap(pixels, bitmap_info);
|
||||
|
||||
AndroidBitmap_unlockPixels(env, bitmap);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNIEnv*, jobject /* thiz */, jlong, jint, jint);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNIEnv*, jobject /* thiz */, jlong instance, jint w, jint h)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
impl->set_viewport_geometry(w, h);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeLoadURL(JNIEnv*, jobject /* thiz */, jlong, jstring);
|
||||
|
||||
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 = 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeMouseEvent(JNIEnv*, jobject /* thiz */, jlong, jint, jfloat, jfloat, jfloat, jfloat);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeMouseEvent(JNIEnv*, jobject /* thiz */, jlong instance, jint event_type, jfloat x, jfloat y, jfloat raw_x, jfloat raw_y)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
|
||||
Web::MouseEvent::Type web_event_type;
|
||||
|
||||
// These integers are defined in Android's MotionEvent.
|
||||
// See https://developer.android.com/reference/android/view/MotionEvent#constants_1
|
||||
if (event_type == 0) {
|
||||
// MotionEvent.ACTION_DOWN
|
||||
web_event_type = Web::MouseEvent::Type::MouseDown;
|
||||
} else if (event_type == 1) {
|
||||
// MotionEvent.ACTION_UP
|
||||
web_event_type = Web::MouseEvent::Type::MouseUp;
|
||||
} else if (event_type == 2) {
|
||||
// MotionEvent.ACTION_MOVE
|
||||
web_event_type = Web::MouseEvent::Type::MouseMove;
|
||||
} else {
|
||||
// Unknown event type, default to MouseUp
|
||||
web_event_type = Web::MouseEvent::Type::MouseUp;
|
||||
}
|
||||
|
||||
impl->mouse_event(web_event_type, x, y, raw_x, raw_y);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.os.Message
|
||||
|
||||
class ImageDecoderService : LadybirdServiceBase("ImageDecoderService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("imagedecoder")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import org.serenityos.ladybird.databinding.ActivityMainBinding
|
||||
|
||||
class LadybirdActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var resourceDir: String
|
||||
private lateinit var view: WebView
|
||||
private lateinit var urlEditText: EditText
|
||||
private var timerService = TimerExecutorService()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
resourceDir = TransferAssets.transferAssets(this)
|
||||
initNativeCode(resourceDir, "Ladybird", timerService)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
urlEditText = binding.urlEditText
|
||||
view = binding.webView
|
||||
view.onLoadStart = { url: String, _ ->
|
||||
urlEditText.setText(url, TextView.BufferType.EDITABLE)
|
||||
}
|
||||
urlEditText.setOnEditorActionListener { textView: TextView, actionId: Int, _: KeyEvent? ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_SEARCH -> view.loadURL(textView.text.toString())
|
||||
}
|
||||
false
|
||||
}
|
||||
view.initialize(resourceDir)
|
||||
view.loadURL(intent.dataString ?: "https://ladybird.dev")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
view.dispose()
|
||||
disposeNativeCode()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun scheduleEventLoop() {
|
||||
mainExecutor.execute {
|
||||
execMainEventLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private external fun initNativeCode(
|
||||
resourceDir: String, tag: String, timerService: TimerExecutorService
|
||||
)
|
||||
|
||||
private external fun disposeNativeCode()
|
||||
private external fun execMainEventLoop()
|
||||
|
||||
companion object {
|
||||
// Used to load the 'ladybird' library on application startup.
|
||||
init {
|
||||
System.loadLibrary("Ladybird")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* 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.Looper
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
const val MSG_SET_RESOURCE_ROOT = 1
|
||||
const val MSG_TRANSFER_SOCKET = 2
|
||||
|
||||
abstract class LadybirdServiceBase(protected val TAG: String) : Service() {
|
||||
private val threadPool = Executors.newCachedThreadPool()
|
||||
protected lateinit var resourceDir: String
|
||||
|
||||
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")!!
|
||||
createThread(ipcSocket)
|
||||
}
|
||||
|
||||
private fun handleSetResourceRoot(msg: Message) {
|
||||
// FIXME: Handle this being already set, not being present, etc
|
||||
resourceDir = msg.data.getString("PATH")!!
|
||||
|
||||
initNativeCode(resourceDir, TAG)
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
// FIXME: Check the intent to make sure it's legit
|
||||
return Messenger(IncomingHandler(WeakReference(this))).binder
|
||||
}
|
||||
|
||||
|
||||
private fun createThread(ipcSocket: ParcelFileDescriptor) {
|
||||
threadPool.execute {
|
||||
nativeThreadLoop(ipcSocket.detachFd())
|
||||
}
|
||||
}
|
||||
|
||||
private external fun nativeThreadLoop(ipcSocket: Int)
|
||||
private external fun initNativeCode(resourceDir: String, tagName: String);
|
||||
|
||||
abstract fun handleServiceSpecificMessage(msg: Message): Boolean
|
||||
|
||||
companion object {
|
||||
|
||||
class IncomingHandler(private val service: WeakReference<LadybirdServiceBase>) :
|
||||
Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
MSG_TRANSFER_SOCKET -> service.get()?.handleTransferSockets(msg)
|
||||
?: super.handleMessage(msg)
|
||||
|
||||
MSG_SET_RESOURCE_ROOT -> service.get()?.handleSetResourceRoot(msg)
|
||||
?: super.handleMessage(msg)
|
||||
|
||||
else -> {
|
||||
val ret = service.get()?.handleServiceSpecificMessage(msg)
|
||||
if (ret == null || !ret)
|
||||
super.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* 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 LadybirdServiceConnection(
|
||||
private var ipcFd: Int,
|
||||
private var resourceDir: String
|
||||
) :
|
||||
ServiceConnection {
|
||||
var boundToService: Boolean = false
|
||||
var onDisconnect: () -> Unit = {}
|
||||
private var service: 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.
|
||||
service = Messenger(svc)
|
||||
boundToService = true
|
||||
|
||||
val init = Message.obtain(null, MSG_SET_RESOURCE_ROOT)
|
||||
init.data.putString("PATH", resourceDir)
|
||||
service!!.send(init)
|
||||
|
||||
val parcel = ParcelFileDescriptor.adoptFd(ipcFd)
|
||||
val msg = Message.obtain(null, MSG_TRANSFER_SOCKET)
|
||||
msg.data.putParcelable("IPC_SOCKET", parcel)
|
||||
service!!.send(msg)
|
||||
parcel.detachFd()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
// This is called when the connection with the service has been
|
||||
// unexpectedly disconnected; that is, its process crashed.
|
||||
service = null
|
||||
boundToService = false
|
||||
|
||||
// Notify owner that the service is dead
|
||||
onDisconnect()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.os.Message
|
||||
|
||||
class RequestServerService : LadybirdServiceBase("RequestServerService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("requestserver")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TimerExecutorService {
|
||||
|
||||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
class Timer(private var nativeData: Long) : Runnable {
|
||||
override fun run() {
|
||||
nativeRun(nativeData, id)
|
||||
}
|
||||
|
||||
private external fun nativeRun(nativeData: Long, id: Long)
|
||||
var id: Long = 0
|
||||
}
|
||||
|
||||
fun registerTimer(timer: Timer, singleShot: Boolean, milliseconds: Long): Long {
|
||||
val id = ++nextId
|
||||
timer.id = id
|
||||
val handle: ScheduledFuture<*> = if (singleShot) executor.schedule(
|
||||
timer,
|
||||
milliseconds,
|
||||
TimeUnit.MILLISECONDS
|
||||
) else executor.scheduleWithFixedDelay(
|
||||
timer,
|
||||
milliseconds,
|
||||
milliseconds,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
timers[id] = handle
|
||||
return id
|
||||
}
|
||||
|
||||
fun unregisterTimer(id: Long) {
|
||||
val timer = timers[id] ?: return
|
||||
timer.cancel(false)
|
||||
}
|
||||
|
||||
private var nextId: Long = 0
|
||||
private val timers: HashMap<Long, ScheduledFuture<*>> = hashMapOf()
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
||||
* <p>
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.lang.String;
|
||||
|
||||
public class TransferAssets {
|
||||
/**
|
||||
* @return new ladybird resource root
|
||||
*/
|
||||
static public String transferAssets(Context context) {
|
||||
Log.d("Ladybird", "Hello from java");
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
File assetDir = applicationContext.getFilesDir();
|
||||
AssetManager assetManager = applicationContext.getAssets();
|
||||
if (!copyAsset(assetManager, "ladybird-assets.tar", assetDir.getAbsolutePath() + "/ladybird-assets.tar")) {
|
||||
Log.e("Ladybird", "Unable to copy assets");
|
||||
return "Invalid Assets, this won't work";
|
||||
}
|
||||
Log.d("Ladybird", "Copied ladybird-assets.tar to app-specific storage path");
|
||||
return assetDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
// ty to https://stackoverflow.com/a/22903693 for the sauce
|
||||
private static boolean copyAsset(AssetManager assetManager,
|
||||
String fromAssetPath, String toPath) {
|
||||
try {
|
||||
InputStream in = assetManager.open(fromAssetPath);
|
||||
new File(toPath).createNewFile();
|
||||
OutputStream out = new FileOutputStream(toPath);
|
||||
copyFile(in, out);
|
||||
in.close();
|
||||
out.flush();
|
||||
out.close();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFile(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Message
|
||||
import android.util.Log
|
||||
|
||||
class WebContentService : LadybirdServiceBase("WebContentService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
init {
|
||||
nativeInit();
|
||||
}
|
||||
|
||||
private fun bindRequestServer(ipcFd: Int)
|
||||
{
|
||||
val connector = LadybirdServiceConnection(ipcFd, resourceDir)
|
||||
connector.onDisconnect = {
|
||||
// FIXME: Notify impl that service is dead and might need restarted
|
||||
Log.e(TAG, "RequestServer Died! :(")
|
||||
}
|
||||
// FIXME: Unbind this at some point maybe
|
||||
bindService(
|
||||
Intent(this, RequestServerService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindImageDecoder(ipcFd: Int)
|
||||
{
|
||||
val connector = LadybirdServiceConnection(ipcFd, resourceDir)
|
||||
connector.onDisconnect = {
|
||||
// FIXME: Notify impl that service is dead and might need restarted
|
||||
Log.e(TAG, "ImageDecoder Died! :(")
|
||||
}
|
||||
// FIXME: Unbind this at some point maybe
|
||||
bindService(
|
||||
Intent(this, ImageDecoderService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
external fun nativeInit()
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("webcontent")
|
||||
}
|
||||
}
|
||||
}
|
69
UI/Android/src/main/java/org/serenityos/ladybird/WebView.kt
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
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(this)
|
||||
private lateinit var contentBitmap: Bitmap
|
||||
var onLoadStart: (url: String, isRedirect: Boolean) -> Unit = { _, _ -> }
|
||||
|
||||
fun initialize(resourceDir: String) {
|
||||
viewImpl.initialize(resourceDir)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
viewImpl.dispose()
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
viewImpl.loadURL(url)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
// The native side only supports down, move, and up events.
|
||||
// So, ignore any other MotionEvents.
|
||||
if (event.action != MotionEvent.ACTION_DOWN &&
|
||||
event.action != MotionEvent.ACTION_MOVE &&
|
||||
event.action != MotionEvent.ACTION_UP) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
// FIXME: We are passing these through as mouse events.
|
||||
// We should really be handling them as touch events.
|
||||
// (And we should handle scrolling - right now you have tap and drag the scrollbar!)
|
||||
viewImpl.mouseEvent(event.action, event.x, event.y, event.rawX, event.rawY)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
viewImpl.drawIntoBitmap(contentBitmap);
|
||||
canvas.drawBitmap(contentBitmap, 0f, 0f, null)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
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.MotionEvent
|
||||
import android.view.View
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Wrapper around WebView::ViewImplementation for use by Kotlin
|
||||
*/
|
||||
class WebViewImplementation(private val view: WebView) {
|
||||
// Instance Pointer to native object, very unsafe :)
|
||||
private var nativeInstance: Long = 0
|
||||
private lateinit var resourceDir: String
|
||||
private lateinit var connection: ServiceConnection
|
||||
|
||||
fun initialize(resourceDir: String) {
|
||||
this.resourceDir = resourceDir
|
||||
nativeInstance = nativeObjectInit()
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
nativeObjectDispose(nativeInstance)
|
||||
nativeInstance = 0
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
nativeLoadURL(nativeInstance, url)
|
||||
}
|
||||
|
||||
fun drawIntoBitmap(bitmap: Bitmap) {
|
||||
nativeDrawIntoBitmap(nativeInstance, bitmap)
|
||||
}
|
||||
|
||||
fun setViewportGeometry(w: Int, h: Int) {
|
||||
nativeSetViewportGeometry(nativeInstance, w, h)
|
||||
}
|
||||
|
||||
fun setDevicePixelRatio(ratio: Float) {
|
||||
nativeSetDevicePixelRatio(nativeInstance, ratio)
|
||||
}
|
||||
|
||||
fun mouseEvent(eventType: Int, x: Float, y: Float, rawX: Float, rawY: Float) {
|
||||
nativeMouseEvent(nativeInstance, eventType, x, y, rawX, rawY)
|
||||
}
|
||||
|
||||
// Functions called from native code
|
||||
fun bindWebContentService(ipcFd: Int) {
|
||||
val connector = LadybirdServiceConnection(ipcFd, 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
|
||||
view.context.bindService(
|
||||
Intent(view.context, WebContentService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
connection = connector
|
||||
}
|
||||
|
||||
fun invalidateLayout() {
|
||||
view.requestLayout()
|
||||
view.invalidate()
|
||||
}
|
||||
|
||||
fun onLoadStart(url: String, isRedirect: Boolean) {
|
||||
view.onLoadStart(url, isRedirect)
|
||||
}
|
||||
|
||||
// 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)
|
||||
private external fun nativeMouseEvent(instance: Long, eventType: Int, x: Float, y: Float, rawX: Float, rawY: Float)
|
||||
|
||||
companion object {
|
||||
/*
|
||||
* We use a static class initializer to allow the native code to cache some
|
||||
* field offsets. This native function looks up and caches interesting
|
||||
* class/field/method IDs. Throws on failure.
|
||||
*/
|
||||
private external fun nativeClassInit()
|
||||
|
||||
init {
|
||||
nativeClassInit()
|
||||
}
|
||||
}
|
||||
};
|
170
UI/Android/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
30
UI/Android/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
47
UI/Android/src/main/res/layout/activity_main.xml
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".LadybirdActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize">
|
||||
<!-- FIXME: Add Navigation, URL bar, Tab interactions, etc -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_scrollFlags="scroll|snap|enterAlways">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlEditText"
|
||||
style="@style/Widget.AppCompat.EditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="url"
|
||||
android:ems="10"
|
||||
android:hint="@string/url_edit_default"
|
||||
android:imeOptions="actionGo|actionSearch"
|
||||
android:inputType="textUri"
|
||||
android:singleLine="true" />
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/web_view_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<org.serenityos.ladybird.WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".LadybirdActivity" />
|
||||
</FrameLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
6
UI/Android/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
BIN
UI/Android/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
UI/Android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
UI/Android/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 982 B |
BIN
UI/Android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
UI/Android/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
UI/Android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 7.6 KiB |
19
UI/Android/src/main/res/values-night/themes.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Ladybird" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@color/grey</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
11
UI/Android/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="grey">#FF6B6B6B</color>
|
||||
</resources>
|
4
UI/Android/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Ladybird</string>
|
||||
<string name="url_edit_default">Enter URL...</string>
|
||||
</resources>
|
19
UI/Android/src/main/res/values/themes.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Ladybird" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@color/white</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
13
UI/Android/src/main/res/xml/backup_rules.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
19
UI/Android/src/main/res/xml/data_extraction_rules.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|