Compare commits

..

35 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
cefe90b93a fix(android): Fix wrong id of the file provider 2023-09-10 19:15:27 +02:00
6491ac38b2 fix(docs): Fix typo 2023-09-10 13:49:18 +02:00
5fc2b716be feat(repo): Add a linting script 2023-09-10 13:48:26 +02:00
62b6e16cbc feat(repo): Add gitlint 2023-09-10 13:46:45 +02:00
9f8c148162 fix: Cleanup of the Android side of things 2023-09-10 13:44:26 +02:00
d6ce224956 feat: Provide a much cleaner Dart API 2023-09-10 13:28:12 +02:00
2299b766cc feat: Implement a wrapper around the service APIs 2023-09-10 01:02:56 +02:00
3b5e331aca fix: Fix foreground/background communication 2023-09-09 20:18:56 +02:00
403c6225f1 fix: Replace wrong quotation marks 2023-09-09 00:28:48 +02:00
dfbb64c8ae feat: Move over the service/background service API 2023-09-09 00:28:01 +02:00
42ff70a966 refactor: Move the notification code into its own file 2023-09-08 21:56:09 +02:00
c22b35b4ac chore: Fix linter issues 2023-09-08 21:44:37 +02:00
5dcfc7239a feat: Move over the media API 2023-09-08 21:41:10 +02:00
f7218f57cb fix: Remove unused field 2023-09-08 19:48:16 +02:00
79f4420510 feat: Move over the platform API 2023-09-08 19:47:39 +02:00
6e6b50d0c2 feat: Move over the contacts API 2023-09-08 19:40:03 +02:00
809ddfaf80 fix: Fix wrong file provider ID 2023-09-08 19:36:11 +02:00
b52ca03eff feat: Bring over the cryptography API from moxplatform 2023-09-08 19:25:26 +02:00
92 changed files with 4654 additions and 499 deletions

14
.gitlint Normal file
View File

@@ -0,0 +1,14 @@
[general]
ignore=B5,B6,B7,B8
[title-max-length]
line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^(feat|fix|chore)\((android|ios|linux|windows|macos|all|docs|service|media|platform|notifications|picker|repo)\): [A-Z0-9].*$
[body-trailing-whitespace]
[body-first-line-empty]

View File

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

View File

@@ -2,5 +2,32 @@
Interactions with the system for Moxxy.
This library is supposed to be the successor of moxplatfor, featuring
This library is the successor of moxplatform, featuring
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

@@ -7,6 +7,7 @@ linter:
avoid_positional_boolean_parameters: false
avoid_bool_literals_in_conditional_expressions: false
file_names: false
one_member_abstracts: false
analyzer:
exclude:

View File

@@ -48,5 +48,6 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0"
implementation "androidx.datastore:datastore-preferences:1.0.0"
}

View File

@@ -12,8 +12,36 @@
android:resource="@xml/file_paths" />
</provider>
<service
android:enabled="true"
android:exported="true"
android:name="org.moxxy.moxxy_native.service.BackgroundService"
/>
<receiver
android:name="org.moxxy.moxxy_native.service.WatchdogReceiver"
android:enabled="true"
android:exported="true"
/>
<receiver android:name="org.moxxy.moxxy_native.service.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:name="org.moxxy.moxxy_native.notifications.NotificationReceiver" />
</application>
<!-- Foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- Notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>

View File

@@ -2,6 +2,15 @@ package org.moxxy.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
const val BUFFER_SIZE = 4096
// The data key for text entered in the notification's reply field
const val REPLY_TEXT_KEY = "key_reply_text"
@@ -34,3 +43,17 @@ const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path"
const val PICK_FILE_REQUEST = 42
const val PICK_FILES_REQUEST = 43
const val PICK_FILE_WITH_DATA_REQUEST = 44
// Service
const val SERVICE_SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android"
const val SERVICE_ENTRYPOINT_KEY = "entrypoint_handle"
const val SERVICE_EXTRA_DATA_KEY = "extra_data"
const val SERVICE_START_AT_BOOT_KEY = "auto_start_at_boot"
const val SERVICE_MANUALLY_STOPPED_KEY = "manually_stopped"
// https://github.com/ekasetiawans/flutter_background_service/blob/e427f3b70138ec26f9671c2617f9061f25eade6f/packages/flutter_background_service_android/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java#L20
const val SERVICE_WAKELOCK_DURATION = 10 * 60 * 1000L
const val SERVICE_DEFAULT_TITLE = "Moxxy"
const val SERVICE_DEFAULT_BODY = "Preparing..."
const val SERVICE_FOREGROUND_METHOD_CHANNEL_KEY = "org.moxxy.moxxy_native/foreground"
const val SERVICE_BACKGROUND_METHOD_CHANNEL_KEY = "org.moxxy.moxxy_native/background"

View File

@@ -1,49 +1,42 @@
package org.moxxy.moxxy_native
import android.app.Activity
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.NonNull
import androidx.core.app.NotificationManagerCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.embedding.engine.plugins.service.ServiceAware
import io.flutter.embedding.engine.plugins.service.ServicePluginBinding
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.notifications.MessagingNotification
import io.flutter.plugin.common.MethodChannel
import org.moxxy.moxxy_native.contacts.ContactsImplementation
import org.moxxy.moxxy_native.contacts.MoxxyContactsApi
import org.moxxy.moxxy_native.cryptography.CryptographyImplementation
import org.moxxy.moxxy_native.cryptography.MoxxyCryptographyApi
import org.moxxy.moxxy_native.media.MediaImplementation
import org.moxxy.moxxy_native.media.MoxxyMediaApi
import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi
import org.moxxy.moxxy_native.notifications.NotificationChannel
import org.moxxy.moxxy_native.notifications.NotificationDataManager
import org.moxxy.moxxy_native.notifications.NotificationEvent
import org.moxxy.moxxy_native.notifications.NotificationGroup
import org.moxxy.moxxy_native.notifications.NotificationI18nData
import org.moxxy.moxxy_native.notifications.RegularNotification
import org.moxxy.moxxy_native.notifications.createNotificationChannelsImpl
import org.moxxy.moxxy_native.notifications.createNotificationGroupsImpl
import org.moxxy.moxxy_native.notifications.showNotificationImpl
import org.moxxy.moxxy_native.notifications.NotificationStreamHandler
import org.moxxy.moxxy_native.notifications.NotificationsImplementation
import org.moxxy.moxxy_native.picker.FilePickerType
import org.moxxy.moxxy_native.picker.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener
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
}
}
import org.moxxy.moxxy_native.platform.KeyboardStreamHandler
import org.moxxy.moxxy_native.platform.MoxxyPlatformApi
import org.moxxy.moxxy_native.platform.PlatformImplementation
import org.moxxy.moxxy_native.service.BackgroundService
import org.moxxy.moxxy_native.service.MoxxyServiceApi
import org.moxxy.moxxy_native.service.PluginTracker
import org.moxxy.moxxy_native.service.ServiceImplementation
/*
* Hold the last notification event in case we did a cold start.
@@ -52,45 +45,103 @@ object NotificationCache {
var lastEvent: NotificationEvent? = null
}
class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNotificationsApi {
class MoxxyNativePlugin : FlutterPlugin, ActivityAware, ServiceAware, BroadcastReceiver(), MoxxyPickerApi {
private var context: Context? = null
private var activity: Activity? = null
private lateinit var activityClass: Class<Any>
private lateinit var pickerListener: PickerResultListener
private val cryptographyImplementation = CryptographyImplementation()
private lateinit var contactsImplementation: ContactsImplementation
private lateinit var platformImplementation: PlatformImplementation
private val mediaImplementation = MediaImplementation()
private lateinit var notificationsImplementation: NotificationsImplementation
private lateinit var serviceImplementation: ServiceImplementation
var service: BackgroundService? = null
var channel: MethodChannel? = null
init {
PluginTracker.instances.add(this)
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
contactsImplementation = ContactsImplementation(context!!)
platformImplementation = PlatformImplementation(context!!)
notificationsImplementation = NotificationsImplementation(context!!)
serviceImplementation = ServiceImplementation(context!!)
// Register the pigeon handlers
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, this)
MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, notificationsImplementation)
MoxxyCryptographyApi.setUp(flutterPluginBinding.binaryMessenger, cryptographyImplementation)
MoxxyContactsApi.setUp(flutterPluginBinding.binaryMessenger, contactsImplementation)
MoxxyPlatformApi.setUp(flutterPluginBinding.binaryMessenger, platformImplementation)
MoxxyMediaApi.setUp(flutterPluginBinding.binaryMessenger, mediaImplementation)
MoxxyServiceApi.setUp(flutterPluginBinding.binaryMessenger, serviceImplementation)
// Special handling for the service APIs
channel = MethodChannel(flutterPluginBinding.getBinaryMessenger(), SERVICE_FOREGROUND_METHOD_CHANNEL_KEY)
LocalBroadcastManager.getInstance(context!!).registerReceiver(
this,
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
pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine")
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
LocalBroadcastManager.getInstance(context!!).registerReceiver(
this,
IntentFilter(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY),
)
Log.d(TAG, "Detached from engine")
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
activityClass = activity!!.javaClass
binding.addActivityResultListener(pickerListener)
KeyboardStreamHandler.activity = activity
Log.d(TAG, "Attached to activity")
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
KeyboardStreamHandler.activity = null
Log.d(TAG, "Detached from activity")
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
KeyboardStreamHandler.activity = activity
}
override fun onDetachedFromActivity() {
activity = null
KeyboardStreamHandler.activity = null
Log.d(TAG, "Detached from activity")
}
override fun onAttachedToService(binding: ServicePluginBinding) {
Log.d(TAG, "Attached to service")
service = binding.getService() as BackgroundService
}
override fun onDetachedFromService() {
Log.d(TAG, "Detached from service")
service = null
}
override fun pickFiles(
type: FilePickerType,
multiple: Boolean,
@@ -148,53 +199,11 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNot
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
}
override fun createNotificationGroups(groups: List<NotificationGroup>) {
createNotificationGroupsImpl(context!!, groups)
}
override fun deleteNotificationGroups(ids: List<String>) {
val notificationManager = context!!.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannelGroup(id)
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent with ${intent.action}")
if (intent.action?.equals(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY) == true) {
val data = intent.getStringExtra("data")
channel?.invokeMethod("dataReceived", data)
}
}
override fun createNotificationChannels(channels: List<NotificationChannel>) {
createNotificationChannelsImpl(context!!, channels)
}
override fun deleteNotificationChannels(ids: List<String>) {
val notificationManager = context!!.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannel(id)
}
}
override fun showMessagingNotification(notification: MessagingNotification) {
org.moxxy.moxxy_native.notifications.showMessagingNotification(context!!, notification)
}
override fun showNotification(notification: RegularNotification) {
showNotificationImpl(context!!, notification)
}
override fun dismissNotification(id: Long) {
NotificationManagerCompat.from(context!!).cancel(id.toInt())
}
override fun setNotificationSelfAvatar(path: String) {
NotificationDataManager.setAvatarPath(context!!, path)
}
override fun setNotificationI18n(data: NotificationI18nData) {
NotificationDataManager.apply {
setYou(context!!, data.you)
setReply(context!!, data.reply)
setMarkAsRead(context!!, data.markAsRead)
}
}
override fun notificationStub(event: NotificationEvent) {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,95 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.contacts
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
/** The type of icon to use when no avatar path is provided. */
enum class FallbackIconType(val raw: Int) {
NONE(0),
PERSON(1),
NOTES(2),
;
companion object {
fun ofRaw(raw: Int): FallbackIconType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyContactsApi {
fun recordSentMessage(name: String, jid: String, avatarPath: String?, fallbackIcon: FallbackIconType)
companion object {
/** The codec used by MoxxyContactsApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
}
/** Sets up an instance of `MoxxyContactsApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyContactsApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyContactsApi.recordSentMessage", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val nameArg = args[0] as String
val jidArg = args[1] as String
val avatarPathArg = args[2] as String?
val fallbackIconArg = FallbackIconType.ofRaw(args[3] as Int)!!
var wrapped: List<Any?>
try {
api.recordSentMessage(nameArg, jidArg, avatarPathArg, fallbackIconArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
package org.moxxy.moxxy_native.contacts
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import androidx.core.app.Person
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import org.moxxy.moxxy_native.R
/*
* Implementation of Moxxy's contact APIs.
* */
class ContactsImplementation(private val context: Context) : MoxxyContactsApi {
override fun recordSentMessage(
name: String,
jid: String,
avatarPath: String?,
fallbackIcon: FallbackIconType,
) {
val pkgName = context.packageName
val intent = Intent(context, Class.forName("$pkgName.MainActivity")).apply {
action = Intent.ACTION_SEND
// Compatibility with share_handler
putExtra("conversationIdentifier", jid)
}
val shortcutTarget = "$pkgName.dynamic_share_target"
val shortcutBuilder = ShortcutInfoCompat.Builder(context, jid).apply {
setShortLabel(name)
setIsConversation()
setCategories(setOf(shortcutTarget))
setIntent(intent)
setLongLived(true)
}
val personBuilder = Person.Builder().apply {
setKey(jid)
setName(name)
}
// Either set an avatar image OR a fallback icon
if (avatarPath != null) {
val icon = IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath),
)
shortcutBuilder.setIcon(icon)
personBuilder.setIcon(icon)
} else {
val resourceId = when (fallbackIcon) {
FallbackIconType.NONE, FallbackIconType.PERSON -> R.mipmap.person
FallbackIconType.NOTES -> R.mipmap.notes
}
val icon = IconCompat.createWithResource(context, resourceId)
shortcutBuilder.setIcon(icon)
personBuilder.setIcon(icon)
}
shortcutBuilder.setPerson(personBuilder.build())
ShortcutManagerCompat.addDynamicShortcuts(
context,
listOf(shortcutBuilder.build()),
)
}
}

View File

@@ -1,6 +1,24 @@
package org.moxxy.moxxy_native.content
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MOXXY_FILEPROVIDER_ID
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

@@ -0,0 +1,192 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.cryptography
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
enum class CipherAlgorithm(val raw: Int) {
AES128GCMNOPADDING(0),
AES256GCMNOPADDING(1),
AES256CBCPKCS7(2),
;
companion object {
fun ofRaw(raw: Int): CipherAlgorithm? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class CryptographyResult(
val plaintextHash: ByteArray,
val ciphertextHash: ByteArray,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): CryptographyResult {
val plaintextHash = list[0] as ByteArray
val ciphertextHash = list[1] as ByteArray
return CryptographyResult(plaintextHash, ciphertextHash)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
plaintextHash,
ciphertextHash,
)
}
}
@Suppress("UNCHECKED_CAST")
private object MoxxyCryptographyApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CryptographyResult.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is CryptographyResult -> {
stream.write(128)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyCryptographyApi {
fun encryptFile(sourcePath: String, destPath: String, key: ByteArray, iv: ByteArray, algorithm: CipherAlgorithm, hashSpec: String, callback: (Result<CryptographyResult?>) -> Unit)
fun decryptFile(sourcePath: String, destPath: String, key: ByteArray, iv: ByteArray, algorithm: CipherAlgorithm, hashSpec: String, callback: (Result<CryptographyResult?>) -> Unit)
fun hashFile(sourcePath: String, hashSpec: String, callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by MoxxyCryptographyApi. */
val codec: MessageCodec<Any?> by lazy {
MoxxyCryptographyApiCodec
}
/** Sets up an instance of `MoxxyCryptographyApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyCryptographyApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.encryptFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val destPathArg = args[1] as String
val keyArg = args[2] as ByteArray
val ivArg = args[3] as ByteArray
val algorithmArg = CipherAlgorithm.ofRaw(args[4] as Int)!!
val hashSpecArg = args[5] as String
api.encryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg) { result: Result<CryptographyResult?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.decryptFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val destPathArg = args[1] as String
val keyArg = args[2] as ByteArray
val ivArg = args[3] as ByteArray
val algorithmArg = CipherAlgorithm.ofRaw(args[4] as Int)!!
val hashSpecArg = args[5] as String
api.decryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg) { result: Result<CryptographyResult?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.hashFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val hashSpecArg = args[1] as String
api.hashFile(sourcePathArg, hashSpecArg) { result: Result<ByteArray?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
package org.moxxy.moxxy_native.cryptography
import android.util.Log
import org.moxxy.moxxy_native.BUFFER_SIZE
import org.moxxy.moxxy_native.TAG
import java.io.FileInputStream
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.thread
/*
* Convert the algorithm spec @algorithm to the format that Java/Android understands
* */
private fun getCipherSpecFromInteger(algorithm: CipherAlgorithm): String {
return when (algorithm) {
CipherAlgorithm.AES128GCMNOPADDING -> "AES_128/GCM/NoPadding"
CipherAlgorithm.AES256GCMNOPADDING -> "AES_256/GCM/NoPadding"
CipherAlgorithm.AES256CBCPKCS7 -> "AES_256/CBC/PKCS7PADDING"
}
}
/*
* Implementation of Moxxy's cryptography API
* */
class CryptographyImplementation : MoxxyCryptographyApi {
override fun encryptFile(
sourcePath: String,
destPath: String,
key: ByteArray,
iv: ByteArray,
algorithm: CipherAlgorithm,
hashSpec: String,
callback: (Result<CryptographyResult?>) -> Unit,
) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(algorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
val cipher = Cipher.getInstance(cipherSpec).apply {
init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
}
val fileOutputStream = HashedFileOutputStream(destPath, hashSpec)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Clean up
cipherOutputStream.apply {
flush()
close()
}
// Success
callback(
Result.success(
CryptographyResult(
plaintextHash = digest.digest(),
ciphertextHash = fileOutputStream.digest(),
),
),
)
} catch (ex: Exception) {
Log.e(TAG, "Failed to encrypt file $sourcePath: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
override fun decryptFile(
sourcePath: String,
destPath: String,
key: ByteArray,
iv: ByteArray,
algorithm: CipherAlgorithm,
hashSpec: String,
callback: (Result<CryptographyResult?>) -> Unit,
) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(algorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
val cipher = Cipher.getInstance(cipherSpec).apply {
init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
}
val fileOutputStream = HashedFileOutputStream(destPath, hashSpec)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Clean up
cipherOutputStream.apply {
flush()
close()
}
// Success
callback(
Result.success(
CryptographyResult(
plaintextHash = digest.digest(),
ciphertextHash = fileOutputStream.digest(),
),
),
)
} catch (ex: Exception) {
Log.e(TAG, "Failed to decrypt file $sourcePath: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
override fun hashFile(
sourcePath: String,
hashSpec: String,
callback: (Result<ByteArray?>) -> Unit,
) {
thread(start = true) {
val buffer = ByteArray(BUFFER_SIZE)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
}
// Return success
callback(Result.success(digest.digest()))
} catch (ex: Exception) {
Log.e(TAG, "Failed to has file $sourcePath with $hashSpec: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
}

View File

@@ -0,0 +1,25 @@
package org.moxxy.moxxy_native.cryptography
import java.io.FileOutputStream
import java.security.MessageDigest
/*
* A FileOutputStream that continuously hashes whatever it writes to the file.
*/
class HashedFileOutputStream(name: String, hashAlgorithm: String) : FileOutputStream(name) {
private val digest: MessageDigest
init {
this.digest = MessageDigest.getInstance(hashAlgorithm)
}
override fun write(buffer: ByteArray, offset: Int, length: Int) {
super.write(buffer, offset, length)
digest.update(buffer, offset, length)
}
fun digest(): ByteArray {
return digest.digest()
}
}

View File

@@ -0,0 +1,79 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.media
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyMediaApi {
fun generateVideoThumbnail(src: String, dest: String, maxWidth: Long): Boolean
companion object {
/** The codec used by MoxxyMediaApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
}
/** Sets up an instance of `MoxxyMediaApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyMediaApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyMediaApi.generateVideoThumbnail", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val srcArg = args[0] as String
val destArg = args[1] as String
val maxWidthArg = args[2].let { if (it is Int) it.toLong() else it as Long }
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.generateVideoThumbnail(srcArg, destArg, maxWidthArg))
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,45 @@
package org.moxxy.moxxy_native.media
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.util.Log
import org.moxxy.moxxy_native.TAG
import java.io.FileOutputStream
class MediaImplementation : MoxxyMediaApi {
override fun generateVideoThumbnail(src: String, dest: String, maxWidth: Long): Boolean {
try {
// Get a frame as a thumbnail
val mmr = MediaMetadataRetriever().apply {
setDataSource(src)
}
val unscaledThumbnail = mmr.getFrameAtTime(0) ?: return false
// Scale down the thumbnail while keeping the aspect ratio
val scalingFactor = maxWidth.toDouble() / unscaledThumbnail.width
Log.d(TAG, "Scaling to $maxWidth from ${unscaledThumbnail.width} with scalingFactor $scalingFactor")
val thumbnail = Bitmap.createScaledBitmap(
unscaledThumbnail,
(unscaledThumbnail.width * scalingFactor).toInt(),
(unscaledThumbnail.height * scalingFactor).toInt(),
false,
)
// Write it to the destination file
val fileOutputStream = FileOutputStream(dest)
thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, fileOutputStream)
// Clean up
fileOutputStream.apply {
flush()
close()
}
// Success
return true
} catch (ex: Exception) {
Log.e(TAG, "Failed to create thumbnail for $src: ${ex.message}")
return false
}
}
}

View File

@@ -0,0 +1,17 @@
package org.moxxy.moxxy_native.notifications
import android.content.Intent
/*
* Extract all user-added extra key-value pairs from @intent.
* */
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach {
if (it.startsWith("payload_")) {
extras[it.substring(8)] = intent.extras!!.getString(it)
}
}
return extras
}

View File

@@ -0,0 +1,79 @@
package org.moxxy.moxxy_native.notifications
import android.content.Context
import org.moxxy.moxxy_native.SHARED_PREFERENCES_AVATAR_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_MARK_AS_READ_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_REPLY_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_YOU_KEY
/*
* Holds "persistent" data for notifications, like i18n strings. While not useful now, this is
* useful for when the app is dead and we receive a notification.
* */
object NotificationDataManager {
private var you: String? = null
private var markAsRead: String? = null
private var reply: String? = null
private var fetchedAvatarPath = false
private var avatarPath: String? = null
private fun getString(context: Context, key: String, fallback: String): String {
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!!
}
private fun setString(context: Context, key: String, value: String) {
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
prefs.edit()
.putString(key, value)
.apply()
}
fun getYou(context: Context): String {
if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You")
return you!!
}
fun setYou(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_YOU_KEY, value)
you = value
}
fun getMarkAsRead(context: Context): String {
if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read")
return markAsRead!!
}
fun setMarkAsRead(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value)
markAsRead = value
}
fun getReply(context: Context): String {
if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply")
return reply!!
}
fun setReply(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_REPLY_KEY, value)
reply = value
}
fun getAvatarPath(context: Context): String? {
if (avatarPath == null && !fetchedAvatarPath) {
val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "")
if (path.isNotEmpty()) {
avatarPath = path
}
}
return avatarPath
}
fun setAvatarPath(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_AVATAR_KEY, value)
fetchedAvatarPath = true
avatarPath = value
}
}

View File

@@ -15,7 +15,6 @@ import androidx.core.app.RemoteInput
import androidx.core.content.FileProvider
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
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_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
@@ -27,19 +26,6 @@ import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File
import java.time.Instant
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach {
Log.d(TAG, "Checking $it -> ${intent.extras!!.get(it)}")
if (it.startsWith("payload_")) {
Log.d(TAG, "Adding $it")
extras[it.substring(8)] = intent.extras!!.getString(it)
}
}
return extras
}
class NotificationReceiver : BroadcastReceiver() {
/*
* Dismisses the notification through which we received @intent.
@@ -63,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
}
private fun handleMarkAsRead(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success(
NotificationStreamHandler.sink?.success(
NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -78,7 +64,7 @@ class NotificationReceiver : BroadcastReceiver() {
private fun handleReply(context: Context, intent: Intent) {
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
MoxxyEventChannels.notificationEventSink?.success(
NotificationStreamHandler.sink?.success(
NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
@@ -177,8 +163,8 @@ class NotificationReceiver : BroadcastReceiver() {
}
}
fun handleTap(context: Context, intent: Intent) {
MoxxyEventChannels.notificationEventSink?.success(
private fun handleTap(context: Context, intent: Intent) {
NotificationStreamHandler.sink?.success(
NotificationEvent(
intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
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

@@ -1,362 +0,0 @@
package org.moxxy.moxxy_native.notifications
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.IconCompat
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_JID_KEY
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_MIME
import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_PATH
import org.moxxy.moxxy_native.R
import org.moxxy.moxxy_native.REPLY_ACTION
import org.moxxy.moxxy_native.REPLY_TEXT_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_AVATAR_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_MARK_AS_READ_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_REPLY_KEY
import org.moxxy.moxxy_native.SHARED_PREFERENCES_YOU_KEY
import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File
/*
* Holds "persistent" data for notifications, like i18n strings. While not useful now, this is
* useful for when the app is dead and we receive a notification.
* */
object NotificationDataManager {
private var you: String? = null
private var markAsRead: String? = null
private var reply: String? = null
private var fetchedAvatarPath = false
private var avatarPath: String? = null
private fun getString(context: Context, key: String, fallback: String): String {
return context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)!!.getString(key, fallback)!!
}
private fun setString(context: Context, key: String, value: String) {
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
prefs.edit()
.putString(key, value)
.apply()
}
fun getYou(context: Context): String {
if (you == null) you = getString(context, SHARED_PREFERENCES_YOU_KEY, "You")
return you!!
}
fun setYou(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_YOU_KEY, value)
you = value
}
fun getMarkAsRead(context: Context): String {
if (markAsRead == null) markAsRead = getString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, "Mark as read")
return markAsRead!!
}
fun setMarkAsRead(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_MARK_AS_READ_KEY, value)
markAsRead = value
}
fun getReply(context: Context): String {
if (reply != null) reply = getString(context, SHARED_PREFERENCES_REPLY_KEY, "Reply")
return reply!!
}
fun setReply(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_REPLY_KEY, value)
reply = value
}
fun getAvatarPath(context: Context): String? {
if (avatarPath == null && !fetchedAvatarPath) {
val path = getString(context, SHARED_PREFERENCES_AVATAR_KEY, "")
if (path.isNotEmpty()) {
avatarPath = path
}
}
return avatarPath
}
fun setAvatarPath(context: Context, value: String) {
setString(context, SHARED_PREFERENCES_AVATAR_KEY, value)
fetchedAvatarPath = true
avatarPath = value
}
}
fun createNotificationGroupsImpl(context: Context, groups: List<NotificationGroup>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (group in groups) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(group.id, group.description),
)
}
}
fun createNotificationChannelsImpl(context: Context, channels: List<NotificationChannel>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (channel in channels) {
val importance = when (channel.importance) {
NotificationChannelImportance.DEFAULT -> NotificationManager.IMPORTANCE_DEFAULT
NotificationChannelImportance.MIN -> NotificationManager.IMPORTANCE_MIN
NotificationChannelImportance.HIGH -> NotificationManager.IMPORTANCE_HIGH
}
val notificationChannel = android.app.NotificationChannel(channel.id, channel.title, importance).apply {
description = channel.description
enableVibration(channel.vibration)
enableLights(channel.enableLights)
setShowBadge(channel.showBadge)
if (channel.groupId != null) {
group = channel.groupId
}
}
notificationManager.createNotificationChannel(notificationChannel)
}
}
// / Show a messaging style notification described by @notification.
@SuppressLint("WrongConstant")
fun showMessagingNotification(context: Context, notification: MessagingNotification) {
// Build the actions
// -> Reply action
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
setLabel(NotificationDataManager.getReply(context))
}.build()
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
action = REPLY_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val replyPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
replyIntent,
PendingIntent.FLAG_MUTABLE,
)
val replyAction = NotificationCompat.Action.Builder(
R.drawable.reply,
NotificationDataManager.getReply(context),
replyPendingIntent,
).apply {
addRemoteInput(remoteInput)
setAllowGeneratedReplies(true)
}.build()
// -> Mark as read action
val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply {
action = MARK_AS_READ_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val markAsReadPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
markAsReadIntent,
PendingIntent.FLAG_IMMUTABLE,
)
val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.mark_as_read,
NotificationDataManager.getMarkAsRead(context),
markAsReadPendingIntent,
).build()
// -> Tap action
// Thanks to flutter_local_notifications for this "workaround"
val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply {
action = TAP_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
// Do not launch a new task
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val tapPendingIntent = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(tapIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
// Build the notification
val selfPerson = Person.Builder().apply {
setName(NotificationDataManager.getYou(context))
// Set an avatar, if we have one
val avatarPath = NotificationDataManager.getAvatarPath(context)
if (avatarPath != null) {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath),
),
)
}
}.build()
val style = NotificationCompat.MessagingStyle(selfPerson)
style.isGroupConversation = notification.isGroupchat
if (notification.isGroupchat) {
style.conversationTitle = notification.title
}
for (i in notification.messages.indices) {
val message = notification.messages[i]!!
// Build the sender
// NOTE: Note that we set it to null if message.sender == null because otherwise this results in
// a bogus Person object which messes with the "self-message" display as Android expects
// null in that case.
val sender = if (message.sender == null) {
null
} else {
Person.Builder().apply {
setName(message.sender)
setKey(message.jid)
// Set the avatar, if available
if (message.avatarPath != null) {
try {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(message.avatarPath),
),
)
} catch (ex: Throwable) {
Log.w(TAG, "Failed to open avatar at ${message.avatarPath}")
}
}
}.build()
}
// Build the message
val body = message.content.body ?: ""
val msg = NotificationCompat.MessagingStyle.Message(
body,
message.timestamp,
sender,
)
// If we got an image, turn it into a content URI and set it
if (message.content.mime != null && message.content.path != null) {
val fileUri = FileProvider.getUriForFile(
context,
MOXXY_FILEPROVIDER_ID,
File(message.content.path),
)
msg.apply {
setData(message.content.mime, fileUri)
extras.apply {
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime)
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path)
}
}
}
// Append the message
style.addMessage(msg)
}
// Assemble the notification
val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setStyle(style)
// NOTE: It's okay to use the service icon here as I cannot get Android to display the
// actual logo. So we'll have to make do with the silhouette and the color purple.
setSmallIcon(R.drawable.ic_service)
color = Color.argb(255, 207, 74, 255)
setColorized(true)
// Tap action
setContentIntent(tapPendingIntent)
// Notification actions
addAction(replyAction)
addAction(markAsReadAction)
// Groupchat title
if (notification.isGroupchat) {
setContentTitle(notification.title)
}
// Prevent grouping with the foreground service
if (notification.groupId != null) {
setGroup(notification.groupId)
}
setAllowSystemGeneratedContextualActions(true)
setCategory(Notification.CATEGORY_MESSAGE)
// Prevent no notification when we replied before
setOnlyAlertOnce(false)
// Automatically dismiss the notification on tap
setAutoCancel(true)
}.build()
// Post the notification
try {
NotificationManagerCompat.from(context).notify(
notification.id.toInt(),
finalNotification,
)
} catch (ex: SecurityException) {
// Should never happen as Moxxy checks for the permission before posting the notification
Log.e(TAG, "Failed to post notification: ${ex.message}")
}
}
fun showNotificationImpl(context: Context, notification: RegularNotification) {
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setContentTitle(notification.title)
setContentText(notification.body)
when (notification.icon) {
NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
NotificationIcon.NONE -> {}
}
if (notification.groupId != null) {
setGroup(notification.groupId)
}
}.build()
// Post the notification
try {
NotificationManagerCompat.from(context).notify(notification.id.toInt(), builtNotification)
} catch (ex: SecurityException) {
// Should never happen as Moxxy checks for the permission before posting the notification
Log.e(TAG, "Failed to post notification: ${ex.message}")
}
}

View File

@@ -0,0 +1,326 @@
package org.moxxy.moxxy_native.notifications
import android.app.Notification
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.graphics.drawable.IconCompat
import org.moxxy.moxxy_native.MARK_AS_READ_ACTION
import org.moxxy.moxxy_native.NOTIFICATION_EXTRA_ID_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_PATH
import org.moxxy.moxxy_native.R
import org.moxxy.moxxy_native.REPLY_ACTION
import org.moxxy.moxxy_native.REPLY_TEXT_KEY
import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.TAP_ACTION
import org.moxxy.moxxy_native.content.MoxxyFileProvider
class NotificationsImplementation(private val context: Context) : MoxxyNotificationsApi {
override fun createNotificationGroups(groups: List<NotificationGroup>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (group in groups) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(group.id, group.description),
)
}
}
override fun deleteNotificationGroups(ids: List<String>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannelGroup(id)
}
}
override fun createNotificationChannels(channels: List<NotificationChannel>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (channel in channels) {
val importance = when (channel.importance) {
NotificationChannelImportance.DEFAULT -> NotificationManager.IMPORTANCE_DEFAULT
NotificationChannelImportance.MIN -> NotificationManager.IMPORTANCE_MIN
NotificationChannelImportance.HIGH -> NotificationManager.IMPORTANCE_HIGH
}
val notificationChannel =
android.app.NotificationChannel(channel.id, channel.title, importance).apply {
description = channel.description
enableVibration(channel.vibration)
enableLights(channel.enableLights)
setShowBadge(channel.showBadge)
if (channel.groupId != null) {
group = channel.groupId
}
}
notificationManager.createNotificationChannel(notificationChannel)
}
}
override fun deleteNotificationChannels(ids: List<String>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannel(id)
}
}
override fun showMessagingNotification(notification: MessagingNotification) {
// Build the actions
// -> Reply action
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
setLabel(NotificationDataManager.getReply(context))
}.build()
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
action = REPLY_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val replyPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
replyIntent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val replyAction = NotificationCompat.Action.Builder(
R.drawable.reply,
NotificationDataManager.getReply(context),
replyPendingIntent,
).apply {
addRemoteInput(remoteInput)
setAllowGeneratedReplies(true)
}.build()
// -> Mark as read action
val markAsReadIntent = Intent(context, NotificationReceiver::class.java).apply {
action = MARK_AS_READ_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
Log.d(TAG, "Adding payload_${it.key} -> ${it.value}")
}
}
val markAsReadPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
markAsReadIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val markAsReadAction = NotificationCompat.Action.Builder(
R.drawable.mark_as_read,
NotificationDataManager.getMarkAsRead(context),
markAsReadPendingIntent,
).build()
// -> Tap action
// Thanks to flutter_local_notifications for this "workaround"
val tapIntent =
context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply {
action = TAP_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
// Do not launch a new task
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val tapPendingIntent = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(tapIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
// Build the notification
val selfPerson = Person.Builder().apply {
setName(NotificationDataManager.getYou(context))
// Set an avatar, if we have one
val avatarPath = NotificationDataManager.getAvatarPath(context)
if (avatarPath != null) {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(avatarPath),
),
)
}
}.build()
val style = NotificationCompat.MessagingStyle(selfPerson)
style.isGroupConversation = notification.isGroupchat
if (notification.isGroupchat) {
style.conversationTitle = notification.title
}
for (i in notification.messages.indices) {
val message = notification.messages[i]!!
// Build the sender
// NOTE: Note that we set it to null if message.sender == null because otherwise this results in
// a bogus Person object which messes with the "self-message" display as Android expects
// null in that case.
val sender = if (message.sender == null) {
null
} else {
Person.Builder().apply {
setName(message.sender)
setKey(message.jid)
// Set the avatar, if available
if (message.avatarPath != null) {
try {
setIcon(
IconCompat.createWithAdaptiveBitmap(
BitmapFactory.decodeFile(message.avatarPath),
),
)
} catch (ex: Throwable) {
Log.w(TAG, "Failed to open avatar at ${message.avatarPath}")
}
}
}.build()
}
// Build the message
val body = message.content.body ?: ""
val msg = NotificationCompat.MessagingStyle.Message(
body,
message.timestamp,
sender,
)
// If we got an image, turn it into a content URI and set it
if (message.content.mime != null && message.content.path != null) {
val fileUri = MoxxyFileProvider.getUriForPath(context, message.content.path)
msg.apply {
setData(message.content.mime, fileUri)
extras.apply {
putString(NOTIFICATION_MESSAGE_EXTRA_MIME, message.content.mime)
putString(NOTIFICATION_MESSAGE_EXTRA_PATH, message.content.path)
}
}
}
// Append the message
style.addMessage(msg)
}
// Assemble the notification
val finalNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setStyle(style)
// NOTE: It's okay to use the service icon here as I cannot get Android to display the
// actual logo. So we'll have to make do with the silhouette and the color purple.
setSmallIcon(R.drawable.ic_service)
color = Color.argb(255, 207, 74, 255)
setColorized(true)
// Tap action
setContentIntent(tapPendingIntent)
// Notification actions
addAction(replyAction)
addAction(markAsReadAction)
// Groupchat title
if (notification.isGroupchat) {
setContentTitle(notification.title)
}
// Prevent grouping with the foreground service
if (notification.groupId != null) {
setGroup(notification.groupId)
}
setAllowSystemGeneratedContextualActions(true)
setCategory(Notification.CATEGORY_MESSAGE)
// Prevent no notification when we replied before
setOnlyAlertOnce(true)
// Automatically dismiss the notification on tap
setAutoCancel(true)
}.build()
// Post the notification
try {
NotificationManagerCompat.from(context).notify(
notification.id.toInt(),
finalNotification,
)
} catch (ex: SecurityException) {
// Should never happen as Moxxy checks for the permission before posting the notification
Log.e(TAG, "Failed to post notification: ${ex.message}")
}
}
override fun showNotification(notification: RegularNotification) {
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setContentTitle(notification.title)
setContentText(notification.body)
when (notification.icon) {
NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
NotificationIcon.NONE -> {}
}
if (notification.groupId != null) {
setGroup(notification.groupId)
}
}.build()
// Post the notification
try {
NotificationManagerCompat.from(context)
.notify(notification.id.toInt(), builtNotification)
} catch (ex: SecurityException) {
// Should never happen as Moxxy checks for the permission before posting the notification
Log.e(TAG, "Failed to post notification: ${ex.message}")
}
}
override fun dismissNotification(id: Long) {
NotificationManagerCompat.from(context).cancel(id.toInt())
}
override fun setNotificationSelfAvatar(path: String) {
NotificationDataManager.setAvatarPath(context, path)
}
override fun setNotificationI18n(data: NotificationI18nData) {
NotificationDataManager.apply {
setYou(
context,
data.you,
)
setReply(
context,
data.reply,
)
setMarkAsRead(
context,
data.markAsRead,
)
}
}
override fun notificationStub(event: NotificationEvent) {
// N/A
}
}

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.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import android.provider.MediaStore.Images
import android.util.Log
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
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_FILE_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.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 {
/*
* 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 {
var result: String? = null
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)
cursor.use { cursor ->
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) {
android.os.FileUtils.copy(input, output)
} else {
val buffer = ByteArray(4096)
val buffer = ByteArray(BUFFER_SIZE)
while (input.read(buffer).also {} != -1) {
output.write(buffer)
}
@@ -94,7 +127,7 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
}
val returnBuffer = mutableListOf<Byte>()
val readBuffer = ByteArray(4096)
val readBuffer = ByteArray(BUFFER_SIZE)
try {
val inputStream = context.contentResolver.openInputStream(data!!.data!!)!!
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

@@ -0,0 +1,198 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.platform
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : 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. */
interface MoxxyPlatformApi {
fun getPersistentDataPath(): String
fun getCacheDataPath(): String
fun openBatteryOptimisationSettings()
fun isIgnoringBatteryOptimizations(): Boolean
fun shareItems(items: List<ShareItem>, genericMimeType: String)
companion object {
/** The codec used by MoxxyPlatformApi. */
val codec: MessageCodec<Any?> by lazy {
MoxxyPlatformApiCodec
}
/** Sets up an instance of `MoxxyPlatformApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyPlatformApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.getPersistentDataPath", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.getPersistentDataPath())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.getCacheDataPath", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.getCacheDataPath())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.openBatteryOptimisationSettings", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
api.openBatteryOptimisationSettings()
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.isIgnoringBatteryOptimizations", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.isIgnoringBatteryOptimizations())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
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

@@ -0,0 +1,56 @@
package org.moxxy.moxxy_native.platform
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.PowerManager
import android.provider.Settings
import androidx.core.app.ShareCompat
import org.moxxy.moxxy_native.content.MoxxyFileProvider
class PlatformImplementation(private val context: Context) : MoxxyPlatformApi {
override fun getPersistentDataPath(): String {
return context.filesDir.path
}
override fun getCacheDataPath(): String {
return context.cacheDir.path
}
override fun openBatteryOptimisationSettings() {
val packageUri = Uri.parse("package:${context.packageName}")
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, packageUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(intent)
}
override fun isIgnoringBatteryOptimizations(): Boolean {
val pm = context.getSystemService(PowerManager::class.java)
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

@@ -0,0 +1,284 @@
package org.moxxy.moxxy_native.service
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.PowerManager.WakeLock
import android.util.Log
import androidx.core.app.AlarmManagerCompat
import androidx.core.app.NotificationCompat
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.FlutterCallbackInformation
import org.moxxy.moxxy_native.R
import org.moxxy.moxxy_native.SERVICE_BACKGROUND_METHOD_CHANNEL_KEY
import org.moxxy.moxxy_native.SERVICE_DEFAULT_BODY
import org.moxxy.moxxy_native.SERVICE_DEFAULT_TITLE
import org.moxxy.moxxy_native.SERVICE_ENTRYPOINT_KEY
import org.moxxy.moxxy_native.SERVICE_EXTRA_DATA_KEY
import org.moxxy.moxxy_native.SERVICE_FOREGROUND_METHOD_CHANNEL_KEY
import org.moxxy.moxxy_native.SERVICE_MANUALLY_STOPPED_KEY
import org.moxxy.moxxy_native.SERVICE_SHARED_PREFERENCES_KEY
import org.moxxy.moxxy_native.SERVICE_START_AT_BOOT_KEY
import org.moxxy.moxxy_native.SERVICE_WAKELOCK_DURATION
import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.service.background.MoxxyBackgroundServiceApi
import java.util.concurrent.atomic.AtomicBoolean
object BackgroundServiceStatic {
@Volatile
var wakeLock: WakeLock? = null
fun acquireWakeLock(context: Context): WakeLock {
if (wakeLock == null) {
val manager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock =
manager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "${this.javaClass.name}.class")
wakeLock!!.setReferenceCounted(true)
}
return wakeLock!!
}
fun enqueue(context: Context) {
val mutable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val pendingIntent = PendingIntent.getBroadcast(
context,
111,
Intent(context, WatchdogReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or mutable,
)
AlarmManagerCompat.setAndAllowWhileIdle(
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager,
AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 5000,
pendingIntent,
)
}
fun setConfiguration(context: Context, handle: Long, extraData: String) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putLong(SERVICE_ENTRYPOINT_KEY, handle)
.putString(SERVICE_EXTRA_DATA_KEY, extraData)
.apply()
}
fun getStartAtBoot(context: Context): Boolean {
return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.getBoolean(
SERVICE_START_AT_BOOT_KEY,
false,
)
}
fun setStartAtBoot(context: Context, value: Boolean) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putBoolean(SERVICE_START_AT_BOOT_KEY, value)
.apply()
}
fun getManuallyStopped(context: Context): Boolean {
return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.getBoolean(
SERVICE_MANUALLY_STOPPED_KEY,
false,
)
}
fun setManuallyStopped(context: Context, value: Boolean) {
context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit()
.putBoolean(SERVICE_MANUALLY_STOPPED_KEY, value)
.apply()
}
fun getHandle(context: Context): Long {
return context.getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getLong(
SERVICE_ENTRYPOINT_KEY,
0,
)
}
}
class BackgroundService : Service(), MoxxyBackgroundServiceApi {
// Indicates whether the background service is running or not
private var isRunning = AtomicBoolean(false)
// Indicates whether the service was stopped manually
private var isManuallyStopped = false
// If non-null, the Flutter Engine that is running the background service's code
private var engine: FlutterEngine? = null
// The callback for Dart to start execution at
private var dartCallback: DartExecutor.DartCallback? = null
// Method channel for Java -> Dart
private var methodChannel: MethodChannel? = null
// Data for the notification
private var notificationTitle: String = SERVICE_DEFAULT_TITLE
private var notificationBody: String = SERVICE_DEFAULT_BODY
private fun updateNotificationInfo() {
val mutable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val pendingIntent = PendingIntent.getActivity(
this,
99778,
packageManager.getLaunchIntentForPackage(applicationContext.packageName),
PendingIntent.FLAG_CANCEL_CURRENT or mutable,
)
val notification = NotificationCompat.Builder(this, "foreground_service").apply {
setSmallIcon(R.drawable.ic_service)
setAutoCancel(false)
setOngoing(true)
setContentTitle(notificationTitle)
setContentText(notificationBody)
setContentIntent(pendingIntent)
}.build()
startForeground(99778, notification)
}
private fun runService() {
try {
if (isRunning.get() || (engine?.getDartExecutor()?.isExecutingDart() ?: false)) return
if (BackgroundServiceStatic.wakeLock == null) {
Log.d(TAG, "WakeLock is null. Acquiring and grabbing WakeLock...")
BackgroundServiceStatic.acquireWakeLock(applicationContext)
.acquire(SERVICE_WAKELOCK_DURATION)
Log.d(TAG, "WakeLock grabbed")
}
// Update the notification
updateNotificationInfo()
// Set-up the Flutter Engine, if it's not already set up
if (!FlutterInjector.instance().flutterLoader().initialized()) {
FlutterInjector.instance().flutterLoader().startInitialization(applicationContext)
}
FlutterInjector.instance().flutterLoader().ensureInitializationComplete(
applicationContext,
null,
)
val callback: FlutterCallbackInformation =
FlutterCallbackInformation.lookupCallbackInformation(BackgroundServiceStatic.getHandle(this))
if (callback == null) {
Log.e(TAG, "Callback handle not found")
return
}
isRunning.set(true)
engine = FlutterEngine(this)
engine!!.getServiceControlSurface().attachToService(this, null, true)
methodChannel = MethodChannel(
engine!!.getDartExecutor()!!.getBinaryMessenger(),
SERVICE_BACKGROUND_METHOD_CHANNEL_KEY,
)
MoxxyBackgroundServiceApi.setUp(engine!!.getDartExecutor()!!.getBinaryMessenger(), this)
Log.d(TAG, "MoxxyBackgroundServiceApi ready")
dartCallback = DartExecutor.DartCallback(
assets,
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
callback,
)
engine!!.getDartExecutor().executeDartCallback(dartCallback!!)
} catch (ex: UnsatisfiedLinkError) {
Log.e(TAG, "Failed to set up background service: ${ex.message}")
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
notificationBody = SERVICE_DEFAULT_BODY
updateNotificationInfo()
}
override fun onDestroy() {
if (!isManuallyStopped) {
BackgroundServiceStatic.enqueue(this)
} else {
BackgroundServiceStatic.setManuallyStopped(applicationContext, true)
}
// Dispose of the engine
engine?.apply {
getServiceControlSurface().detachFromService()
destroy()
}
engine = null
dartCallback = null
// Stop the service
stopForeground(true)
isRunning.set(false)
super.onDestroy()
}
fun receiveData(data: String) {
methodChannel?.invokeMethod("dataReceived", data)
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
BackgroundServiceStatic.setManuallyStopped(this, false)
BackgroundServiceStatic.enqueue(this)
runService()
return START_STICKY
}
override fun getExtraData(): String {
return getSharedPreferences(SERVICE_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).getString(
SERVICE_EXTRA_DATA_KEY,
"",
)!!
}
override fun setNotificationBody(body: String) {
notificationBody = body
updateNotificationInfo()
}
override fun sendData(data: String) {
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(
Intent(SERVICE_FOREGROUND_METHOD_CHANNEL_KEY).apply {
putExtra("data", data)
},
)
}
override fun stop() {
isManuallyStopped = true
val mutable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val pendingIntent = PendingIntent.getBroadcast(
applicationContext,
111,
Intent(this, WatchdogReceiver::class.java),
PendingIntent.FLAG_CANCEL_CURRENT or mutable,
)
val stopManager = getSystemService(ALARM_SERVICE) as AlarmManager
stopManager.cancel(pendingIntent)
stopSelf()
BackgroundServiceStatic.setStartAtBoot(applicationContext, false)
}
}

View File

@@ -0,0 +1,25 @@
package org.moxxy.moxxy_native.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import org.moxxy.moxxy_native.TAG
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (BackgroundServiceStatic.getStartAtBoot(context)) {
if (BackgroundServiceStatic.wakeLock == null) {
Log.d(TAG, "WakeLock is null. Acquiring it...")
BackgroundServiceStatic.acquireWakeLock(context)
Log.d(TAG, "WakeLock acquired")
}
ContextCompat.startForegroundService(
context,
Intent(context, BackgroundService::class.java),
)
}
}
}

View File

@@ -0,0 +1,134 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.service
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyServiceApi {
fun configure(handle: Long, extraData: String)
fun isRunning(): Boolean
fun start()
fun sendData(data: String)
companion object {
/** The codec used by MoxxyServiceApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
}
/** Sets up an instance of `MoxxyServiceApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyServiceApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val handleArg = args[0].let { if (it is Int) it.toLong() else it as Long }
val extraDataArg = args[1] as String
var wrapped: List<Any?>
try {
api.configure(handleArg, extraDataArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.isRunning())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
api.start()
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.sendData", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val dataArg = args[0] as String
var wrapped: List<Any?>
try {
api.sendData(dataArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
package org.moxxy.moxxy_native.service
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import org.moxxy.moxxy_native.MoxxyNativePlugin
import org.moxxy.moxxy_native.TAG
import org.moxxy.moxxy_native.service.BackgroundServiceStatic.setStartAtBoot
object PluginTracker {
var instances: MutableList<MoxxyNativePlugin> = mutableListOf()
}
class ServiceImplementation(private val context: Context) : MoxxyServiceApi {
override fun configure(handle: Long, extraData: String) {
BackgroundServiceStatic.setConfiguration(
context,
handle,
extraData,
)
}
override fun isRunning(): Boolean {
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (info in manager.getRunningServices(Int.MAX_VALUE)) {
if (BackgroundService::class.java.name == info.service.className) {
return true
}
}
return false
}
override fun start() {
setStartAtBoot(context, true)
BackgroundServiceStatic.enqueue(context)
ContextCompat.startForegroundService(
context,
Intent(context, BackgroundService::class.java),
)
Log.d(TAG, "Background service started")
}
override fun sendData(data: String) {
for (plugin in PluginTracker.instances) {
val service = plugin.service
if (service != null) {
service.receiveData(data)
break
}
}
}
}

View File

@@ -0,0 +1,18 @@
package org.moxxy.moxxy_native.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import org.moxxy.moxxy_native.service.BackgroundServiceStatic.getManuallyStopped
class WatchdogReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (!getManuallyStopped(context)) {
ContextCompat.startForegroundService(
context,
Intent(context, BackgroundService::class.java),
)
}
}
}

View File

@@ -0,0 +1,133 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.service.background
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyBackgroundServiceApi {
fun getExtraData(): String
fun setNotificationBody(body: String)
fun sendData(data: String)
fun stop()
companion object {
/** The codec used by MoxxyBackgroundServiceApi. */
val codec: MessageCodec<Any?> by lazy {
StandardMessageCodec()
}
/** Sets up an instance of `MoxxyBackgroundServiceApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyBackgroundServiceApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
wrapped = listOf<Any?>(api.getExtraData())
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val bodyArg = args[0] as String
var wrapped: List<Any?>
try {
api.setNotificationBody(bodyArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val dataArg = args[0] as String
var wrapped: List<Any?>
try {
api.sendData(dataArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
var wrapped: List<Any?>
try {
api.stop()
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

@@ -1,13 +1,68 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxy_native/moxxy_native.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart';
@pragma('vm:entrypoint')
Future<void> serviceHandleData(Map<String, dynamic>? data) async {
print('[BG] Received data $data');
GetIt.I.get<BackgroundService>().send(
TestEvent(),
id: data!['id']! as String,
);
}
@pragma('vm:entry-point')
Future<void> serviceEntrypoint(String initialLocale) async {
// avoid_print
print('Initial locale: $initialLocale');
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class TestCommand extends BackgroundCommand {
@override
Map<String, dynamic> toJson() => {
'request': 'return_name',
};
}
class TestEvent extends BackgroundEvent {
@override
Map<String, dynamic> toJson() => {
'name': 'Moxxy',
};
}
class MyAppState extends State<MyApp> {
String? imagePath;
@override
void initState() {
super.initState();
const EventChannel('org.moxxy.moxxyv2/notification_stream')
.receiveBroadcastStream()
.listen(
(event) {
print('Keyboard height: ${event as double}');
},
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
@@ -21,7 +76,6 @@ class MyApp extends StatelessWidget {
onPressed: () async {
final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, false);
// ignore: avoid_print
print('User picked: $result');
},
child: const Text('Photo picker'),
@@ -30,7 +84,6 @@ class MyApp extends StatelessWidget {
onPressed: () async {
final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.imageAndVideo, true);
// ignore: avoid_print
print('User picked: $result');
},
child: const Text('Photo/Video multi-picker'),
@@ -39,11 +92,148 @@ class MyApp extends StatelessWidget {
onPressed: () async {
final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.generic, true);
// ignore: avoid_print
print('User picked: $result');
},
child: const Text('Generic multi-picker'),
),
TextButton(
onPressed: () async {
final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, false);
if (result.isEmpty) return;
final encDest = '${result.first!}.enc';
final decDest = '${result.first!}.dec';
final encResult = await MoxxyCryptographyApi().encryptFile(
result.first!,
encDest,
Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7,
'SHA-256',
);
if (encResult == null) {
print('Failed to encrypt file');
return;
}
final decResult = await MoxxyCryptographyApi().decryptFile(
encDest,
decDest,
Uint8List.fromList(List.filled(32, 1)),
Uint8List.fromList(List.filled(16, 2)),
CipherAlgorithm.aes256CbcPkcs7,
'SHA-256',
);
if (decResult == null) {
print('Failed to decrypt file');
return;
}
setState(() {
imagePath = decDest;
});
},
child: const Text('Test cryptography'),
),
if (imagePath != null) Image.file(File(imagePath!)),
TextButton(
onPressed: () async {
// Create channel
if (Platform.isAndroid) {
await MoxxyNotificationsApi().createNotificationChannels(
[
NotificationChannel(
id: 'foreground_service',
title: 'Foreground service',
description: 'lol',
importance: NotificationChannelImportance.MIN,
showBadge: false,
vibration: false,
enableLights: false,
),
],
);
await Permission.notification.request();
}
final srv = getForegroundService();
await srv.start(
const ServiceConfig(
serviceEntrypoint,
serviceHandleData,
'en',
),
(data) async {
print('[FG] Received data $data');
},
);
await Future<void>.delayed(const Duration(milliseconds: 600));
await getForegroundService().send(
TestCommand(),
awaitable: false,
);
},
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

@@ -0,0 +1 @@

View File

@@ -5,10 +5,10 @@ packages:
dependency: transitive
description:
name: async
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
@@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.3.0"
clock:
dependency: transitive
description:
@@ -37,10 +37,18 @@ packages:
dependency: transitive
description:
name: collection
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.17.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
url: "https://pub.dev"
source: hosted
version: "3.0.3"
cupertino_icons:
dependency: "direct main"
description:
@@ -75,14 +83,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
url: "https://pub.dev"
source: hosted
version: "7.6.0"
js:
dependency: transitive
description:
name: js
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7"
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.5"
version: "0.6.7"
lints:
dependency: transitive
description:
@@ -91,14 +107,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.1"
logging:
dependency: "direct main"
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72"
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev"
source: hosted
version: "0.12.13"
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
@@ -111,25 +135,33 @@ packages:
dependency: transitive
description:
name: meta
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42"
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.8.0"
version: "1.9.1"
moxlib:
dependency: transitive
description:
name: moxlib
sha256: "2a76a632d23ea73906964cee4463352995e40199036162217ea323a6c3846e73"
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
source: hosted
version: "0.2.0"
moxxy_native:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.1.0"
version: "0.2.0"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.2"
version: "1.8.3"
permission_handler:
dependency: "direct main"
description:
@@ -215,6 +247,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
term_glyph:
dependency: transitive
description:
@@ -227,10 +267,26 @@ packages:
dependency: transitive
description:
name: test_api
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev"
source: hosted
version: "0.4.16"
version: "0.5.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "https://pub.dev"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
@@ -240,5 +296,5 @@ packages:
source: hosted
version: "2.1.4"
sdks:
dart: ">=2.19.6 <3.0.0"
dart: ">=3.0.0-0 <4.0.0"
flutter: ">=2.8.0"

View File

@@ -28,6 +28,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
permission_handler: ^10.4.5
get_it: ^7.6.0
logging: ^1.2.0
path: ^1.8.3
dev_dependencies:
flutter_test:

View File

@@ -44,13 +44,16 @@
]);
lib = pkgs.lib;
pinnedJDK = pkgs.jdk17;
flutterVersion = pkgs.flutter37;
flutterVersion = pkgs.flutter;
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
# Android
pinnedJDK sdk ktlint
# Linux
clang cmake gtk3 ninja pkg-config xz pcre2 glib
# Flutter
flutterVersion
@@ -66,5 +69,19 @@
# an used parameter.
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${sdk}/share/android-sdk/build-tools/34.0.0/aapt2";
};
apps = {
androidLint = let
script = pkgs.writeShellScript "lint-android.sh" ''
${pkgs.ktlint}/bin/ktlint \
--format \
--disabled_rules=standard:package-name \
android/src/main/kotlin/org/moxxy/moxxy_native/
'';
in {
program = "${script}";
type = "app";
};
};
});
}

View File

@@ -1,2 +1,12 @@
export 'pigeon/background_service.g.dart';
export 'pigeon/contacts.g.dart';
export 'pigeon/cryptography.g.dart';
export 'pigeon/media.g.dart';
export 'pigeon/notifications.g.dart';
export 'pigeon/picker.g.dart';
export 'pigeon/platform.g.dart';
export 'pigeon/service.g.dart';
export 'src/service/background/base.dart';
export 'src/service/config.dart';
export 'src/service/datasender/types.dart';
export 'src/service/foreground/base.dart';

View File

@@ -0,0 +1,114 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
class MoxxyBackgroundServiceApi {
/// Constructor for [MoxxyBackgroundServiceApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyBackgroundServiceApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec();
Future<String> getExtraData() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.getExtraData',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as String?)!;
}
}
Future<void> setNotificationBody(String arg_body) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.setNotificationBody',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_body]) 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;
}
}
Future<void> sendData(String arg_data) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.sendData',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_data]) 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;
}
}
Future<void> stop() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyBackgroundServiceApi.stop', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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,55 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
/// The type of icon to use when no avatar path is provided.
enum FallbackIconType {
none,
person,
notes,
}
class MoxxyContactsApi {
/// Constructor for [MoxxyContactsApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyContactsApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec();
Future<void> recordSentMessage(String arg_name, String arg_jid,
String? arg_avatarPath, FallbackIconType arg_fallbackIcon) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyContactsApi.recordSentMessage',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_name,
arg_jid,
arg_avatarPath,
arg_fallbackIcon.index
]) 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,168 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
enum CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7,
}
class CryptographyResult {
CryptographyResult({
required this.plaintextHash,
required this.ciphertextHash,
});
Uint8List plaintextHash;
Uint8List ciphertextHash;
Object encode() {
return <Object?>[
plaintextHash,
ciphertextHash,
];
}
static CryptographyResult decode(Object result) {
result as List<Object?>;
return CryptographyResult(
plaintextHash: result[0]! as Uint8List,
ciphertextHash: result[1]! as Uint8List,
);
}
}
class _MoxxyCryptographyApiCodec extends StandardMessageCodec {
const _MoxxyCryptographyApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is CryptographyResult) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return CryptographyResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class MoxxyCryptographyApi {
/// Constructor for [MoxxyCryptographyApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyCryptographyApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = _MoxxyCryptographyApiCodec();
Future<CryptographyResult?> encryptFile(
String arg_sourcePath,
String arg_destPath,
Uint8List arg_key,
Uint8List arg_iv,
CipherAlgorithm arg_algorithm,
String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.encryptFile',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_sourcePath,
arg_destPath,
arg_key,
arg_iv,
arg_algorithm.index,
arg_hashSpec
]) 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 (replyList[0] as CryptographyResult?);
}
}
Future<CryptographyResult?> decryptFile(
String arg_sourcePath,
String arg_destPath,
Uint8List arg_key,
Uint8List arg_iv,
CipherAlgorithm arg_algorithm,
String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.decryptFile',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(<Object?>[
arg_sourcePath,
arg_destPath,
arg_key,
arg_iv,
arg_algorithm.index,
arg_hashSpec
]) 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 (replyList[0] as CryptographyResult?);
}
}
Future<Uint8List?> hashFile(
String arg_sourcePath, String arg_hashSpec) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.hashFile', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_sourcePath, arg_hashSpec]) 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 (replyList[0] as Uint8List?);
}
}
}

49
lib/pigeon/media.g.dart Normal file
View File

@@ -0,0 +1,49 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
class MoxxyMediaApi {
/// Constructor for [MoxxyMediaApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyMediaApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec();
Future<bool> generateVideoThumbnail(
String arg_src, String arg_dest, int arg_maxWidth) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyMediaApi.generateVideoThumbnail',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_src, arg_dest, arg_maxWidth]) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as bool?)!;
}
}
}

200
lib/pigeon/platform.g.dart Normal file
View File

@@ -0,0 +1,200 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
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 {
/// Constructor for [MoxxyPlatformApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyPlatformApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = _MoxxyPlatformApiCodec();
Future<String> getPersistentDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.getPersistentDataPath',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as String?)!;
}
}
Future<String> getCacheDataPath() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.getCacheDataPath',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as String?)!;
}
}
Future<void> openBatteryOptimisationSettings() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.openBatteryOptimisationSettings',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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;
}
}
Future<bool> isIgnoringBatteryOptimizations() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPlatformApi.isIgnoringBatteryOptimizations',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
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;
}
}
}

111
lib/pigeon/service.g.dart Normal file
View File

@@ -0,0 +1,111 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
class MoxxyServiceApi {
/// Constructor for [MoxxyServiceApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
MoxxyServiceApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = StandardMessageCodec();
Future<void> configure(int arg_handle, String arg_extraData) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.configure', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_handle, arg_extraData]) 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;
}
}
Future<bool> isRunning() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.isRunning', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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 if (replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (replyList[0] as bool?)!;
}
}
Future<void> start() async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.start', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel.send(null) 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;
}
}
Future<void> sendData(String arg_data) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyServiceApi.sendData', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_data]) 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,16 @@
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
/// Wrapper API that is only available to the background service.
abstract class BackgroundService {
/// Send [event] with optional id [id] to the foreground.
Future<void> send(BackgroundEvent event, {String? id});
/// Platform specific initialization routine that is called after
/// the entrypoint has been called.
Future<void> init(ServiceConfig config);
/// Update the notification body, if the platform shows a persistent
/// notification.
void setNotificationBody(String body);
}

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,58 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/background_service.g.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 PigeonBackgroundService extends BackgroundService {
final MoxxyBackgroundServiceApi _api = MoxxyBackgroundServiceApi();
/// A method channel for Foreground -> Service communication
// TODO(Unknown): Move this into a constant for reuse
final MethodChannel _channel =
const MethodChannel('org.moxxy.moxxy_native/background');
/// A logger.
final Logger _log = Logger('PigeonBackgroundService');
@override
Future<void> send(BackgroundEvent event, {String? id}) async {
final data = DataWrapper(
id ?? const Uuid().v4(),
event,
);
await _api.sendData(jsonEncode(data.toJson()));
}
@override
Future<void> init(
ServiceConfig config,
) async {
// Ensure that the Dart executor is ready to use plugins
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
// Register the channel for Foreground -> Service communication
_channel.setMethodCallHandler((call) async {
// TODO(Unknown): Maybe do something smarter like pigeon and use Lists instead of Maps
final args = call.arguments! as String;
await config.handleData(jsonDecode(args) as Map<String, dynamic>);
});
// Start execution
_log.finest('Setup complete. Calling main entrypoint...');
await config.entrypoint(config.initialLocale);
}
@override
void setNotificationBody(String body) {
_api.setNotificationBody(body);
}
}

View File

@@ -0,0 +1,55 @@
import 'dart:convert';
import 'dart:ui';
/// A function that can act as a service entrypoint.
typedef EntrypointCallback = Future<void> Function(String initialLocale);
/// A function that can be called when data is received.
typedef HandleEventCallback = Future<void> Function(Map<String, dynamic>? data);
/// Configuration that will be passed to the service's entrypoint
class ServiceConfig {
const ServiceConfig(
this.entrypoint,
this.handleData,
this.initialLocale,
);
/// Reconstruct the configuration from a JSON string.
factory ServiceConfig.fromString(String rawData) {
final data = jsonDecode(rawData) as Map<String, dynamic>;
return ServiceConfig(
PluginUtilities.getCallbackFromHandle(
CallbackHandle.fromRawHandle(
data['entrypoint']! as int,
),
)! as EntrypointCallback,
PluginUtilities.getCallbackFromHandle(
CallbackHandle.fromRawHandle(
data['handleData']! as int,
),
)! as HandleEventCallback,
data['initialLocale']! as String,
);
}
/// The initial locale to use.
final String initialLocale;
/// The entrypoint to call into.
final EntrypointCallback entrypoint;
/// Entry function to call when the service receives data.
final HandleEventCallback handleData;
@override
String toString() {
return jsonEncode({
'entrypoint':
PluginUtilities.getCallbackHandle(entrypoint)!.toRawHandle(),
'handleData':
PluginUtilities.getCallbackHandle(handleData)!.toRawHandle(),
'initialLocale': initialLocale,
});
}
}

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

@@ -0,0 +1,15 @@
import 'dart:convert';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
class PigeonForegroundServiceDataSender
extends AwaitableDataSender<BackgroundCommand, BackgroundEvent> {
PigeonForegroundServiceDataSender(this._api);
final MoxxyServiceApi _api;
@override
Future<void> sendDataImpl(DataWrapper<JsonImplementation> data) {
return _api.sendData(jsonEncode(data.toJson()));
}
}

View File

@@ -0,0 +1,8 @@
import 'package:moxlib/moxlib.dart';
typedef ForegroundServiceDataSender
= AwaitableDataSender<BackgroundCommand, BackgroundEvent>;
abstract class BackgroundCommand implements JsonImplementation {}
abstract class BackgroundEvent implements JsonImplementation {}

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

@@ -0,0 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:get_it/get_it.dart';
import 'package:moxxy_native/pigeon/background_service.g.dart';
import 'package:moxxy_native/src/service/background/base.dart';
import 'package:moxxy_native/src/service/background/pigeon.dart';
import 'package:moxxy_native/src/service/config.dart';
/// An entrypoint that should be used when the service runs
/// in a new Flutter Engine.
@pragma('vm:entry-point')
Future<void> pigeonEntrypoint(dynamic _) async {
// ignore: avoid_print
print('androidEntrypoint: Called on new FlutterEngine');
// Pull and deserialize the extra data passed on.
WidgetsFlutterBinding.ensureInitialized();
final config = ServiceConfig.fromString(
await MoxxyBackgroundServiceApi().getExtraData(),
);
// Setup the background service
final srv = PigeonBackgroundService();
GetIt.I.registerSingleton<BackgroundService>(srv);
await srv.init(config);
}

View File

@@ -0,0 +1,8 @@
import 'dart:io';
/// An exception representing that moxxy_native does not support the given platform.
class UnsupportedPlatformException implements Exception {
UnsupportedPlatformException();
String get message => 'Unsupported platform "${Platform.operatingSystem}"';
}

View File

@@ -0,0 +1,52 @@
import 'dart:io';
import 'package:moxlib/moxlib.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/types.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';
/// Wrapper API that is only available to the UI isolate.
// TODO(Unknown): Dumb naming. Name it something better
abstract class ForegroundService {
/// Perform setup such that we [handleData] is called whenever the background service
/// sends data to the foreground.
Future<void> attach(HandleEventCallback handleData);
/// Start the background service with the config [config]. Additionally, perform
/// setup such that [uiHandleData] is called whenever the background service sends
/// data to the foreground.
Future<void> start(ServiceConfig config, HandleEventCallback uiHandleData);
/// Return true if the background service is running. False, if not.
Future<bool> isRunning();
/// Return the [AwaitableDataSender] that is used to send data to the background service.
ForegroundServiceDataSender getDataSender();
/// Convenience wrapper around getDataSender().sendData. The arguments are the same
/// as for [AwaitableDataSender].
Future<BackgroundEvent?> send(
BackgroundCommand command, {
bool awaitable = true,
});
}
/// "Singleton" ForegroundService instance to prevent having to type "GetIt.I.get<ForegroundService>()"
ForegroundService? _service;
/// Either returns or creates a [ForegroundService] object of the correct type for the
/// current platform.
ForegroundService getForegroundService() {
if (_service == null) {
if (Platform.isAndroid) {
_service = PigeonForegroundService();
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
_service = IsolateForegroundService();
} else {
throw UnsupportedPlatformException();
}
}
return _service!;
}

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

View File

@@ -0,0 +1,82 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:moxxy_native/pigeon/service.g.dart';
import 'package:moxxy_native/src/service/config.dart';
import 'package:moxxy_native/src/service/datasender/pigeon.dart';
import 'package:moxxy_native/src/service/datasender/types.dart';
import 'package:moxxy_native/src/service/entrypoints/pigeon.dart';
import 'package:moxxy_native/src/service/foreground/base.dart';
class PigeonForegroundService extends ForegroundService {
PigeonForegroundService() {
_dataSender = PigeonForegroundServiceDataSender(_api);
}
/// Pigeon channel to the native side.
final MoxxyServiceApi _api = MoxxyServiceApi();
/// A method channel for background service -> UI isolate communication.
final MethodChannel _channel =
const MethodChannel('org.moxxy.moxxy_native/foreground');
/// The data sender backing this class.
late final PigeonForegroundServiceDataSender _dataSender;
/// A logger.
final Logger _log = Logger('PigeonForegroundService');
@override
Future<void> attach(
HandleEventCallback handleData,
) async {
_channel.setMethodCallHandler((call) async {
await handleData(
jsonDecode(call.arguments! as String) as Map<String, dynamic>,
);
});
}
@override
Future<void> start(
ServiceConfig config,
HandleEventCallback uiHandleData,
) async {
await _api.configure(
PluginUtilities.getCallbackHandle(
pigeonEntrypoint,
)!
.toRawHandle(),
config.toString(),
);
// Prepare the method channel
await attach(uiHandleData);
// Start the service
await _api.start();
_log.finest('Background service started...');
}
@override
Future<bool> isRunning() async {
WidgetsFlutterBinding.ensureInitialized();
return _api.isRunning();
}
@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

@@ -0,0 +1,22 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/background_service.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/service/background/BackgroundServiceApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.service.background',
),
),
)
@HostApi()
abstract class MoxxyBackgroundServiceApi {
String getExtraData();
void setNotificationBody(String body);
void sendData(String data);
void stop();
}

29
pigeon/contacts.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/contacts.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/contacts/ContactsApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.contacts',
),
),
)
/// The type of icon to use when no avatar path is provided.
enum FallbackIconType {
none,
person,
notes;
}
@HostApi()
abstract class MoxxyContactsApi {
void recordSentMessage(
String name,
String jid,
String? avatarPath,
FallbackIconType fallbackIcon,
);
}

52
pigeon/cryptography.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/cryptography.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/cryptography/CryptographyApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.cryptography',
),
),
)
enum CipherAlgorithm {
aes128GcmNoPadding,
aes256GcmNoPadding,
aes256CbcPkcs7;
}
class CryptographyResult {
const CryptographyResult(this.plaintextHash, this.ciphertextHash);
final Uint8List plaintextHash;
final Uint8List ciphertextHash;
}
@HostApi()
abstract class MoxxyCryptographyApi {
@async
CryptographyResult? encryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
);
@async
CryptographyResult? decryptFile(
String sourcePath,
String destPath,
Uint8List key,
Uint8List iv,
CipherAlgorithm algorithm,
String hashSpec,
);
@async
Uint8List? hashFile(
String sourcePath,
String hashSpec,
);
}

16
pigeon/media.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/media.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/media/MediaApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.media',
),
),
)
@HostApi()
abstract class MoxxyMediaApi {
bool generateVideoThumbnail(String src, String dest, int maxWidth);
}

View File

@@ -3,7 +3,8 @@ import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/notifications.g.dart',
kotlinOut: 'android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.notifications',
),
@@ -54,9 +55,16 @@ class NotificationMessage {
}
class MessagingNotification {
const MessagingNotification(this.title, this.id, this.jid, this.messages,
this.channelId, this.isGroupchat, this.extra,
{this.groupId});
const MessagingNotification(
this.title,
this.id,
this.jid,
this.messages,
this.channelId,
this.isGroupchat,
this.extra, {
this.groupId,
});
/// The title of the conversation.
final String title;
@@ -91,8 +99,13 @@ enum NotificationIcon {
class RegularNotification {
const RegularNotification(
this.title, this.body, this.channelId, this.id, this.icon,
{this.groupId});
this.title,
this.body,
this.channelId,
this.id,
this.icon, {
this.groupId,
});
/// The title of the notification.
final String title;
@@ -165,6 +178,7 @@ class NotificationGroup {
final String description;
}
// ignore: constant_identifier_names
enum NotificationChannelImportance { MIN, HIGH, DEFAULT }
class NotificationChannel {

View File

@@ -3,13 +3,13 @@ import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/picker.g.dart',
kotlinOut: 'android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.picker',
),
),
)
enum FilePickerType {
/// Pick only image(s)
image,

31
pigeon/platform.dart Normal file
View File

@@ -0,0 +1,31 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/platform.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/platform/PlatformApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.platform',
),
),
)
class ShareItem {
const ShareItem(this.path, this.mime, this.text);
final String? path;
final String mime;
final String? text;
}
@HostApi()
abstract class MoxxyPlatformApi {
String getPersistentDataPath();
String getCacheDataPath();
void openBatteryOptimisationSettings();
bool isIgnoringBatteryOptimizations();
void shareItems(List<ShareItem> items, String genericMimeType);
}

22
pigeon/service.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/service.g.dart',
kotlinOut:
'android/src/main/kotlin/org/moxxy/moxxy_native/service/ServiceApi.kt',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.service',
),
),
)
@HostApi()
abstract class MoxxyServiceApi {
void configure(int handle, String extraData);
bool isRunning();
void start();
void sendData(String data);
}

View File

@@ -1,6 +1,6 @@
name: moxxy_native
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
homepage:
@@ -11,6 +11,12 @@ environment:
dependencies:
flutter:
sdk: flutter
get_it: ^7.6.0
logging: ^1.2.0
moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.2.0
uuid: ^3.0.5
dev_dependencies:
flutter_lints: ^2.0.0
@@ -23,3 +29,5 @@ flutter:
android:
package: org.moxxy.moxxy_native
pluginClass: MoxxyNativePlugin
linux:
pluginClass: MoxxyNativePlugin

6
scripts/lint.sh Normal file
View File

@@ -0,0 +1,6 @@
# Format and lint the Dart code
dart format .
flutter analyze
# Format and lint the Kotlin code
ktlint --disabled_rules=standard:package-name --format android/src/main/kotlin/org/moxxy/moxxy_native