diff --git a/Ladybird/.gitignore b/Ladybird/.gitignore index 84bbdd10cf..d7cdb5d338 100644 --- a/Ladybird/.gitignore +++ b/Ladybird/.gitignore @@ -5,3 +5,8 @@ ladybird moc_* Build build +CMakeLists.txt.user +android/gradle +android/gradlew* +android/assets/ + diff --git a/Ladybird/AndroidPlatform.cpp b/Ladybird/AndroidPlatform.cpp new file mode 100644 index 0000000000..0287b36ba2 --- /dev/null +++ b/Ladybird/AndroidPlatform.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022, Andrew Kaster + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define AK_DONT_REPLACE_STD + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef AK_OS_ANDROID +# error This file is for Android only, check CMake config! +#endif + +// HACK ALERT, we need to include LibMain manually here because the Qt build system doesn't include LibMain.a in the actual executable, +// nor include it in libladybird_.so +#include // NOLINT(bugprone-suspicious-include) + +extern String s_serenity_resource_root; + +void android_platform_init(); +static void extract_ladybird_resources(); +static ErrorOr extract_tar_archive(String archive_file, String output_directory); + +void android_platform_init() +{ + qDebug() << "Device supports OpenSSL: " << QSslSocket::supportsSsl(); + + QJniObject res = QJniObject::callStaticMethod("org/serenityos/ladybird/TransferAssets", + "transferAssets", + "(Landroid/content/Context;)Ljava/lang/String;", + QNativeInterface::QAndroidApplication::context()); + s_serenity_resource_root = res.toString().toUtf8().data(); + + extract_ladybird_resources(); +} + +void extract_ladybird_resources() +{ + qDebug() << "serenity resource root is " << s_serenity_resource_root.characters(); + auto file_or_error = Core::System::open(String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root), O_RDONLY); + if (file_or_error.is_error()) { + qDebug() << "Unable to open test file file as expected, extracting asssets..."; + + MUST(extract_tar_archive(String::formatted("{}/ladybird-assets.tar", s_serenity_resource_root), s_serenity_resource_root)); + } else { + qDebug() << "Opened app-browser.png test file, good to go!"; + qDebug() << "Hopefully no developer changed the asset files and expected them to be re-extracted!"; + } +} + +ErrorOr extract_tar_archive(String archive_file, String output_directory) +{ + constexpr size_t buffer_size = 4096; + + auto file = TRY(Core::File::open(archive_file, Core::OpenMode::ReadOnly)); + + String old_pwd = TRY(Core::System::getcwd()); + + TRY(Core::System::chdir(output_directory)); + ScopeGuard go_back = [&old_pwd] { MUST(Core::System::chdir(old_pwd)); }; + + Core::InputFileStream file_stream(file); + + Archive::TarInputStream tar_stream(file_stream); + if (!tar_stream.valid()) { + qDebug() << "the provided file is not a well-formatted ustar file"; + return Error::from_errno(EINVAL); + } + + HashMap global_overrides; + HashMap local_overrides; + + auto get_override = [&](StringView key) -> Optional { + Optional maybe_local = local_overrides.get(key); + + if (maybe_local.has_value()) + return maybe_local; + + Optional maybe_global = global_overrides.get(key); + + if (maybe_global.has_value()) + return maybe_global; + + return {}; + }; + + for (; !tar_stream.finished(); tar_stream.advance()) { + 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(); + } + + 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 buffer; + size_t bytes_read; + + while ((bytes_read = file_stream.read(buffer)) > 0) + long_name.append(reinterpret_cast(buffer.data()), bytes_read); + + local_overrides.set("path", long_name.to_string()); + 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()); + String filename = get_override("path"sv).value_or(path.string()); + + String absolute_path = Core::File::absolute_path(filename); + auto parent_path = LexicalPath(absolute_path).parent(); + + 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 buffer; + size_t bytes_read; + while ((bytes_read = file_stream.read(buffer)) > 0) + TRY(Core::System::write(fd, buffer.span().slice(0, bytes_read))); + + 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.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(); + } + file_stream.close(); + + return {}; +} diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index aa1db9bf15..aaf23cacbb 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -6,6 +6,10 @@ project(ladybird DESCRIPTION "Ladybird Web Browser" ) +if (ANDROID) + set(BUILD_SHARED_LIBS OFF) +endif() + set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -25,12 +29,19 @@ include(cmake/EnableLLD.cmake) include(FetchContent) include(cmake/FetchLagom.cmake) +get_filename_component( + SERENITY_SOURCE_DIR "${Lagom_SOURCE_DIR}/../.." + ABSOLUTE +) + # Lagom warnings include(${Lagom_SOURCE_DIR}/../CMake/lagom_compile_options.cmake) add_compile_options(-Wno-expansion-to-defined) add_compile_options(-Wno-user-defined-literals) set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network) set(SOURCES @@ -45,13 +56,23 @@ set(SOURCES Tab.cpp ) -add_executable(ladybird ${SOURCES}) -target_link_libraries(ladybird PRIVATE Qt6::Widgets Qt::Network Lagom::Web Lagom::WebSocket Lagom::Main) - -get_filename_component( - SERENITY_SOURCE_DIR "${Lagom_SOURCE_DIR}/../.." - ABSOLUTE +qt_add_executable(ladybird ${SOURCES} + MANUAL_FINALIZATION ) +target_link_libraries(ladybird PRIVATE Qt::Widgets Qt::Network LibWeb LibWebSocket LibGL LibMain) + +set_target_properties(ladybird PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER org.serenityos.ladybird + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist" + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + +if (ANDROID) + include(cmake/AndroidExtras.cmake) +endif() add_custom_target(run COMMAND "${CMAKE_COMMAND}" -E env "SERENITY_SOURCE_DIR=${SERENITY_SOURCE_DIR}" "$" @@ -63,3 +84,10 @@ add_custom_target(debug USES_TERMINAL ) +qt_finalize_executable(ladybird) + +install(TARGETS ladybird + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + BUNDLE DESTINATION bundle + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) diff --git a/Ladybird/WebView.cpp b/Ladybird/WebView.cpp index 9fb418d9d3..363959038d 100644 --- a/Ladybird/WebView.cpp +++ b/Ladybird/WebView.cpp @@ -622,12 +622,22 @@ private: HeadlessWebSocketClientManager() { } }; +static void platform_init() +{ +#ifdef AK_OS_ANDROID + extern void android_platform_init(); + android_platform_init(); +#endif +} + void initialize_web_engine() { Web::ImageDecoding::Decoder::initialize(HeadlessImageDecoderClient::create()); Web::ResourceLoader::initialize(RequestManagerQt::create()); Web::WebSockets::WebSocketClientManager::initialize(HeadlessWebSocketClientManager::create()); + platform_init(); + Web::FrameLoader::set_default_favicon_path(String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root)); dbgln("Set favicon path to {}", String::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root)); diff --git a/Ladybird/android/AndroidManifest.xml b/Ladybird/android/AndroidManifest.xml new file mode 100644 index 0000000000..d128be4f66 --- /dev/null +++ b/Ladybird/android/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Ladybird/android/build.gradle b/Ladybird/android/build.gradle new file mode 100644 index 0000000000..4c711790f8 --- /dev/null +++ b/Ladybird/android/build.gradle @@ -0,0 +1,81 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + } +} + +repositories { + google() + mavenCentral() +} + +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qtAndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion.toInteger() + buildToolsVersion androidBuildToolsVersion + ndkVersion androidNdkVersion + + // Extract native libraries from the APK + packagingOptions.jniLibs.useLegacyPackaging true + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qtAndroidDir + '/res', 'res'] + resources.srcDirs = ['resources'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + // Do not compress Qt binary resources file + aaptOptions { + noCompress 'rcc' + } + + defaultConfig { + resConfig "en" + minSdkVersion qtMinSdkVersion + targetSdkVersion qtTargetSdkVersion + ndk.abiFilters = qtTargetAbiList.split(",") + } +} diff --git a/Ladybird/android/gradle.properties b/Ladybird/android/gradle.properties new file mode 100644 index 0000000000..263d70238a --- /dev/null +++ b/Ladybird/android/gradle.properties @@ -0,0 +1,14 @@ +# Project-wide Gradle settings. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# Enable building projects in parallel +org.gradle.parallel=true + +# Gradle caching allows reusing the build artifacts from a previous +# build with the same inputs. However, over time, the cache size will +# grow. Uncomment the following line to enable it. +#org.gradle.caching=true diff --git a/Ladybird/android/res/values/libs.xml b/Ladybird/android/res/values/libs.xml new file mode 100644 index 0000000000..fe63866f72 --- /dev/null +++ b/Ladybird/android/res/values/libs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Ladybird/android/src/org/serenityos/ladybird/TransferAssets.java b/Ladybird/android/src/org/serenityos/ladybird/TransferAssets.java new file mode 100644 index 0000000000..edf5e8352d --- /dev/null +++ b/Ladybird/android/src/org/serenityos/ladybird/TransferAssets.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2022, Andrew Kaster + * + * 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 +{ + /** + @returns 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) { + InputStream in = null; + OutputStream out = null; + try { + in = assetManager.open(fromAssetPath); + new File(toPath).createNewFile(); + out = new FileOutputStream(toPath); + copyFile(in, out); + in.close(); + in = null; + out.flush(); + out.close(); + out = null; + 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); + } + } +}; diff --git a/Ladybird/cmake/AndroidExtras.cmake b/Ladybird/cmake/AndroidExtras.cmake new file mode 100644 index 0000000000..b668d0e18e --- /dev/null +++ b/Ladybird/cmake/AndroidExtras.cmake @@ -0,0 +1,54 @@ +# Copyright (c) 2022, Andrew Kaster +# +# SPDX-License-Identifier: BSD-2-Clause +# + +# +# Source directory for androiddeployqt to use when bundling the application +# +set_property(TARGET ladybird APPEND PROPERTY + QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android +) + +# +# Android-specific sources and libs +# +target_sources(ladybird PRIVATE AndroidPlatform.cpp) +target_link_libraries(ladybird PRIVATE LibCompress LibArchive) + +# +# NDK and Qt don't ship OpenSSL for Android +# Download the prebuilt binaries from KDAB for inclusion as recommended in Qt docs. +# +include(FetchContent) +FetchContent_Declare(android_openssl + GIT_REPOSITORY https://github.com/KDAB/android_openssl + GIT_TAG origin/master + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(android_openssl) +set_property(TARGET ladybird APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${ANDROID_EXTRA_LIBS}) + +# +# Copy resources into tarball for inclusion in /assets of APK +# +set(LADYBIRD_RESOURCE_ROOT "${SERENITY_SOURCE_DIR}/Base/res") +macro(copy_res_folder folder) + add_custom_target(copy-${folder} + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${LADYBIRD_RESOURCE_ROOT}/${folder}" + "asset-bundle/res/${folder}" + ) + add_dependencies(archive-assets copy-${folder}) +endmacro() +add_custom_target(archive-assets COMMAND ${CMAKE_COMMAND} -E chdir asset-bundle tar czf ../ladybird-assets.tar.gz ./ ) +copy_res_folder(html) +copy_res_folder(fonts) +copy_res_folder(icons) +copy_res_folder(emoji) +copy_res_folder(themes) +copy_res_folder(color-palettes) +copy_res_folder(cursor-themes) +add_custom_target(copy-assets COMMAND ${CMAKE_COMMAND} -E copy_if_different ladybird-assets.tar.gz "${CMAKE_SOURCE_DIR}/android/assets") +add_dependencies(copy-assets archive-assets) +add_dependencies(ladybird copy-assets) diff --git a/Ladybird/cmake/EnableLLD.cmake b/Ladybird/cmake/EnableLLD.cmake index 52416a404d..ce0f107e00 100644 --- a/Ladybird/cmake/EnableLLD.cmake +++ b/Ladybird/cmake/EnableLLD.cmake @@ -1,4 +1,7 @@ - +# Copyright (c) 2022, Andrew Kaster +# +# SPDX-License-Identifier: BSD-2-Clause +# option(LADYBIRD_USE_LLD "Use llvm lld to link application" ON) if (LADYBIRD_USE_LLD AND NOT APPLE) find_program(LLD_LINKER NAMES "ld.lld") diff --git a/Ladybird/cmake/FetchLagom.cmake b/Ladybird/cmake/FetchLagom.cmake index ef5abeb46d..78c0a70160 100644 --- a/Ladybird/cmake/FetchLagom.cmake +++ b/Ladybird/cmake/FetchLagom.cmake @@ -25,6 +25,7 @@ if (NOT lagom_POPULATED) # FIXME: Setting target_include_directories on Lagom libraries might make this unecessary? include_directories(${lagom_SOURCE_DIR}/Userland/Libraries) + include_directories(${lagom_BINARY_DIR}/Services) include_directories(${lagom_SOURCE_DIR}) include_directories(${lagom_BINARY_DIR})