Compare commits

..

4 Commits

27 changed files with 1402 additions and 606 deletions

View File

@ -18,6 +18,9 @@ migration:
- platform: android - platform: android
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
- platform: linux
create_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
base_revision: 4d9e56e694b656610ab87fcf2efbcd226e0ed8cf
# User provided section # User provided section

View File

@ -2,5 +2,28 @@
Interactions with the system for Moxxy. Interactions with the system for Moxxy.
This library is supposed to be the successor of moxplatform, featuring This library is the successor of moxplatform, featuring
cleaner and more maintainable code. cleaner and more maintainable code.
## Implementation Status
### Android
Everything works.
### Linux
Only creating the "background service" works. For everything else, we're waiting on
[this Flutter issue](https://github.com/flutter/flutter/issues/73740), which would allow
us to implement/stub the missing native APIs.
## License
See `./LICENSE`.
## Special Thanks
Thanks to [ekasetiawans](https://github.com/ekasetiawans) for [flutter_background_service](https://github.com/ekasetiawans/flutter_background_service), which
was essentially the blueprint for the service and background service APIs. They were reimplemented
to allow the root isolate to pass some additional data to the service, which `flutter_background_service`
did not support.

View File

@ -20,13 +20,13 @@ private fun wrapError(exception: Throwable): List<Any?> {
return listOf( return listOf(
exception.code, exception.code,
exception.message, exception.message,
exception.details, exception.details
) )
} else { } else {
return listOf( return listOf(
exception.javaClass.simpleName, exception.javaClass.simpleName,
exception.toString(), exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception), "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
) )
} }
} }
@ -37,17 +37,16 @@ private fun wrapError(exception: Throwable): List<Any?> {
* @property message The error message. * @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec. * @property details The error details. Must be a datatype supported by the api codec.
*/ */
class FlutterError( class FlutterError (
val code: String, val code: String,
override val message: String? = null, override val message: String? = null,
val details: Any? = null, val details: Any? = null
) : Throwable() ) : Throwable()
enum class NotificationIcon(val raw: Int) { enum class NotificationIcon(val raw: Int) {
WARNING(0), WARNING(0),
ERROR(1), ERROR(1),
NONE(2), NONE(2);
;
companion object { companion object {
fun ofRaw(raw: Int): NotificationIcon? { fun ofRaw(raw: Int): NotificationIcon? {
@ -59,8 +58,7 @@ enum class NotificationIcon(val raw: Int) {
enum class NotificationEventType(val raw: Int) { enum class NotificationEventType(val raw: Int) {
MARKASREAD(0), MARKASREAD(0),
REPLY(1), REPLY(1),
OPEN(2), OPEN(2);
;
companion object { companion object {
fun ofRaw(raw: Int): NotificationEventType? { fun ofRaw(raw: Int): NotificationEventType? {
@ -72,8 +70,7 @@ enum class NotificationEventType(val raw: Int) {
enum class NotificationChannelImportance(val raw: Int) { enum class NotificationChannelImportance(val raw: Int) {
MIN(0), MIN(0),
HIGH(1), HIGH(1),
DEFAULT(2), DEFAULT(2);
;
companion object { companion object {
fun ofRaw(raw: Int): NotificationChannelImportance? { fun ofRaw(raw: Int): NotificationChannelImportance? {
@ -83,12 +80,12 @@ enum class NotificationChannelImportance(val raw: Int) {
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationMessageContent( data class NotificationMessageContent (
/** The textual body of the message. */ /** The textual body of the message. */
val body: String? = null, val body: String? = null,
/** The path and mime type of the media to show. */ /** The path and mime type of the media to show. */
val mime: String? = null, val mime: String? = null,
val path: String? = null, val path: String? = null
) { ) {
companion object { companion object {
@ -110,7 +107,7 @@ data class NotificationMessageContent(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationMessage( data class NotificationMessage (
/** The grouping key for the notification. */ /** The grouping key for the notification. */
val groupId: String? = null, val groupId: String? = null,
/** The sender of the message. */ /** The sender of the message. */
@ -122,7 +119,7 @@ data class NotificationMessage(
/** Milliseconds since epoch. */ /** Milliseconds since epoch. */
val timestamp: Long, val timestamp: Long,
/** The path to the avatar to use */ /** The path to the avatar to use */
val avatarPath: String? = null, val avatarPath: String? = null
) { ) {
companion object { companion object {
@ -150,7 +147,7 @@ data class NotificationMessage(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class MessagingNotification( data class MessagingNotification (
/** The title of the conversation. */ /** The title of the conversation. */
val title: String, val title: String,
/** The id of the notification. */ /** The id of the notification. */
@ -166,7 +163,7 @@ data class MessagingNotification(
/** The id for notification grouping. */ /** The id for notification grouping. */
val groupId: String? = null, val groupId: String? = null,
/** Additional data to include. */ /** Additional data to include. */
val extra: Map<String?, String?>? = null, val extra: Map<String?, String?>? = null
) { ) {
companion object { companion object {
@ -198,7 +195,7 @@ data class MessagingNotification(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class RegularNotification( data class RegularNotification (
/** The title of the notification. */ /** The title of the notification. */
val title: String, val title: String,
/** The body of the notification. */ /** The body of the notification. */
@ -210,7 +207,7 @@ data class RegularNotification(
/** The id of the notification. */ /** The id of the notification. */
val id: Long, val id: Long,
/** The icon to use. */ /** The icon to use. */
val icon: NotificationIcon, val icon: NotificationIcon
) { ) {
companion object { companion object {
@ -238,7 +235,7 @@ data class RegularNotification(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationEvent( data class NotificationEvent (
/** The notification id. */ /** The notification id. */
val id: Long, val id: Long,
/** The JID the notification was for. */ /** The JID the notification was for. */
@ -252,7 +249,7 @@ data class NotificationEvent(
*/ */
val payload: String? = null, val payload: String? = null,
/** Extra data. Only set when type == NotificationType.reply. */ /** Extra data. Only set when type == NotificationType.reply. */
val extra: Map<String?, String?>? = null, val extra: Map<String?, String?>? = null
) { ) {
companion object { companion object {
@ -278,13 +275,13 @@ data class NotificationEvent(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationI18nData( data class NotificationI18nData (
/** The content of the reply button. */ /** The content of the reply button. */
val reply: String, val reply: String,
/** The content of the "mark as read" button. */ /** The content of the "mark as read" button. */
val markAsRead: String, val markAsRead: String,
/** The text to show when *you* reply. */ /** The text to show when *you* reply. */
val you: String, val you: String
) { ) {
companion object { companion object {
@ -306,9 +303,9 @@ data class NotificationI18nData(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationGroup( data class NotificationGroup (
val id: String, val id: String,
val description: String, val description: String
) { ) {
companion object { companion object {
@ -328,7 +325,7 @@ data class NotificationGroup(
} }
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class NotificationChannel( data class NotificationChannel (
val title: String, val title: String,
val description: String, val description: String,
val id: String, val id: String,
@ -336,7 +333,7 @@ data class NotificationChannel(
val showBadge: Boolean, val showBadge: Boolean,
val groupId: String? = null, val groupId: String? = null,
val vibration: Boolean, val vibration: Boolean,
val enableLights: Boolean, val enableLights: Boolean
) { ) {
companion object { companion object {
@ -366,7 +363,6 @@ data class NotificationChannel(
) )
} }
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private object MoxxyNotificationsApiCodec : StandardMessageCodec() { private object MoxxyNotificationsApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@ -472,7 +468,6 @@ interface MoxxyNotificationsApi {
val codec: MessageCodec<Any?> by lazy { val codec: MessageCodec<Any?> by lazy {
MoxxyNotificationsApiCodec MoxxyNotificationsApiCodec
} }
/** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */ /** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) { fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) {

View File

@ -127,6 +127,7 @@ class MyAppState extends State<MyApp> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
// Create channel // Create channel
if (Platform.isAndroid) {
await MoxxyNotificationsApi().createNotificationChannels( await MoxxyNotificationsApi().createNotificationChannels(
[ [
NotificationChannel( NotificationChannel(
@ -142,6 +143,7 @@ class MyAppState extends State<MyApp> {
); );
await Permission.notification.request(); await Permission.notification.request();
}
final srv = getForegroundService(); final srv = getForegroundService();
await srv.start( await srv.start(

1
example/linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -0,0 +1,138 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "moxxy_native_example")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "org.moxxy.moxxy_native")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
# not the value here, or `flutter run` will no longer work.
#
# Any new source files that you add to the application should be added here.
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
# Apply the standard set of build settings. This can be removed for applications
# that need different build settings.
apply_standard_settings(${BINARY_NAME})
# Add dependency libraries. Add any application-specific dependencies here.
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

View File

@ -0,0 +1,88 @@
# This file controls Flutter-level build steps. It should not be edited.
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <moxxy_native/moxxy_native_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) moxxy_native_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "MoxxyNativePlugin");
moxxy_native_plugin_register_with_registrar(moxxy_native_registrar);
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,24 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
moxxy_native
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)

6
example/linux/main.cc Normal file
View File

@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}

View File

@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "moxxy_native_example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "moxxy_native_example");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}

View File

@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_

View File

@ -51,6 +51,9 @@
# Android # Android
pinnedJDK sdk ktlint pinnedJDK sdk ktlint
# Linux
clang cmake gtk3 ninja pkg-config xz pcre2 glib
# Flutter # Flutter
flutterVersion flutterVersion

View File

@ -20,10 +20,10 @@ class MoxxyBackgroundServiceApi {
Future<String> getExtraData() async { Future<String> getExtraData() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData',
codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
await channel.send(null) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',
@ -47,7 +47,8 @@ class MoxxyBackgroundServiceApi {
Future<void> setNotificationBody(String arg_body) async { Future<void> setNotificationBody(String arg_body) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody',
codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList =
await channel.send(<Object?>[arg_body]) as List<Object?>?; await channel.send(<Object?>[arg_body]) as List<Object?>?;
@ -69,7 +70,8 @@ class MoxxyBackgroundServiceApi {
Future<void> sendData(String arg_data) async { Future<void> sendData(String arg_data) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData',
codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList =
await channel.send(<Object?>[arg_data]) as List<Object?>?; await channel.send(<Object?>[arg_data]) as List<Object?>?;
@ -93,8 +95,7 @@ class MoxxyBackgroundServiceApi {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel.send(null) as List<Object?>?;
await channel.send(null) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',

View File

@ -0,0 +1,53 @@
import 'dart:convert';
import 'dart:isolate';
import 'dart:ui';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/src/service/background/base.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:uuid/uuid.dart';
class IsolateBackgroundService extends BackgroundService {
IsolateBackgroundService(this._sendPort);
final SendPort _sendPort;
final ReceivePort receivePort = ReceivePort();
/// A logger.
final Logger _log = Logger('IsolateBackgroundService');
@override
Future<void> send(BackgroundEvent event, {String? id}) async {
final data = DataWrapper(
id ?? const Uuid().v4(),
event,
);
_sendPort.send(jsonEncode(data.toJson()));
}
@override
Future<void> init(
ServiceConfig config,
) async {
// Ensure that the Dart executor is ready to use plugins
// NOTE: We're not allowed to use this here. Maybe reusing the RootIsolateToken
// (See IsolateForegroundService) helps?
// WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
// Register the channel for Foreground -> Service communication
receivePort.listen((data) async {
// TODO(Unknown): Maybe do something smarter like pigeon and use Lists instead of Maps
await config
.handleData(jsonDecode(data! as String) as Map<String, dynamic>);
});
// Start execution
_log.finest('Setup complete. Calling main entrypoint...');
await config.entrypoint(config.initialLocale);
}
@override
void setNotificationBody(String body) {}
}

View File

@ -0,0 +1,15 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
class IsolateForegroundServiceDataSender
extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
IsolateForegroundServiceDataSender(this._port);
final SendPort _port;
@override
Future<void> sendDataImpl(DataWrapper<JsonImplementation> data) async {
_port.send(jsonEncode(data.toJson()));
}
}

View File

@ -1,8 +1,4 @@
import 'dart:io';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/datasender/pigeon.dart';
import 'package:moxxy_native/src/service/exceptions.dart';
typedef ForegroundServiceDataSender typedef ForegroundServiceDataSender
= AwaitableDataSender<BackgroundCommand, BackgroundEvent>; = AwaitableDataSender<BackgroundCommand, BackgroundEvent>;
@ -10,11 +6,3 @@ typedef ForegroundServiceDataSender
abstract class BackgroundCommand implements JsonImplementation {} abstract class BackgroundCommand implements JsonImplementation {}
abstract class BackgroundEvent implements JsonImplementation {} abstract class BackgroundEvent implements JsonImplementation {}
ForegroundServiceDataSender getForegroundDataSender(MoxxyServiceApi api) {
if (Platform.isAndroid) {
return PigeonForegroundServiceDataSender(api);
} else {
throw UnsupportedPlatformException();
}
}

View File

@ -0,0 +1,28 @@
import 'dart:io';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/entrypoints/isolate.dart';
import 'package:moxxy_native/src/service/entrypoints/pigeon.dart';
import 'package:moxxy_native/src/service/exceptions.dart';
typedef PlatformEntrypointCallback = Future<void> Function(dynamic);
ServiceConfig getServiceConfig(
HandleEventCallback srvHandleData,
HandleEventCallback uiHandleData,
String initialLocale,
) {
PlatformEntrypointCallback entrypoint;
if (Platform.isAndroid) {
entrypoint = pigeonEntrypoint;
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
entrypoint = isolateEntrypoint;
} else {
throw UnsupportedPlatformException();
}
return ServiceConfig(
entrypoint,
srvHandleData,
initialLocale,
);
}

View File

@ -0,0 +1,30 @@
import 'dart:isolate';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxy_native/src/service/background/base.dart';
import 'package:moxxy_native/src/service/background/isolate.dart';
import 'package:moxxy_native/src/service/config.dart';
@pragma('vm:entry-point')
Future<void> isolateEntrypoint(dynamic parameters) async {
parameters as List<dynamic>;
final sendPort = parameters[0] as SendPort;
final config = ServiceConfig.fromString(parameters[1] as String);
// This allows us to use the root isolate's method channels.
// See https://medium.com/flutter/introducing-background-isolate-channels-7a299609cad8
BackgroundIsolateBinaryMessenger.ensureInitialized(
parameters[2] as RootIsolateToken,
);
// Set up the background service
final srv = IsolateBackgroundService(sendPort);
GetIt.I.registerSingleton<BackgroundService>(srv);
// Reply back with the new send port
sendPort.send(srv.receivePort.sendPort);
// Run the entrypoint
await srv.init(config);
}

View File

@ -8,7 +8,7 @@ import 'package:moxxy_native/src/service/config.dart';
/// An entrypoint that should be used when the service runs /// An entrypoint that should be used when the service runs
/// in a new Flutter Engine. /// in a new Flutter Engine.
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> pigeonEntrypoint() async { Future<void> pigeonEntrypoint(dynamic _) async {
// ignore: avoid_print // ignore: avoid_print
print('androidEntrypoint: Called on new FlutterEngine'); print('androidEntrypoint: Called on new FlutterEngine');

View File

@ -3,6 +3,7 @@ import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/src/service/config.dart'; import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart'; import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:moxxy_native/src/service/exceptions.dart'; import 'package:moxxy_native/src/service/exceptions.dart';
import 'package:moxxy_native/src/service/foreground/isolate.dart';
import 'package:moxxy_native/src/service/foreground/pigeon.dart'; import 'package:moxxy_native/src/service/foreground/pigeon.dart';
/// Wrapper API that is only available to the UI isolate. /// Wrapper API that is only available to the UI isolate.
@ -40,6 +41,8 @@ ForegroundService getForegroundService() {
if (_service == null) { if (_service == null) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
_service = PigeonForegroundService(); _service = PigeonForegroundService();
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
_service = IsolateForegroundService();
} else { } else {
throw UnsupportedPlatformException(); throw UnsupportedPlatformException();
} }

View File

@ -0,0 +1,98 @@
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'dart:ui';
import 'package:logging/logging.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/isolate.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:moxxy_native/src/service/entrypoints/isolate.dart';
import 'package:moxxy_native/src/service/foreground/base.dart';
class IsolateForegroundService extends ForegroundService {
/// The port on which we receive data from the isolate.
final ReceivePort _receivePort = ReceivePort();
/// The port on which we send data to the isolate.
late final SendPort _sendPort;
/// A completer that indicates when _sendPort has been set.
/// For more notes, see the comment in [start].
Completer<void>? _sendPortCompleter = Completer<void>();
/// The data sender backing this class.
late final IsolateForegroundServiceDataSender _dataSender;
/// A logger.
final Logger _log = Logger('IsolateForegroundService');
@override
Future<void> attach(
HandleEventCallback handleData,
) async {
_receivePort.asBroadcastStream().listen((data) async {
if (data is SendPort) {
// Set the send port.
_sendPort = data;
// Resolve the waiting future.
assert(
_sendPortCompleter != null,
'_sendPort should only be received once!',
);
_sendPortCompleter?.complete();
return;
}
await handleData(
jsonDecode(data! as String) as Map<String, dynamic>,
);
});
}
@override
Future<void> start(
ServiceConfig config,
HandleEventCallback uiHandleData,
) async {
// Listen for events
await attach(uiHandleData);
await Isolate.spawn(
isolateEntrypoint,
[
_receivePort.sendPort,
config.toString(),
RootIsolateToken.instance!,
],
);
// Wait for [_sendPort] to get set.
// The issue is that [_receivePort] provides a stream that only one listener can listen to.
// This means that we cannot do `await _receivePort.first`. To work around this, we just cram
// an approximation of `_receivePort.first` into the actual listener.
await _sendPortCompleter!.future;
_sendPortCompleter = null;
// Create the data sender
_dataSender = IsolateForegroundServiceDataSender(_sendPort);
_log.finest('Background service started...');
}
@override
Future<bool> isRunning() async => false;
@override
ForegroundServiceDataSender getDataSender() => _dataSender;
@override
Future<BackgroundEvent?> send(
BackgroundCommand command, {
bool awaitable = true,
}) {
return _dataSender.sendData(
command,
awaitable: awaitable,
);
}
}

47
linux/CMakeLists.txt Normal file
View File

@ -0,0 +1,47 @@
# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)
# Project-level configuration.
set(PROJECT_NAME "moxxy_native")
project(${PROJECT_NAME} LANGUAGES CXX)
# This value is used when generating builds using this plugin, so it must
# not be changed.
set(PLUGIN_NAME "moxxy_native_plugin")
# Define the plugin library target. Its name must not be changed (see comment
# on PLUGIN_NAME above).
#
# Any new source files that you add to the plugin should be added here.
add_library(${PLUGIN_NAME} SHARED
"moxxy_native_plugin.cc"
)
# Apply a standard set of build settings that are configured in the
# application-level CMakeLists.txt. This can be removed for plugins that want
# full control over build settings.
apply_standard_settings(${PLUGIN_NAME})
# Symbols are hidden by default to reduce the chance of accidental conflicts
# between plugins. This should not be removed; any symbols that should be
# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro.
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
# Source include directories and library dependencies. Add any plugin-specific
# dependencies here.
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(moxxy_native_bundled_libraries
""
PARENT_SCOPE
)

View File

@ -0,0 +1,26 @@
#ifndef FLUTTER_PLUGIN_MOXXY_NATIVE_PLUGIN_H_
#define FLUTTER_PLUGIN_MOXXY_NATIVE_PLUGIN_H_
#include <flutter_linux/flutter_linux.h>
G_BEGIN_DECLS
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define FLUTTER_PLUGIN_EXPORT
#endif
typedef struct _MoxxyNativePlugin MoxxyNativePlugin;
typedef struct {
GObjectClass parent_class;
} MoxxyNativePluginClass;
FLUTTER_PLUGIN_EXPORT GType moxxy_native_plugin_get_type();
FLUTTER_PLUGIN_EXPORT void moxxy_native_plugin_register_with_registrar(
FlPluginRegistrar* registrar);
G_END_DECLS
#endif // FLUTTER_PLUGIN_MOXXY_NATIVE_PLUGIN_H_

View File

@ -0,0 +1,70 @@
#include "include/moxxy_native/moxxy_native_plugin.h"
#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <sys/utsname.h>
#include <cstring>
#define MOXXY_NATIVE_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), moxxy_native_plugin_get_type(), \
MoxxyNativePlugin))
struct _MoxxyNativePlugin {
GObject parent_instance;
};
G_DEFINE_TYPE(MoxxyNativePlugin, moxxy_native_plugin, g_object_get_type())
// Called when a method call is received from Flutter.
static void moxxy_native_plugin_handle_method_call(
MoxxyNativePlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = nullptr;
const gchar* method = fl_method_call_get_name(method_call);
if (strcmp(method, "getPlatformVersion") == 0) {
struct utsname uname_data = {};
uname(&uname_data);
g_autofree gchar *version = g_strdup_printf("Linux %s", uname_data.version);
g_autoptr(FlValue) result = fl_value_new_string(version);
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
static void moxxy_native_plugin_dispose(GObject* object) {
G_OBJECT_CLASS(moxxy_native_plugin_parent_class)->dispose(object);
}
static void moxxy_native_plugin_class_init(MoxxyNativePluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = moxxy_native_plugin_dispose;
}
static void moxxy_native_plugin_init(MoxxyNativePlugin* self) {}
static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
gpointer user_data) {
MoxxyNativePlugin* plugin = MOXXY_NATIVE_PLUGIN(user_data);
moxxy_native_plugin_handle_method_call(plugin, method_call);
}
void moxxy_native_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
MoxxyNativePlugin* plugin = MOXXY_NATIVE_PLUGIN(
g_object_new(moxxy_native_plugin_get_type(), nullptr));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
g_autoptr(FlMethodChannel) channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
"moxxy_native",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(channel, method_call_cb,
g_object_ref(plugin),
g_object_unref);
g_object_unref(plugin);
}

View File

@ -1,6 +1,6 @@
name: moxxy_native name: moxxy_native
description: Interactions with the system for Moxxy description: Interactions with the system for Moxxy
version: 0.1.0 version: 0.2.0
publish_to: https://git.polynom.me/api/packages/Moxxy/pub publish_to: https://git.polynom.me/api/packages/Moxxy/pub
homepage: homepage:
@ -29,3 +29,5 @@ flutter:
android: android:
package: org.moxxy.moxxy_native package: org.moxxy.moxxy_native
pluginClass: MoxxyNativePlugin pluginClass: MoxxyNativePlugin
linux:
pluginClass: MoxxyNativePlugin