Everywhere: Move the Ladybird folder to UI

This commit is contained in:
Timothy Flynn 2024-11-09 12:50:33 -05:00 committed by Andreas Kling
commit db47cc41f8
Notes: github-actions[bot] 2024-11-10 11:51:45 +00:00
203 changed files with 266 additions and 244 deletions

View file

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

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

View 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>(&current_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(&notifier);
current_impl().register_notifier(notifier);
}
void ALooperEventLoopManager::unregister_notifier(Core::Notifier& notifier)
{
EventLoopThreadData::the().notifiers.remove(&notifier);
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, &notifier_callback, &notifier);
VERIFY(ret == 1);
}
void ALooperEventLoopImplementation::unregister_notifier(Core::Notifier& notifier)
{
ALooper_removeFd(m_event_loop, notifier.fd());
}
}

View 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 };
};
}

View 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();
}

View 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());
}
}

View 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;

View 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 {};
}

View 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);

View 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);
}

View 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();
}

View 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);
}
}

View 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 {};
}

View 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);

View 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);
}

View 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;
}
}

View 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;
};
}

View 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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

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

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

View file

@ -0,0 +1,4 @@
<resources>
<string name="app_name">Ladybird</string>
<string name="url_edit_default">Enter URL...</string>
</resources>

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

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

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