diff --git a/android/build.gradle b/android/build.gradle
index 9976768..f9e4677 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -48,4 +48,5 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.activity:activity-ktx:1.7.2"
+ implementation "androidx.datastore:datastore-preferences:1.0.0"
}
\ No newline at end of file
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 6b1d6f6..9de0eaa 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,3 +1,19 @@
+ package="org.moxxy.moxxy_native">
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
index 67a3a9a..f3f2ce0 100644
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/Constants.kt
@@ -2,7 +2,35 @@ package org.moxxy.moxxy_native
const val TAG = "moxxy_native"
+// The data key for text entered in the notification's reply field
+const val REPLY_TEXT_KEY = "key_reply_text"
+
+// The key for the notification id to mark as read
+const val MARK_AS_READ_ID_KEY = "notification_id"
+
+// Values for actions performed through the notification
+const val REPLY_ACTION = "reply"
+const val MARK_AS_READ_ACTION = "mark_as_read"
+const val TAP_ACTION = "tap"
+
+// Extra data keys for the intents that reach the NotificationReceiver
+const val NOTIFICATION_EXTRA_JID_KEY = "jid"
+const val NOTIFICATION_EXTRA_ID_KEY = "notification_id"
+
+// Extra data keys for messages embedded inside the notification style
+const val NOTIFICATION_MESSAGE_EXTRA_MIME = "mime"
+const val NOTIFICATION_MESSAGE_EXTRA_PATH = "path"
+
+const val MOXXY_FILEPROVIDER_ID = "org.moxxy.moxxyv2.fileprovider"
+
+// Shared preferences keys
+const val SHARED_PREFERENCES_KEY = "org.moxxy.moxxyv2"
+const val SHARED_PREFERENCES_YOU_KEY = "you"
+const val SHARED_PREFERENCES_MARK_AS_READ_KEY = "mark_as_read"
+const val SHARED_PREFERENCES_REPLY_KEY = "reply"
+const val SHARED_PREFERENCES_AVATAR_KEY = "avatar_path"
+
// Request codes
const val PICK_FILE_REQUEST = 42
const val PICK_FILES_REQUEST = 43
-const val PICK_FILE_WITH_DATA_REQUEST = 44
\ No newline at end of file
+const val PICK_FILE_WITH_DATA_REQUEST = 44
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
index 90fc20f..e7d01ab 100644
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/MoxxyNativePlugin.kt
@@ -1,110 +1,200 @@
package org.moxxy.moxxy_native
import android.app.Activity
+import android.app.NotificationManager
import android.content.Context
import android.content.Intent
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 io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
-import org.moxxy.moxxy_native.generated.FilePickerType
-import org.moxxy.moxxy_native.generated.MoxxyPickerApi
+import io.flutter.plugin.common.EventChannel
+import org.moxxy.moxxy_native.notifications.MessagingNotification
+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.picker.FilePickerType
+import org.moxxy.moxxy_native.picker.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener
-class MoxxyNativePlugin: FlutterPlugin, ActivityAware, MoxxyPickerApi {
- private var context: Context? = null
- private var activity: Activity? = null
- private lateinit var pickerListener: PickerResultListener
-
- override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
- context = flutterPluginBinding.applicationContext
- MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
- pickerListener = PickerResultListener(context!!)
- Log.d(TAG, "Attached to engine")
- }
-
- override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
- Log.d(TAG, "Detached from engine")
- }
-
- override fun onAttachedToActivity(binding: ActivityPluginBinding) {
- activity = binding.activity
- binding.addActivityResultListener(pickerListener)
- Log.d(TAG, "Attached to activity")
- }
-
- override fun onDetachedFromActivityForConfigChanges() {
- activity = null
- Log.d(TAG, "Detached from activity")
- }
-
- override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
- activity = binding.activity
- }
-
- override fun onDetachedFromActivity() {
- activity = null
- Log.d(TAG, "Detached from activity")
- }
-
- override fun pickFiles(
- type: FilePickerType,
- multiple: Boolean,
- callback: (Result>) -> Unit
- ) {
- val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
- AsyncRequestTracker.requestTracker[requestCode] = callback as (Result) -> Unit
- if (type == FilePickerType.GENERIC) {
- val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- this.type = "*/*"
-
- // Allow/disallow picking multiple files
- putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
- }
- activity?.startActivityForResult(pickIntent, requestCode)
- return
- }
-
- val contract = when (multiple) {
- false -> ActivityResultContracts.PickVisualMedia()
- true -> ActivityResultContracts.PickMultipleVisualMedia()
- }
- val pickType = when (type) {
- // We keep FilePickerType.GENERIC here, even though we know that @type will never be
- // GENERIC to make Kotlin happy.
- FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
- FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
- FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
- }
- val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
- activity?.startActivityForResult(pickIntent, requestCode)
- }
-
- override fun pickFileWithData(type: FilePickerType, callback: (Result) -> Unit) {
- AsyncRequestTracker.requestTracker[PICK_FILE_WITH_DATA_REQUEST] = callback as (Result) -> Unit
- if (type == FilePickerType.GENERIC) {
- val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- this.type = "*/*"
- }
- activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
- return
- }
-
- val pickType = when (type) {
- // We keep FilePickerType.GENERIC here, even though we know that @type will never be
- // GENERIC to make Kotlin happy.
- FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
- FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
- FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
- }
- val contract = ActivityResultContracts.PickVisualMedia()
- val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
- activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
- }
+object MoxxyEventChannels {
+ var notificationChannel: EventChannel? = null
+ var notificationEventSink: EventChannel.EventSink? = null
+}
+
+object NotificationStreamHandler : EventChannel.StreamHandler {
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ Log.d(TAG, "NotificationStreamHandler: Attached stream")
+ MoxxyEventChannels.notificationEventSink = events
+ }
+
+ override fun onCancel(arguments: Any?) {
+ Log.d(TAG, "NotificationStreamHandler: Detached stream")
+ MoxxyEventChannels.notificationEventSink = null
+ }
+}
+
+/*
+ * Hold the last notification event in case we did a cold start.
+ */
+object NotificationCache {
+ var lastEvent: NotificationEvent? = null
+}
+
+class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNotificationsApi {
+ private var context: Context? = null
+ private var activity: Activity? = null
+ private lateinit var activityClass: Class
+ private lateinit var pickerListener: PickerResultListener
+
+ override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ context = flutterPluginBinding.applicationContext
+ MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
+ MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, this)
+ pickerListener = PickerResultListener(context!!)
+ Log.d(TAG, "Attached to engine")
+ }
+
+ override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+ Log.d(TAG, "Detached from engine")
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ activity = binding.activity
+ activityClass = activity!!.javaClass
+ binding.addActivityResultListener(pickerListener)
+ Log.d(TAG, "Attached to activity")
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ activity = null
+ Log.d(TAG, "Detached from activity")
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ activity = binding.activity
+ }
+
+ override fun onDetachedFromActivity() {
+ activity = null
+ Log.d(TAG, "Detached from activity")
+ }
+
+ override fun pickFiles(
+ type: FilePickerType,
+ multiple: Boolean,
+ callback: (Result>) -> Unit,
+ ) {
+ val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
+ AsyncRequestTracker.requestTracker[requestCode] = callback as (Result) -> Unit
+ if (type == FilePickerType.GENERIC) {
+ val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ this.type = "*/*"
+
+ // Allow/disallow picking multiple files
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
+ }
+ activity?.startActivityForResult(pickIntent, requestCode)
+ return
+ }
+
+ val contract = when (multiple) {
+ false -> ActivityResultContracts.PickVisualMedia()
+ true -> ActivityResultContracts.PickMultipleVisualMedia()
+ }
+ val pickType = when (type) {
+ // We keep FilePickerType.GENERIC here, even though we know that @type will never be
+ // GENERIC to make Kotlin happy.
+ FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
+ FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
+ FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
+ }
+ val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
+ activity?.startActivityForResult(pickIntent, requestCode)
+ }
+
+ override fun pickFileWithData(type: FilePickerType, callback: (Result) -> Unit) {
+ AsyncRequestTracker.requestTracker[PICK_FILE_WITH_DATA_REQUEST] = callback as (Result) -> Unit
+ if (type == FilePickerType.GENERIC) {
+ val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ this.type = "*/*"
+ }
+ activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
+ return
+ }
+
+ val pickType = when (type) {
+ // We keep FilePickerType.GENERIC here, even though we know that @type will never be
+ // GENERIC to make Kotlin happy.
+ FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
+ FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly
+ FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo
+ }
+ val contract = ActivityResultContracts.PickVisualMedia()
+ val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
+ activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST)
+ }
+
+ override fun createNotificationGroups(groups: List) {
+ createNotificationGroupsImpl(context!!, groups)
+ }
+
+ override fun deleteNotificationGroups(ids: List) {
+ val notificationManager = context!!.getSystemService(NotificationManager::class.java)
+ for (id in ids) {
+ notificationManager.deleteNotificationChannelGroup(id)
+ }
+ }
+
+ override fun createNotificationChannels(channels: List) {
+ createNotificationChannelsImpl(context!!, channels)
+ }
+
+ override fun deleteNotificationChannels(ids: List) {
+ 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")
+ }
}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/content/MoxxyFileProvider.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/content/MoxxyFileProvider.kt
new file mode 100644
index 0000000..63639b1
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/content/MoxxyFileProvider.kt
@@ -0,0 +1,6 @@
+package org.moxxy.moxxy_native.content
+
+import androidx.core.content.FileProvider
+import org.moxxy.moxxy_native.R
+
+class MoxxyFileProvider : FileProvider(R.xml.file_paths)
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/generated/PickerApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/generated/PickerApi.kt
deleted file mode 100644
index dd42fcc..0000000
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/generated/PickerApi.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-// Autogenerated from Pigeon (v11.0.1), do not edit directly.
-// See also: https://pub.dev/packages/pigeon
-
-package org.moxxy.moxxy_native.generated
-
-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 {
- return listOf(result)
-}
-
-private fun wrapError(exception: Throwable): List {
- 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 FilePickerType(val raw: Int) {
- /** Pick only image(s) */
- IMAGE(0),
- /** Pick only video(s) */
- VIDEO(1),
- /** Pick image(s) and video(s) */
- IMAGEANDVIDEO(2),
- /** Pick any kind of file(s) */
- GENERIC(3);
-
- companion object {
- fun ofRaw(raw: Int): FilePickerType? {
- return values().firstOrNull { it.raw == raw }
- }
- }
-}
-
-/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
-interface MoxxyPickerApi {
- /**
- * Open either the photo picker or the generic file picker to get a list of paths that were
- * selected and are accessable. If the list is empty, then the user dismissed the picker without
- * selecting anything.
- *
- * [type] specifies what kind of file(s) should be picked.
- *
- * [multiple] controls whether multiple files can be picked (true) or just a single file
- * is enough (false).
- */
- fun pickFiles(type: FilePickerType, multiple: Boolean, callback: (Result>) -> Unit)
- /** Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. */
- fun pickFileWithData(type: FilePickerType, callback: (Result) -> Unit)
-
- companion object {
- /** The codec used by MoxxyPickerApi. */
- val codec: MessageCodec by lazy {
- StandardMessageCodec()
- }
- /** Sets up an instance of `MoxxyPickerApi` to handle messages through the `binaryMessenger`. */
- @Suppress("UNCHECKED_CAST")
- fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyPickerApi?) {
- run {
- val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles", codec)
- if (api != null) {
- channel.setMessageHandler { message, reply ->
- val args = message as List
- val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
- val multipleArg = args[1] as Boolean
- api.pickFiles(typeArg, multipleArg) { result: Result> ->
- 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(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFileWithData", codec)
- if (api != null) {
- channel.setMessageHandler { message, reply ->
- val args = message as List
- val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
- api.pickFileWithData(typeArg) { result: Result ->
- val error = result.exceptionOrNull()
- if (error != null) {
- reply.reply(wrapError(error))
- } else {
- val data = result.getOrNull()
- reply.reply(wrapResult(data))
- }
- }
- }
- } else {
- channel.setMessageHandler(null)
- }
- }
- }
- }
-}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationReceiver.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationReceiver.kt
new file mode 100644
index 0000000..0292067
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationReceiver.kt
@@ -0,0 +1,212 @@
+package org.moxxy.moxxy_native.notifications
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+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
+import org.moxxy.moxxy_native.NOTIFICATION_MESSAGE_EXTRA_PATH
+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 java.io.File
+import java.time.Instant
+
+fun extractPayloadMapFromIntent(intent: Intent): Map {
+ val extras = mutableMapOf()
+ 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.
+ * */
+ private fun dismissNotification(context: Context, intent: Intent) {
+ // Dismiss the notification
+ val notificationId = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
+ if (notificationId != -1) {
+ NotificationManagerCompat.from(context).cancel(
+ notificationId,
+ )
+ } else {
+ Log.e("NotificationReceiver", "No id specified. Cannot dismiss notification")
+ }
+ }
+
+ private fun findActiveNotification(context: Context, id: Int): Notification? {
+ return (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
+ .activeNotifications
+ .find { it.id == id }?.notification
+ }
+
+ private fun handleMarkAsRead(context: Context, intent: Intent) {
+ MoxxyEventChannels.notificationEventSink?.success(
+ NotificationEvent(
+ intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
+ intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
+ NotificationEventType.MARKASREAD,
+ null,
+ extractPayloadMapFromIntent(intent),
+ ).toList(),
+ )
+ dismissNotification(context, intent)
+ }
+
+ private fun handleReply(context: Context, intent: Intent) {
+ val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
+ val replyPayload = remoteInput.getCharSequence(REPLY_TEXT_KEY)
+ MoxxyEventChannels.notificationEventSink?.success(
+ NotificationEvent(
+ intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
+ intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
+ NotificationEventType.REPLY,
+ replyPayload.toString(),
+ extractPayloadMapFromIntent(intent),
+ ).toList(),
+ )
+
+ val id = intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1).toInt()
+ if (id == -1) {
+ Log.e(TAG, "Failed to find notification id for reply")
+ return
+ }
+
+ val notification = findActiveNotification(context, id)
+ if (notification == null) {
+ Log.e(TAG, "Failed to find notification for id $id")
+ return
+ }
+
+ // Thanks https://medium.com/@sidorovroman3/android-how-to-use-messagingstyle-for-notifications-without-caching-messages-c414ef2b816c
+ val recoveredStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification)!!
+ val newStyle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ Notification.MessagingStyle(
+ android.app.Person.Builder().apply {
+ setName(NotificationDataManager.getYou(context))
+
+ // Set an avatar, if we have one
+ val avatarPath = NotificationDataManager.getAvatarPath(context)
+ if (avatarPath != null) {
+ setIcon(
+ Icon.createWithAdaptiveBitmap(
+ BitmapFactory.decodeFile(avatarPath),
+ ),
+ )
+ }
+ }.build(),
+ )
+ } else {
+ Notification.MessagingStyle(NotificationDataManager.getYou(context))
+ }
+
+ newStyle.apply {
+ conversationTitle = recoveredStyle.conversationTitle
+ recoveredStyle.messages.forEach {
+ // Check if we have to request (or refresh) the content URI to be able to still
+ // see the embedded image.
+ val mime = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_MIME)
+ val path = it.extras.getString(NOTIFICATION_MESSAGE_EXTRA_PATH)
+ val message = Notification.MessagingStyle.Message(it.text, it.timestamp, it.sender)
+ if (mime != null && path != null) {
+ // Request a new URI from the file provider to ensure we can still see the image
+ // in the notification
+ val fileUri = FileProvider.getUriForFile(
+ context,
+ MOXXY_FILEPROVIDER_ID,
+ File(path),
+ )
+ message.setData(
+ mime,
+ fileUri,
+ )
+
+ // As we're creating a new message, also recreate the additional metadata
+ message.extras.apply {
+ putString(NOTIFICATION_MESSAGE_EXTRA_MIME, mime)
+ putString(NOTIFICATION_MESSAGE_EXTRA_PATH, path)
+ }
+ }
+
+ // Append the old message
+ addMessage(message)
+ }
+ }
+
+ // Append our new message
+ newStyle.addMessage(
+ Notification.MessagingStyle.Message(
+ replyPayload!!,
+ Instant.now().toEpochMilli(),
+ null as CharSequence?,
+ ),
+ )
+
+ // Post the new notification
+ val recoveredBuilder = Notification.Builder.recoverBuilder(context, notification).apply {
+ style = newStyle
+ setOnlyAlertOnce(true)
+ }
+
+ try {
+ NotificationManagerCompat.from(context).notify(id, recoveredBuilder.build())
+ } catch (ex: SecurityException) {
+ Log.e(TAG, "Failed to post reply-notification: ${ex.message}")
+ }
+ }
+
+ fun handleTap(context: Context, intent: Intent) {
+ MoxxyEventChannels.notificationEventSink?.success(
+ NotificationEvent(
+ intent.getLongExtra(NOTIFICATION_EXTRA_ID_KEY, -1),
+ intent.getStringExtra(NOTIFICATION_EXTRA_JID_KEY)!!,
+ NotificationEventType.OPEN,
+ null,
+ extractPayloadMapFromIntent(intent),
+ ).toList(),
+ )
+
+ // Bring the app into the foreground
+ Log.d(TAG, "Querying launch intent for ${context.packageName}")
+ val tapIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!
+ Log.d(TAG, "Starting activity")
+ context.startActivity(tapIntent)
+
+ // Dismiss the notification
+ Log.d(TAG, "Dismissing notification")
+ dismissNotification(context, intent)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ // TODO: We need to be careful to ensure that the Flutter engine is running.
+ // If it's not, we have to start it. However, that's only an issue when we expect to
+ // receive notifications while not running, i.e. Push Notifications.
+ when (intent.action) {
+ MARK_AS_READ_ACTION -> handleMarkAsRead(context, intent)
+ REPLY_ACTION -> handleReply(context, intent)
+ TAP_ACTION -> handleTap(context, intent)
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/Notifications.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/Notifications.kt
new file mode 100644
index 0000000..7ed0ce8
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/Notifications.kt
@@ -0,0 +1,362 @@
+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) {
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
+ for (group in groups) {
+ notificationManager.createNotificationChannelGroup(
+ NotificationChannelGroup(group.id, group.description),
+ )
+ }
+}
+
+fun createNotificationChannelsImpl(context: Context, channels: List) {
+ 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}")
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt
new file mode 100644
index 0000000..4a3272d
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/notifications/NotificationsApi.kt
@@ -0,0 +1,671 @@
+// Autogenerated from Pigeon (v11.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package org.moxxy.moxxy_native.notifications
+
+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 {
+ return listOf(result)
+}
+
+private fun wrapError(exception: Throwable): List {
+ 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 NotificationIcon(val raw: Int) {
+ WARNING(0),
+ ERROR(1),
+ NONE(2),
+ ;
+
+ companion object {
+ fun ofRaw(raw: Int): NotificationIcon? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+enum class NotificationEventType(val raw: Int) {
+ MARKASREAD(0),
+ REPLY(1),
+ OPEN(2),
+ ;
+
+ companion object {
+ fun ofRaw(raw: Int): NotificationEventType? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+enum class NotificationChannelImportance(val raw: Int) {
+ MIN(0),
+ HIGH(1),
+ DEFAULT(2),
+ ;
+
+ companion object {
+ fun ofRaw(raw: Int): NotificationChannelImportance? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationMessageContent(
+ /** The textual body of the message. */
+ val body: String? = null,
+ /** The path and mime type of the media to show. */
+ val mime: String? = null,
+ val path: String? = null,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationMessageContent {
+ val body = list[0] as String?
+ val mime = list[1] as String?
+ val path = list[2] as String?
+ return NotificationMessageContent(body, mime, path)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ body,
+ mime,
+ path,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationMessage(
+ /** The grouping key for the notification. */
+ val groupId: String? = null,
+ /** The sender of the message. */
+ val sender: String? = null,
+ /** The jid of the sender. */
+ val jid: String? = null,
+ /** The body of the message. */
+ val content: NotificationMessageContent,
+ /** Milliseconds since epoch. */
+ val timestamp: Long,
+ /** The path to the avatar to use */
+ val avatarPath: String? = null,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationMessage {
+ val groupId = list[0] as String?
+ val sender = list[1] as String?
+ val jid = list[2] as String?
+ val content = NotificationMessageContent.fromList(list[3] as List)
+ val timestamp = list[4].let { if (it is Int) it.toLong() else it as Long }
+ val avatarPath = list[5] as String?
+ return NotificationMessage(groupId, sender, jid, content, timestamp, avatarPath)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ groupId,
+ sender,
+ jid,
+ content.toList(),
+ timestamp,
+ avatarPath,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class MessagingNotification(
+ /** The title of the conversation. */
+ val title: String,
+ /** The id of the notification. */
+ val id: Long,
+ /** The id of the notification channel the notification should appear on. */
+ val channelId: String,
+ /** The JID of the chat in which the notifications happen. */
+ val jid: String,
+ /** Messages to show. */
+ val messages: List,
+ /** Flag indicating whether this notification is from a groupchat or not. */
+ val isGroupchat: Boolean,
+ /** The id for notification grouping. */
+ val groupId: String? = null,
+ /** Additional data to include. */
+ val extra: Map? = null,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): MessagingNotification {
+ val title = list[0] as String
+ val id = list[1].let { if (it is Int) it.toLong() else it as Long }
+ val channelId = list[2] as String
+ val jid = list[3] as String
+ val messages = list[4] as List
+ val isGroupchat = list[5] as Boolean
+ val groupId = list[6] as String?
+ val extra = list[7] as Map?
+ return MessagingNotification(title, id, channelId, jid, messages, isGroupchat, groupId, extra)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ title,
+ id,
+ channelId,
+ jid,
+ messages,
+ isGroupchat,
+ groupId,
+ extra,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class RegularNotification(
+ /** The title of the notification. */
+ val title: String,
+ /** The body of the notification. */
+ val body: String,
+ /** The id of the channel to show the notification on. */
+ val channelId: String,
+ /** The id for notification grouping. */
+ val groupId: String? = null,
+ /** The id of the notification. */
+ val id: Long,
+ /** The icon to use. */
+ val icon: NotificationIcon,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): RegularNotification {
+ val title = list[0] as String
+ val body = list[1] as String
+ val channelId = list[2] as String
+ val groupId = list[3] as String?
+ val id = list[4].let { if (it is Int) it.toLong() else it as Long }
+ val icon = NotificationIcon.ofRaw(list[5] as Int)!!
+ return RegularNotification(title, body, channelId, groupId, id, icon)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ title,
+ body,
+ channelId,
+ groupId,
+ id,
+ icon.raw,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationEvent(
+ /** The notification id. */
+ val id: Long,
+ /** The JID the notification was for. */
+ val jid: String,
+ /** The type of event. */
+ val type: NotificationEventType,
+ /**
+ * An optional payload.
+ * - type == NotificationType.reply: The reply message text.
+ * Otherwise: undefined.
+ */
+ val payload: String? = null,
+ /** Extra data. Only set when type == NotificationType.reply. */
+ val extra: Map? = null,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationEvent {
+ val id = list[0].let { if (it is Int) it.toLong() else it as Long }
+ val jid = list[1] as String
+ val type = NotificationEventType.ofRaw(list[2] as Int)!!
+ val payload = list[3] as String?
+ val extra = list[4] as Map?
+ return NotificationEvent(id, jid, type, payload, extra)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ id,
+ jid,
+ type.raw,
+ payload,
+ extra,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationI18nData(
+ /** The content of the reply button. */
+ val reply: String,
+ /** The content of the "mark as read" button. */
+ val markAsRead: String,
+ /** The text to show when *you* reply. */
+ val you: String,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationI18nData {
+ val reply = list[0] as String
+ val markAsRead = list[1] as String
+ val you = list[2] as String
+ return NotificationI18nData(reply, markAsRead, you)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ reply,
+ markAsRead,
+ you,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationGroup(
+ val id: String,
+ val description: String,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationGroup {
+ val id = list[0] as String
+ val description = list[1] as String
+ return NotificationGroup(id, description)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ id,
+ description,
+ )
+ }
+}
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class NotificationChannel(
+ val title: String,
+ val description: String,
+ val id: String,
+ val importance: NotificationChannelImportance,
+ val showBadge: Boolean,
+ val groupId: String? = null,
+ val vibration: Boolean,
+ val enableLights: Boolean,
+
+) {
+ companion object {
+ @Suppress("UNCHECKED_CAST")
+ fun fromList(list: List): NotificationChannel {
+ val title = list[0] as String
+ val description = list[1] as String
+ val id = list[2] as String
+ val importance = NotificationChannelImportance.ofRaw(list[3] as Int)!!
+ val showBadge = list[4] as Boolean
+ val groupId = list[5] as String?
+ val vibration = list[6] as Boolean
+ val enableLights = list[7] as Boolean
+ return NotificationChannel(title, description, id, importance, showBadge, groupId, vibration, enableLights)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ title,
+ description,
+ id,
+ importance.raw,
+ showBadge,
+ groupId,
+ vibration,
+ enableLights,
+ )
+ }
+}
+
+@Suppress("UNCHECKED_CAST")
+private object MoxxyNotificationsApiCodec : StandardMessageCodec() {
+ override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
+ return when (type) {
+ 128.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ MessagingNotification.fromList(it)
+ }
+ }
+ 129.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationChannel.fromList(it)
+ }
+ }
+ 130.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationEvent.fromList(it)
+ }
+ }
+ 131.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationGroup.fromList(it)
+ }
+ }
+ 132.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationI18nData.fromList(it)
+ }
+ }
+ 133.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationMessage.fromList(it)
+ }
+ }
+ 134.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ NotificationMessageContent.fromList(it)
+ }
+ }
+ 135.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ RegularNotification.fromList(it)
+ }
+ }
+ else -> super.readValueOfType(type, buffer)
+ }
+ }
+ override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
+ when (value) {
+ is MessagingNotification -> {
+ stream.write(128)
+ writeValue(stream, value.toList())
+ }
+ is NotificationChannel -> {
+ stream.write(129)
+ writeValue(stream, value.toList())
+ }
+ is NotificationEvent -> {
+ stream.write(130)
+ writeValue(stream, value.toList())
+ }
+ is NotificationGroup -> {
+ stream.write(131)
+ writeValue(stream, value.toList())
+ }
+ is NotificationI18nData -> {
+ stream.write(132)
+ writeValue(stream, value.toList())
+ }
+ is NotificationMessage -> {
+ stream.write(133)
+ writeValue(stream, value.toList())
+ }
+ is NotificationMessageContent -> {
+ stream.write(134)
+ writeValue(stream, value.toList())
+ }
+ is RegularNotification -> {
+ stream.write(135)
+ writeValue(stream, value.toList())
+ }
+ else -> super.writeValue(stream, value)
+ }
+ }
+}
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface MoxxyNotificationsApi {
+ /** Notification APIs */
+ fun createNotificationGroups(groups: List)
+ fun deleteNotificationGroups(ids: List)
+ fun createNotificationChannels(channels: List)
+ fun deleteNotificationChannels(ids: List)
+ fun showMessagingNotification(notification: MessagingNotification)
+ fun showNotification(notification: RegularNotification)
+ fun dismissNotification(id: Long)
+ fun setNotificationSelfAvatar(path: String)
+ fun setNotificationI18n(data: NotificationI18nData)
+ fun notificationStub(event: NotificationEvent)
+
+ companion object {
+ /** The codec used by MoxxyNotificationsApi. */
+ val codec: MessageCodec by lazy {
+ MoxxyNotificationsApiCodec
+ }
+
+ /** Sets up an instance of `MoxxyNotificationsApi` to handle messages through the `binaryMessenger`. */
+ @Suppress("UNCHECKED_CAST")
+ fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyNotificationsApi?) {
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationGroups", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val groupsArg = args[0] as List
+ var wrapped: List
+ try {
+ api.createNotificationGroups(groupsArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.deleteNotificationGroups", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val idsArg = args[0] as List
+ var wrapped: List
+ try {
+ api.deleteNotificationGroups(idsArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationChannels", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val channelsArg = args[0] as List
+ var wrapped: List
+ try {
+ api.createNotificationChannels(channelsArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.deleteNotificationChannels", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val idsArg = args[0] as List
+ var wrapped: List
+ try {
+ api.deleteNotificationChannels(idsArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.showMessagingNotification", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val notificationArg = args[0] as MessagingNotification
+ var wrapped: List
+ try {
+ api.showMessagingNotification(notificationArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.showNotification", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val notificationArg = args[0] as RegularNotification
+ var wrapped: List
+ try {
+ api.showNotification(notificationArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.dismissNotification", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val idArg = args[0].let { if (it is Int) it.toLong() else it as Long }
+ var wrapped: List
+ try {
+ api.dismissNotification(idArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.setNotificationSelfAvatar", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val pathArg = args[0] as String
+ var wrapped: List
+ try {
+ api.setNotificationSelfAvatar(pathArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.setNotificationI18n", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val dataArg = args[0] as NotificationI18nData
+ var wrapped: List
+ try {
+ api.setNotificationI18n(dataArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.notificationStub", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val eventArg = args[0] as NotificationEvent
+ var wrapped: List
+ try {
+ api.notificationStub(eventArg)
+ wrapped = listOf(null)
+ } catch (exception: Throwable) {
+ wrapped = wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt
new file mode 100644
index 0000000..6ef0e5f
--- /dev/null
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt
@@ -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.picker
+
+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 {
+ return listOf(result)
+}
+
+private fun wrapError(exception: Throwable): List {
+ 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 FilePickerType(val raw: Int) {
+ /** Pick only image(s) */
+ IMAGE(0),
+
+ /** Pick only video(s) */
+ VIDEO(1),
+
+ /** Pick image(s) and video(s) */
+ IMAGEANDVIDEO(2),
+
+ /** Pick any kind of file(s) */
+ GENERIC(3),
+ ;
+
+ companion object {
+ fun ofRaw(raw: Int): FilePickerType? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface MoxxyPickerApi {
+ /**
+ * Open either the photo picker or the generic file picker to get a list of paths that were
+ * selected and are accessable. If the list is empty, then the user dismissed the picker without
+ * selecting anything.
+ *
+ * [type] specifies what kind of file(s) should be picked.
+ *
+ * [multiple] controls whether multiple files can be picked (true) or just a single file
+ * is enough (false).
+ */
+ fun pickFiles(type: FilePickerType, multiple: Boolean, callback: (Result>) -> Unit)
+
+ /** Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. */
+ fun pickFileWithData(type: FilePickerType, callback: (Result) -> Unit)
+
+ companion object {
+ /** The codec used by MoxxyPickerApi. */
+ val codec: MessageCodec by lazy {
+ StandardMessageCodec()
+ }
+
+ /** Sets up an instance of `MoxxyPickerApi` to handle messages through the `binaryMessenger`. */
+ @Suppress("UNCHECKED_CAST")
+ fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyPickerApi?) {
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
+ val multipleArg = args[1] as Boolean
+ api.pickFiles(typeArg, multipleArg) { result: Result> ->
+ 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(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFileWithData", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
+ api.pickFileWithData(typeArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerResultListener.kt b/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerResultListener.kt
index 1e714f8..25d2358 100644
--- a/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerResultListener.kt
+++ b/android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerResultListener.kt
@@ -174,4 +174,4 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
result!!(Result.success(pickedFiles))
return true
}
-}
\ No newline at end of file
+}
diff --git a/android/src/main/res/drawable-hdpi/ic_service.png b/android/src/main/res/drawable-hdpi/ic_service.png
new file mode 100644
index 0000000..e678754
Binary files /dev/null and b/android/src/main/res/drawable-hdpi/ic_service.png differ
diff --git a/android/src/main/res/drawable-mdpi/ic_service.png b/android/src/main/res/drawable-mdpi/ic_service.png
new file mode 100644
index 0000000..fd69fab
Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_service.png differ
diff --git a/android/src/main/res/drawable-xhdpi/ic_service.png b/android/src/main/res/drawable-xhdpi/ic_service.png
new file mode 100644
index 0000000..f7e77cd
Binary files /dev/null and b/android/src/main/res/drawable-xhdpi/ic_service.png differ
diff --git a/android/src/main/res/drawable-xxhdpi/ic_service.png b/android/src/main/res/drawable-xxhdpi/ic_service.png
new file mode 100644
index 0000000..92db591
Binary files /dev/null and b/android/src/main/res/drawable-xxhdpi/ic_service.png differ
diff --git a/android/src/main/res/drawable-xxxhdpi/ic_service.png b/android/src/main/res/drawable-xxxhdpi/ic_service.png
new file mode 100644
index 0000000..ce7c110
Binary files /dev/null and b/android/src/main/res/drawable-xxxhdpi/ic_service.png differ
diff --git a/android/src/main/res/drawable/error.xml b/android/src/main/res/drawable/error.xml
new file mode 100644
index 0000000..1757571
--- /dev/null
+++ b/android/src/main/res/drawable/error.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/android/src/main/res/drawable/mark_as_read.xml b/android/src/main/res/drawable/mark_as_read.xml
new file mode 100644
index 0000000..5b80636
--- /dev/null
+++ b/android/src/main/res/drawable/mark_as_read.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/android/src/main/res/drawable/reply.xml b/android/src/main/res/drawable/reply.xml
new file mode 100644
index 0000000..fb8b6df
--- /dev/null
+++ b/android/src/main/res/drawable/reply.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/android/src/main/res/drawable/warning.xml b/android/src/main/res/drawable/warning.xml
new file mode 100644
index 0000000..3c9a4b3
--- /dev/null
+++ b/android/src/main/res/drawable/warning.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/android/src/main/res/xml/file_paths.xml b/android/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..ab7ff51
--- /dev/null
+++ b/android/src/main/res/xml/file_paths.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/org/moxxy/moxxy_native_example/MainActivity.kt b/example/android/app/src/main/kotlin/org/moxxy/moxxy_native_example/MainActivity.kt
index bb43767..6ac1445 100644
--- a/example/android/app/src/main/kotlin/org/moxxy/moxxy_native_example/MainActivity.kt
+++ b/example/android/app/src/main/kotlin/org/moxxy/moxxy_native_example/MainActivity.kt
@@ -1,6 +1,20 @@
package org.moxxy.moxxy_native_example
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ Log.d("moxxy_native", "onCreate intent ${intent?.action}")
+ }
+
+ override fun onNewIntent(intent: Intent) {
+ super.onNewIntent(intent)
+
+ Log.d("moxxy_native", "New intent ${intent.action}")
+ }
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 5735ad9..715e251 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,5 +1,3 @@
-import 'dart:developer';
-
import 'package:flutter/material.dart';
import 'package:moxxy_native/moxxy_native.dart';
@@ -21,7 +19,8 @@ class MyApp extends StatelessWidget {
children: [
TextButton(
onPressed: () async {
- final result = await MoxxyPickerApi().pickFiles(FilePickerType.image, false);
+ final result = await MoxxyPickerApi()
+ .pickFiles(FilePickerType.image, false);
// ignore: avoid_print
print('User picked: $result');
},
@@ -29,7 +28,8 @@ class MyApp extends StatelessWidget {
),
TextButton(
onPressed: () async {
- final result = await MoxxyPickerApi().pickFiles(FilePickerType.imageAndVideo, true);
+ final result = await MoxxyPickerApi()
+ .pickFiles(FilePickerType.imageAndVideo, true);
// ignore: avoid_print
print('User picked: $result');
},
@@ -37,7 +37,8 @@ class MyApp extends StatelessWidget {
),
TextButton(
onPressed: () async {
- final result = await MoxxyPickerApi().pickFiles(FilePickerType.generic, true);
+ final result = await MoxxyPickerApi()
+ .pickFiles(FilePickerType.generic, true);
// ignore: avoid_print
print('User picked: $result');
},
diff --git a/example/pubspec.lock b/example/pubspec.lock
index 14fd041..9d7235d 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -130,6 +130,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.2"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.4.5"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.3.6"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.1.4"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: f2343e9fa9c22ae4fd92d4732755bfe452214e7189afcc097380950cf567b4b2
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.11.5"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.3"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.6"
sky_engine:
dependency: transitive
description: flutter
@@ -193,4 +241,4 @@ packages:
version: "2.1.4"
sdks:
dart: ">=2.19.6 <3.0.0"
- flutter: ">=2.5.0"
+ flutter: ">=2.8.0"
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
deleted file mode 100644
index 71fe1db..0000000
--- a/example/test/widget_test.dart
+++ /dev/null
@@ -1,27 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:moxxy_native_example/main.dart';
-
-void main() {
- testWidgets('Verify Platform version', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
-
- // Verify that platform version is retrieved.
- expect(
- find.byWidgetPredicate(
- (Widget widget) => widget is Text &&
- widget.data!.startsWith('Running on:'),
- ),
- findsOneWidget,
- );
- });
-}
diff --git a/lib/moxxy_native.dart b/lib/moxxy_native.dart
index ad85162..f49b8b5 100644
--- a/lib/moxxy_native.dart
+++ b/lib/moxxy_native.dart
@@ -1 +1,2 @@
-export 'pigeon/picker.dart';
+export 'pigeon/notifications.g.dart';
+export 'pigeon/picker.g.dart';
diff --git a/lib/pigeon/notifications.g.dart b/lib/pigeon/notifications.g.dart
new file mode 100644
index 0000000..1d23271
--- /dev/null
+++ b/lib/pigeon/notifications.g.dart
@@ -0,0 +1,695 @@
+// 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 NotificationIcon {
+ warning,
+ error,
+ none,
+}
+
+enum NotificationEventType {
+ markAsRead,
+ reply,
+ open,
+}
+
+enum NotificationChannelImportance {
+ MIN,
+ HIGH,
+ DEFAULT,
+}
+
+class NotificationMessageContent {
+ NotificationMessageContent({
+ this.body,
+ this.mime,
+ this.path,
+ });
+
+ /// The textual body of the message.
+ String? body;
+
+ /// The path and mime type of the media to show.
+ String? mime;
+
+ String? path;
+
+ Object encode() {
+ return