Compare commits

...

17 Commits

Author SHA1 Message Date
2ba5674850 chore(all): Release version 0.3.2 2023-09-20 22:13:50 +02:00
ed2a928d12 feat(android): Use .jpg instead of .jpeg 2023-09-20 22:12:53 +02:00
3c0db620c8 fix(android): Prevent filename.jpg.png 2023-09-20 22:11:49 +02:00
c14b3a7f58 fix(android): Use the BUFFER_SIZE constant 2023-09-20 19:26:34 +02:00
53b6b65e70 chore(all): Bump version to 0.3.1 2023-09-20 14:19:26 +02:00
2adefecc92 fix(android): Fix multiple notification issues
- Fix updating the notification causing another vibration
- Fix the "mark as read" intent being stuck
2023-09-20 14:18:08 +02:00
4ec44ab3e1 fix(android): Send notification events again 2023-09-20 13:55:03 +02:00
cab031bd07 fix(android): Fix keyboard height events being sent over the notification stream 2023-09-20 13:45:13 +02:00
6b4b15bb87 fix(android): "Fix" wrong file extension on image picker usage 2023-09-20 13:44:52 +02:00
c905b3242d chore(all): Bump version to 0.3.0 2023-09-18 20:50:50 +02:00
f949b008b3 feat(android): Add the keyboard height code 2023-09-18 20:50:08 +02:00
1852f2d198 fix(android): Fix wrong file provider id 2023-09-18 18:40:37 +02:00
44187675c7 feat(android): Implement sharing internal files and text 2023-09-18 17:58:16 +02:00
f971a0e078 chore(all): Relase 0.2.0 2023-09-10 22:09:24 +02:00
d5e86911ea chore(docs): Update README 2023-09-10 22:07:39 +02:00
e0ccbc8b85 feat(linux): Creating a background service works 2023-09-10 22:02:39 +02:00
01374a8eb9 feat(linux): Get the isolate implementation somewhat working 2023-09-10 21:58:07 +02:00
43 changed files with 1316 additions and 106 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,32 @@
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.
Thanks to [nschairer](https://github.com/nschairer) for [flutter_keyboard_height](https://github.com/nschairer/keyboard_height_plugin), which was the base for keeping track of the keyboard height.
Due to having an issue with the height calculation if the Android device uses gesture navigation, I
[forked the package](https://git.polynom.me/moxxy/keyboard_height_plugin) and modified the height calculation.

View File

@@ -2,6 +2,12 @@ package org.moxxy.moxxy_native
const val TAG = "moxxy_native" const val TAG = "moxxy_native"
// The event channel name for the keyboard height
const val KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME = "org.moxxy.moxxyv2/keyboard_stream"
// The event channel name for notification events
const val NOTIFICATION_EVENT_CHANNEL_NAME = "org.moxxy.moxxyv2/notification_stream"
// The size of buffers to use for various operations // The size of buffers to use for various operations
const val BUFFER_SIZE = 4096 const val BUFFER_SIZE = 4096

View File

@@ -25,10 +25,12 @@ import org.moxxy.moxxy_native.media.MediaImplementation
import org.moxxy.moxxy_native.media.MoxxyMediaApi import org.moxxy.moxxy_native.media.MoxxyMediaApi
import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi
import org.moxxy.moxxy_native.notifications.NotificationEvent import org.moxxy.moxxy_native.notifications.NotificationEvent
import org.moxxy.moxxy_native.notifications.NotificationStreamHandler
import org.moxxy.moxxy_native.notifications.NotificationsImplementation import org.moxxy.moxxy_native.notifications.NotificationsImplementation
import org.moxxy.moxxy_native.picker.FilePickerType import org.moxxy.moxxy_native.picker.FilePickerType
import org.moxxy.moxxy_native.picker.MoxxyPickerApi import org.moxxy.moxxy_native.picker.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener import org.moxxy.moxxy_native.picker.PickerResultListener
import org.moxxy.moxxy_native.platform.KeyboardStreamHandler
import org.moxxy.moxxy_native.platform.MoxxyPlatformApi import org.moxxy.moxxy_native.platform.MoxxyPlatformApi
import org.moxxy.moxxy_native.platform.PlatformImplementation import org.moxxy.moxxy_native.platform.PlatformImplementation
import org.moxxy.moxxy_native.service.BackgroundService import org.moxxy.moxxy_native.service.BackgroundService
@@ -36,23 +38,6 @@ import org.moxxy.moxxy_native.service.MoxxyServiceApi
import org.moxxy.moxxy_native.service.PluginTracker import org.moxxy.moxxy_native.service.PluginTracker
import org.moxxy.moxxy_native.service.ServiceImplementation import org.moxxy.moxxy_native.service.ServiceImplementation
object MoxxyEventChannels {
var notificationChannel: EventChannel? = null
var notificationEventSink: EventChannel.EventSink? = null
}
object NotificationStreamHandler : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "NotificationStreamHandler: Attached stream")
MoxxyEventChannels.notificationEventSink = events
}
override fun onCancel(arguments: Any?) {
Log.d(TAG, "NotificationStreamHandler: Detached stream")
MoxxyEventChannels.notificationEventSink = null
}
}
/* /*
* Hold the last notification event in case we did a cold start. * Hold the last notification event in case we did a cold start.
*/ */
@@ -102,6 +87,14 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, BroadcastR
IntentFilter(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY), IntentFilter(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY),
) )
// Special handling for the keyboard height
val keyboardChannel = EventChannel(flutterPluginBinding.getBinaryMessenger(), KEYBOARD_HEIGHT_EVENT_CHANNEL_NAME)
keyboardChannel?.setStreamHandler(KeyboardStreamHandler)
// Special handling from notification events
val notificationChannel = EventChannel(flutterPluginBinding.getBinaryMessenger(), NOTIFICATION_EVENT_CHANNEL_NAME)
notificationChannel?.setStreamHandler(NotificationStreamHandler)
// Register the picker handler // Register the picker handler
pickerListener = PickerResultListener(context!!) pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine") Log.d(TAG, "Attached to engine")
@@ -118,20 +111,24 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, BroadcastR
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
binding.addActivityResultListener(pickerListener) binding.addActivityResultListener(pickerListener)
KeyboardStreamHandler.activity = activity
Log.d(TAG, "Attached to activity") Log.d(TAG, "Attached to activity")
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
activity = null activity = null
KeyboardStreamHandler.activity = null
Log.d(TAG, "Detached from activity") Log.d(TAG, "Detached from activity")
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
KeyboardStreamHandler.activity = activity
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
activity = null activity = null
KeyboardStreamHandler.activity = null
Log.d(TAG, "Detached from activity") Log.d(TAG, "Detached from activity")
} }

View File

@@ -1,6 +1,24 @@
package org.moxxy.moxxy_native.content package org.moxxy.moxxy_native.content
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.R import org.moxxy.moxxy_native.R
import java.io.File
class MoxxyFileProvider : FileProvider(R.xml.file_paths) class MoxxyFileProvider : FileProvider(R.xml.file_paths) {
companion object {
/*
* Convert a path @path inside a sharable storage directory into a content URI, given
* the application's context @context.
* */
fun getUriForPath(context: Context, path: String): Uri {
return getUriForFile(
context,
MOXXY_FILEPROVIDER_ID,
File(path),
)
}
}
}

View File

@@ -1,8 +1,6 @@
package org.moxxy.moxxy_native.notifications package org.moxxy.moxxy_native.notifications
import android.content.Intent import android.content.Intent
import android.util.Log
import org.moxxy.moxxy_native.TAG
/* /*
* Extract all user-added extra key-value pairs from @intent. * Extract all user-added extra key-value pairs from @intent.
@@ -10,9 +8,7 @@ import org.moxxy.moxxy_native.TAG
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> { fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>() val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach { intent.extras?.keySet()!!.forEach {
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
if (it.startsWith("payload_")) { if (it.startsWith("payload_")) {
Log.d(TAG, "Adding $it")
extras[it.substring(8)] = intent.extras!!.getString(it) extras[it.substring(8)] = intent.extras!!.getString(it)
} }
} }

View File

@@ -15,7 +15,6 @@ import androidx.core.app.RemoteInput
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.MoxxyEventChannels
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
@@ -50,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
} }
private fun handleMarkAsRead(context: Context, intent: Intent) { private fun handleMarkAsRead(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -65,7 +64,7 @@ class NotificationReceiver : BroadcastReceiver() {
private fun handleReply(context: Context, intent: Intent) { private fun handleReply(context: Context, intent: Intent) {
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY) val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -164,8 +163,8 @@ class NotificationReceiver : BroadcastReceiver() {
} }
} }
fun handleTap(context: Context, intent: Intent) { private fun handleTap(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success( NotificationStreamHandler.sink?.success(
NotificationEvent( NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1), intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!, intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,

View File

@@ -0,0 +1,22 @@
package org.moxxy.moxxy_native.notifications
import android.util.Log
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.TAG
object NotificationStreamHandler : EventChannel.StreamHandler {
// The event sink to use for sending notification events to the service.
var sink: EventChannel.EventSink? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
// "register" the event sink
sink = events
Log.d(TAG, "NotificationStreamHandler: Attached stream")
}
override fun onCancel(arguments: Any?) {
sink = null
Log.d(TAG, "NotificationStreamHandler: Detached stream")
}
}

View File

@@ -14,10 +14,8 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person import androidx.core.app.Person
import androidx.core.app.RemoteInput import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
@@ -27,7 +25,7 @@ import org.moxxy.moxxy_native.REPLY_ACTION
import org.moxxy.moxxy_native.REPLY_TEXT_KEY import org.moxxy.moxxy_native.REPLY_TEXT_KEY
import org.moxxy.moxxy_native.TAG import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.TAP_ACTION import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File import org.moxxy.moxxy_native.content.MoxxyFileProvider
class NotificationsImplementation(private val context: Context) : MoxxyNotificationsApi { class NotificationsImplementation(private val context: Context) : MoxxyNotificationsApi {
override fun createNotificationGroups(groups: List<NotificationGroup>) { override fun createNotificationGroups(groups: List<NotificationGroup>) {
@@ -96,7 +94,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
context.applicationContext, context.applicationContext,
0, 0,
replyIntent, replyIntent,
PendingIntent.FLAG_MUTABLE, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
val replyAction = NotificationCompat.Action.Builder( val replyAction = NotificationCompat.Action.Builder(
R.drawable.reply, R.drawable.reply,
@@ -115,13 +113,14 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
notification.extra?.forEach { notification.extra?.forEach {
putExtra("payload_${it.key}", it.value) putExtra("payload_${it.key}", it.value)
Log.d(TAG, "Adding payload_${it.key} -> ${it.value}")
} }
} }
val markAsReadPendingIntent = PendingIntent.getBroadcast( val markAsReadPendingIntent = PendingIntent.getBroadcast(
context.applicationContext, context.applicationContext,
0, 0,
markAsReadIntent, markAsReadIntent,
PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
val markAsReadAction = NotificationCompat.Action.Builder( val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.mark_as_read, R.drawable.mark_as_read,
@@ -207,11 +206,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
) )
// If we got an image, turn it into a content URI and set it // If we got an image, turn it into a content URI and set it
if (message.content.mime != null && message.content.path != null) { if (message.content.mime != null && message.content.path != null) {
val fileUri = FileProvider.getUriForFile( val fileUri = MoxxyFileProvider.getUriForPath(context, message.content.path)
context,
MOXXY_FILEPROVIDER_ID,
File(message.content.path),
)
msg.apply { msg.apply {
setData(message.content.mime, fileUri) setData(message.content.mime, fileUri)
@@ -256,7 +251,7 @@ class NotificationsImplementation(private val context: Context) : MoxxyNotificat
setCategory(Notification.CATEGORY_MESSAGE) setCategory(Notification.CATEGORY_MESSAGE)
// Prevent no notification when we replied before // Prevent no notification when we replied before
setOnlyAlertOnce(false) setOnlyAlertOnce(true)
// Automatically dismiss the notification on tap // Automatically dismiss the notification on tap
setAutoCancel(true) setAutoCancel(true)

View File

@@ -0,0 +1,13 @@
package org.moxxy.moxxy_native.picker
object MimeUtils {
// A reverse-mapping of image mime types to their commonly used file extension.
val imageMimeTypesToFileExtension = mapOf(
"image/png" to ".png",
"image/apng" to ".apng",
"image/avif" to ".avif",
"image/gif" to ".gif",
"image/jpeg" to ".jpg",
"image/webp" to ".webp",
)
}

View File

@@ -6,10 +6,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.OpenableColumns import android.provider.MediaStore.Images
import android.util.Log import android.util.Log
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
import org.moxxy.moxxy_native.AsyncRequestTracker import org.moxxy.moxxy_native.AsyncRequestTracker
import org.moxxy.moxxy_native.BUFFER_SIZE
import org.moxxy.moxxy_native.PICK_FILES_REQUEST import org.moxxy.moxxy_native.PICK_FILES_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_REQUEST import org.moxxy.moxxy_native.PICK_FILE_REQUEST
import org.moxxy.moxxy_native.PICK_FILE_WITH_DATA_REQUEST import org.moxxy.moxxy_native.PICK_FILE_WITH_DATA_REQUEST
@@ -20,6 +21,26 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
/*
* Attempt to replace the file extension in @fileName with @newExtension. If @newExtension is null,
* then @fileName is returned verbatim.
* */
private fun maybeReplaceExtension(fileName: String, newExtension: String?): String {
if (newExtension == null) {
return fileName
}
assert(newExtension[0] == '.')
val parts = fileName.split(".")
return if (parts.size == 1) {
"$fileName$newExtension"
} else {
// Split at the ".", join all but the list end together and append the new extension
val fileNameWithoutExtension = parts.subList(0, parts.size - 1).joinToString(".")
"$fileNameWithoutExtension$newExtension"
}
}
class PickerResultListener(private val context: Context) : ActivityResultListener { class PickerResultListener(private val context: Context) : ActivityResultListener {
/* /*
* Attempt to deduce the filename for the URI @uri. * Attempt to deduce the filename for the URI @uri.
@@ -29,10 +50,22 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
private fun queryFileName(context: Context, uri: Uri): String { private fun queryFileName(context: Context, uri: Uri): String {
var result: String? = null var result: String? = null
if (uri.scheme == "content") { if (uri.scheme == "content") {
val projection = arrayOf(
Images.Media._ID,
Images.Media.MIME_TYPE,
Images.Media.DISPLAY_NAME,
)
val cursor = context.contentResolver.query(uri, null, null, null, null) val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor.use { cursor -> cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) val mimeType = cursor.getString(cursor.getColumnIndex(Images.Media.MIME_TYPE))
val displayName = cursor.getString(cursor.getColumnIndex(Images.Media.DISPLAY_NAME))
val fileExtension = MimeUtils.imageMimeTypesToFileExtension[mimeType]
// Note: This is a workaround for the Dart image library failing to parse the file
// because displayName somehow is always ".jpg", which confuses image.
result = maybeReplaceExtension(displayName, fileExtension)
Log.d(TAG, "Returning $result as filename (MIME: $mimeType)")
} }
} }
} }
@@ -51,7 +84,7 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
if (Build.VERSION.SDK_INT >= 33) { if (Build.VERSION.SDK_INT >= 33) {
android.os.FileUtils.copy(input, output) android.os.FileUtils.copy(input, output)
} else { } else {
val buffer = ByteArray(4096) val buffer = ByteArray(BUFFER_SIZE)
while (input.read(buffer).also {} != -1) { while (input.read(buffer).also {} != -1) {
output.write(buffer) output.write(buffer)
} }
@@ -94,7 +127,7 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
} }
val returnBuffer = mutableListOf<Byte>() val returnBuffer = mutableListOf<Byte>()
val readBuffer = ByteArray(4096) val readBuffer = ByteArray(BUFFER_SIZE)
try { try {
val inputStream = context.contentResolver.openInputStream(data!!.data!!)!! val inputStream = context.contentResolver.openInputStream(data!!.data!!)!!
while (inputStream.read(readBuffer).also {} != -1) { while (inputStream.read(readBuffer).also {} != -1) {

View File

@@ -0,0 +1,72 @@
package org.moxxy.moxxy_native.platform
import android.app.Activity
import android.graphics.Rect
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.TAG
object KeyboardStreamHandler : EventChannel.StreamHandler {
// The currently active activity. Set by @MoxxyNativePlugin.
var activity: Activity? = null
// The current bottom inset.
private var bottomInset: Int = 0
// The current event sink to use for sending events to the UI.
private var sink: EventChannel.EventSink? = null
private fun handleKeyboardHeightCheck(rootView: View?) {
rootView?.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val r = Rect()
rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = rootView.height
// Also subtract the height of the bottom inset as the SafeArea with "bottom: false"
// allows us to draw under the bottom system bar, if it is there.
val keypadHeight = screenHeight - r.bottom - bottomInset
val displayMetrics = activity?.resources?.displayMetrics
val logicalKeypadHeight = keypadHeight / (displayMetrics?.density ?: 1f)
if (keypadHeight > screenHeight * 0.15) {
sink?.success(logicalKeypadHeight.toDouble())
} else {
sink?.success(0.0)
}
}
})
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
// "register" the event sink
sink = events
val rootView = activity?.window?.decorView?.rootView
handleKeyboardHeightCheck(rootView)
if (rootView != null) {
ViewCompat.setOnApplyWindowInsetsListener(rootView!!) { _, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val triggerEvent = bottomInset != insets.bottom
bottomInset = insets.bottom
// Notify in case the inset changed
if (triggerEvent) handleKeyboardHeightCheck(rootView)
WindowInsetsCompat.CONSUMED
}
}
Log.d(TAG, "KeyboardStreamHandler: Attached stream")
}
override fun onCancel(arguments: Any?) {
sink = null
Log.d(TAG, "KeyboardStreamHandler: Detached stream")
}
}

View File

@@ -8,6 +8,8 @@ import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> { private fun wrapResult(result: Any?): List<Any?> {
return listOf(result) return listOf(result)
@@ -41,17 +43,66 @@ class FlutterError(
val details: Any? = null, val details: Any? = null,
) : Throwable() ) : Throwable()
/** Generated class from Pigeon that represents data sent in messages. */
data class ShareItem(
val path: String? = null,
val mime: String,
val text: String? = null,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): ShareItem {
val path = list[0] as String?
val mime = list[1] as String
val text = list[2] as String?
return ShareItem(path, mime, text)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
path,
mime,
text,
)
}
}
@Suppress("UNCHECKED_CAST")
private object MoxxyPlatformApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ShareItem.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is ShareItem -> {
stream.write(128)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyPlatformApi { interface MoxxyPlatformApi {
fun getPersistentDataPath(): String fun getPersistentDataPath(): String
fun getCacheDataPath(): String fun getCacheDataPath(): String
fun openBatteryOptimisationSettings() fun openBatteryOptimisationSettings()
fun isIgnoringBatteryOptimizations(): Boolean fun isIgnoringBatteryOptimizations(): Boolean
fun shareItems(items: List<ShareItem>, genericMimeType: String)
companion object { companion object {
/** The codec used by MoxxyPlatformApi. */ /** The codec used by MoxxyPlatformApi. */
val codec: MessageCodec<Any?> by lazy { val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec() MoxxyPlatformApiCodec
} }
/** Sets up an instance of `MoxxyPlatformApi` to handle messages through the `binaryMessenger`. */ /** Sets up an instance of `MoxxyPlatformApi` to handle messages through the `binaryMessenger`. */
@@ -122,6 +173,26 @@ interface MoxxyPlatformApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.shareItems", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val itemsArg = args[0] as List<ShareItem>
val genericMimeTypeArg = args[1] as String
var wrapped: List<Any?>
try {
api.shareItems(itemsArg, genericMimeTypeArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
} }
} }
} }

View File

@@ -5,6 +5,8 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import androidx.core.app.ShareCompat
import org.moxxy.moxxy_native.content.MoxxyFileProvider
class PlatformImplementation(private val context: Context) : MoxxyPlatformApi { class PlatformImplementation(private val context: Context) : MoxxyPlatformApi {
override fun getPersistentDataPath(): String { override fun getPersistentDataPath(): String {
@@ -27,4 +29,28 @@ class PlatformImplementation(private val context: Context) : MoxxyPlatformApi {
val pm = context.getSystemService(PowerManager::class.java) val pm = context.getSystemService(PowerManager::class.java)
return pm.isIgnoringBatteryOptimizations(context.packageName) return pm.isIgnoringBatteryOptimizations(context.packageName)
} }
override fun shareItems(items: List<ShareItem>, genericMimeType: String) {
// Empty lists make no sense
assert(items.isNotEmpty())
// Convert the paths to content URIs
val builder = ShareCompat.IntentBuilder(context).setType(genericMimeType)
for (item in items) {
assert(item.text == null && item.path != null || item.text != null && item.path == null)
if (item.text != null) {
builder.setText(item.text)
} else if (item.path != null) {
builder.addStream(MoxxyFileProvider.getUriForPath(context, item.path))
}
}
// We cannot just use startChooser() because then Android complains that we're not attached
// to an Activity. So, we just ask it to start a new one.
val intent = builder.createChooserIntent().apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
} }

View File

@@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View File

@@ -1,10 +1,10 @@
// ignore_for_file: avoid_print // ignore_for_file: avoid_print
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:moxxy_native/moxxy_native.dart'; import 'package:moxxy_native/moxxy_native.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@pragma('vm:entrypoint') @pragma('vm:entrypoint')
@@ -50,6 +50,19 @@ class TestEvent extends BackgroundEvent {
class MyAppState extends State<MyApp> { class MyAppState extends State<MyApp> {
String? imagePath; String? imagePath;
@override
void initState() {
super.initState();
const EventChannel('org.moxxy.moxxyv2/notification_stream')
.receiveBroadcastStream()
.listen(
(event) {
print('Keyboard height: ${event as double}');
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
@@ -127,6 +140,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 +156,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(
@@ -161,7 +176,64 @@ class MyAppState extends State<MyApp> {
awaitable: false, awaitable: false,
); );
}, },
child: const Text('Start foreground service')), child: const Text('Start foreground service'),
),
TextButton(
onPressed: () async {
// Pick a file and copy it into the internal storage directory
final mediaDir = Directory(
p.join(
await MoxxyPlatformApi().getPersistentDataPath(),
'media',
),
);
if (!mediaDir.existsSync()) {
await mediaDir.create(recursive: true);
}
final pickResult = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, true);
if (pickResult.isEmpty) return;
final shareItems = List<ShareItem>.empty(growable: true);
for (final result in pickResult) {
final mediaDirPath = p.join(
mediaDir.path,
p.basename(result!),
);
await File(result).copy(mediaDirPath);
shareItems.add(
ShareItem(
path: mediaDirPath,
mime: 'image/jpeg',
),
);
}
// Share with the system
await MoxxyPlatformApi().shareItems(
shareItems,
'image/*',
);
},
child: const Text('Share internal files'),
),
TextButton(
onPressed: () async {
// Share with the system
await MoxxyPlatformApi().shareItems(
[
ShareItem(
mime: 'text/plain',
text: 'Hello World!',
),
],
'text/*',
);
},
child: const Text('Share some text'),
),
const TextField(),
], ],
), ),
), ),

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

@@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.10.0" version: "2.11.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -21,10 +21,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.3.0"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.1"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -95,10 +95,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.5" version: "0.6.7"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -119,10 +119,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.13" version: "0.12.15"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -135,10 +135,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.0" version: "1.9.1"
moxlib: moxlib:
dependency: transitive dependency: transitive
description: description:
@@ -153,15 +153,15 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.1.0" version: "0.2.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.2" version: "1.8.3"
permission_handler: permission_handler:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -267,10 +267,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" version: "0.5.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -296,5 +296,5 @@ packages:
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
sdks: sdks:
dart: ">=2.19.6 <3.0.0" dart: ">=3.0.0-0 <4.0.0"
flutter: ">=2.8.0" flutter: ">=2.8.0"

View File

@@ -31,6 +31,7 @@ dependencies:
permission_handler: ^10.4.5 permission_handler: ^10.4.5
get_it: ^7.6.0 get_it: ^7.6.0
logging: ^1.2.0 logging: ^1.2.0
path: ^1.8.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -44,13 +44,16 @@
]); ]);
lib = pkgs.lib; lib = pkgs.lib;
pinnedJDK = pkgs.jdk17; pinnedJDK = pkgs.jdk17;
flutterVersion = pkgs.flutter37; flutterVersion = pkgs.flutter;
in { in {
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
# 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

@@ -8,6 +8,60 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class ShareItem {
ShareItem({
this.path,
required this.mime,
this.text,
});
String? path;
String mime;
String? text;
Object encode() {
return <Object?>[
path,
mime,
text,
];
}
static ShareItem decode(Object result) {
result as List<Object?>;
return ShareItem(
path: result[0] as String?,
mime: result[1]! as String,
text: result[2] as String?,
);
}
}
class _MoxxyPlatformApiCodec extends StandardMessageCodec {
const _MoxxyPlatformApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is ShareItem) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return ShareItem.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class MoxxyPlatformApi { class MoxxyPlatformApi {
/// Constructor for [MoxxyPlatformApi]. The [binaryMessenger] named argument is /// Constructor for [MoxxyPlatformApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default /// available for dependency injection. If it is left null, the default
@@ -16,7 +70,7 @@ class MoxxyPlatformApi {
: _binaryMessenger = binaryMessenger; : _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger; final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec(); static const MessageCodec<Object?> codec = _MoxxyPlatformApiCodec();
Future<String> getPersistentDataPath() async { Future<String> getPersistentDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
@@ -120,4 +174,27 @@ class MoxxyPlatformApi {
return (replyList[0] as bool?)!; return (replyList[0] as bool?)!;
} }
} }
Future<void> shareItems(
List<ShareItem?> arg_items, String arg_genericMimeType) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.shareItems', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_items, arg_genericMimeType]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else {
return;
}
}
} }

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

@@ -10,6 +10,13 @@ import 'package:pigeon/pigeon.dart';
), ),
), ),
) )
class ShareItem {
const ShareItem(this.path, this.mime, this.text);
final String? path;
final String mime;
final String? text;
}
@HostApi() @HostApi()
abstract class MoxxyPlatformApi { abstract class MoxxyPlatformApi {
String getPersistentDataPath(); String getPersistentDataPath();
@@ -19,4 +26,6 @@ abstract class MoxxyPlatformApi {
void openBatteryOptimisationSettings(); void openBatteryOptimisationSettings();
bool isIgnoringBatteryOptimizations(); bool isIgnoringBatteryOptimizations();
void shareItems(List<ShareItem> items, String genericMimeType);
} }

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.3.2
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