feat: Move the notification code back into moxxy_native

This commit is contained in:
PapaTutuWawa 2023-09-08 15:41:06 +02:00
parent d8a4394f17
commit fd91ccc46f
30 changed files with 2629 additions and 269 deletions

View File

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

View File

@ -1,3 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.moxxy.moxxy_native"> package="org.moxxy.moxxy_native">
<application>
<provider
android:name="org.moxxy.moxxy_native.content.MoxxyFileProvider"
android:authorities="org.moxxy.moxxyv2.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<receiver android:name="org.moxxy.moxxy_native.notifications.NotificationReceiver" />
</application>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest> </manifest>

View File

@ -2,6 +2,34 @@ package org.moxxy.moxxy_native
const val TAG = "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 // Request codes
const val PICK_FILE_REQUEST = 42 const val PICK_FILE_REQUEST = 42
const val PICK_FILES_REQUEST = 43 const val PICK_FILES_REQUEST = 43

View File

@ -1,110 +1,200 @@
package org.moxxy.moxxy_native package org.moxxy.moxxy_native
import android.app.Activity import android.app.Activity
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.NonNull import androidx.annotation.NonNull
import androidx.core.app.NotificationManagerCompat
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import org.moxxy.moxxy_native.generated.FilePickerType import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.generated.MoxxyPickerApi 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 import org.moxxy.moxxy_native.picker.PickerResultListener
class MoxxyNativePlugin: FlutterPlugin, ActivityAware, MoxxyPickerApi { object MoxxyEventChannels {
private var context: Context? = null var notificationChannel: EventChannel? = null
private var activity: Activity? = null var notificationEventSink: EventChannel.EventSink? = null
private lateinit var pickerListener: PickerResultListener }
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { object NotificationStreamHandler : EventChannel.StreamHandler {
context = flutterPluginBinding.applicationContext override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this) Log.d(TAG, "NotificationStreamHandler: Attached stream")
pickerListener = PickerResultListener(context!!) MoxxyEventChannels.notificationEventSink = events
Log.d(TAG, "Attached to engine") }
}
override fun onCancel(arguments: Any?) {
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { Log.d(TAG, "NotificationStreamHandler: Detached stream")
Log.d(TAG, "Detached from engine") MoxxyEventChannels.notificationEventSink = null
} }
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity /*
binding.addActivityResultListener(pickerListener) * Hold the last notification event in case we did a cold start.
Log.d(TAG, "Attached to activity") */
} object NotificationCache {
var lastEvent: NotificationEvent? = null
override fun onDetachedFromActivityForConfigChanges() { }
activity = null
Log.d(TAG, "Detached from activity") class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNotificationsApi {
} private var context: Context? = null
private var activity: Activity? = null
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { private lateinit var activityClass: Class<Any>
activity = binding.activity private lateinit var pickerListener: PickerResultListener
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
override fun onDetachedFromActivity() { context = flutterPluginBinding.applicationContext
activity = null MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
Log.d(TAG, "Detached from activity") MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, this)
} pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine")
override fun pickFiles( }
type: FilePickerType,
multiple: Boolean, override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
callback: (Result<List<String>>) -> Unit Log.d(TAG, "Detached from engine")
) { }
val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
AsyncRequestTracker.requestTracker[requestCode] = callback as (Result<Any>) -> Unit override fun onAttachedToActivity(binding: ActivityPluginBinding) {
if (type == FilePickerType.GENERIC) { activity = binding.activity
val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { activityClass = activity!!.javaClass
addCategory(Intent.CATEGORY_OPENABLE) binding.addActivityResultListener(pickerListener)
this.type = "*/*" Log.d(TAG, "Attached to activity")
}
// Allow/disallow picking multiple files
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple) override fun onDetachedFromActivityForConfigChanges() {
} activity = null
activity?.startActivityForResult(pickIntent, requestCode) Log.d(TAG, "Detached from activity")
return }
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
val contract = when (multiple) { activity = binding.activity
false -> ActivityResultContracts.PickVisualMedia() }
true -> ActivityResultContracts.PickMultipleVisualMedia()
} override fun onDetachedFromActivity() {
val pickType = when (type) { activity = null
// We keep FilePickerType.GENERIC here, even though we know that @type will never be Log.d(TAG, "Detached from activity")
// GENERIC to make Kotlin happy. }
FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly override fun pickFiles(
FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo type: FilePickerType,
} multiple: Boolean,
val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType)) callback: (Result<List<String>>) -> Unit,
activity?.startActivityForResult(pickIntent, requestCode) ) {
} val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
AsyncRequestTracker.requestTracker[requestCode] = callback as (Result<Any>) -> Unit
override fun pickFileWithData(type: FilePickerType, callback: (Result<ByteArray?>) -> Unit) { if (type == FilePickerType.GENERIC) {
AsyncRequestTracker.requestTracker[PICK_FILE_WITH_DATA_REQUEST] = callback as (Result<Any>) -> Unit val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
if (type == FilePickerType.GENERIC) { addCategory(Intent.CATEGORY_OPENABLE)
val pickIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { this.type = "*/*"
addCategory(Intent.CATEGORY_OPENABLE)
this.type = "*/*" // Allow/disallow picking multiple files
} putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST) }
return activity?.startActivityForResult(pickIntent, requestCode)
} return
}
val pickType = when (type) {
// We keep FilePickerType.GENERIC here, even though we know that @type will never be val contract = when (multiple) {
// GENERIC to make Kotlin happy. false -> ActivityResultContracts.PickVisualMedia()
FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly true -> ActivityResultContracts.PickMultipleVisualMedia()
FilePickerType.VIDEO -> ActivityResultContracts.PickVisualMedia.VideoOnly }
FilePickerType.IMAGEANDVIDEO -> ActivityResultContracts.PickVisualMedia.ImageAndVideo val pickType = when (type) {
} // We keep FilePickerType.GENERIC here, even though we know that @type will never be
val contract = ActivityResultContracts.PickVisualMedia() // GENERIC to make Kotlin happy.
val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType)) FilePickerType.GENERIC, FilePickerType.IMAGE -> ActivityResultContracts.PickVisualMedia.ImageOnly
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST) 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<ByteArray?>) -> Unit) {
AsyncRequestTracker.requestTracker[PICK_FILE_WITH_DATA_REQUEST] = callback as (Result<Any>) -> 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<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 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,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)

View File

@ -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<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 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<List<String>>) -> Unit)
/** Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. */
fun pickFileWithData(type: FilePickerType, callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by MoxxyPickerApi. */
val codec: MessageCodec<Any?> 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<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
val multipleArg = args[1] as Boolean
api.pickFiles(typeArg, multipleArg) { result: Result<List<String>> ->
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.MoxxyPickerApi.pickFileWithData", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
api.pickFileWithData(typeArg) { 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,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<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.
* */
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)
}
}
}

View File

@ -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<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,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<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 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<Any?>): 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<Any?> {
return listOf<Any?>(
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<Any?>): 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<Any?>)
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<Any?> {
return listOf<Any?>(
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<NotificationMessage?>,
/** 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<String?, String?>? = null,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): 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<NotificationMessage?>
val isGroupchat = list[5] as Boolean
val groupId = list[6] as String?
val extra = list[7] as Map<String?, String?>?
return MessagingNotification(title, id, channelId, jid, messages, isGroupchat, groupId, extra)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
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<Any?>): 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<Any?> {
return listOf<Any?>(
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<String?, String?>? = null,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): 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<String?, String?>?
return NotificationEvent(id, jid, type, payload, extra)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
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<Any?>): 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<Any?> {
return listOf<Any?>(
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<Any?>): NotificationGroup {
val id = list[0] as String
val description = list[1] as String
return NotificationGroup(id, description)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
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<Any?>): 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<Any?> {
return listOf<Any?>(
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<Any?>)?.let {
MessagingNotification.fromList(it)
}
}
129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationChannel.fromList(it)
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationEvent.fromList(it)
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationGroup.fromList(it)
}
}
132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationI18nData.fromList(it)
}
}
133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationMessage.fromList(it)
}
}
134.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
NotificationMessageContent.fromList(it)
}
}
135.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.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<NotificationGroup>)
fun deleteNotificationGroups(ids: List<String>)
fun createNotificationChannels(channels: List<NotificationChannel>)
fun deleteNotificationChannels(ids: List<String>)
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<Any?> 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<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationGroups", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val groupsArg = args[0] as List<NotificationGroup>
var wrapped: List<Any?>
try {
api.createNotificationGroups(groupsArg)
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.MoxxyNotificationsApi.deleteNotificationGroups", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idsArg = args[0] as List<String>
var wrapped: List<Any?>
try {
api.deleteNotificationGroups(idsArg)
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.MoxxyNotificationsApi.createNotificationChannels", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val channelsArg = args[0] as List<NotificationChannel>
var wrapped: List<Any?>
try {
api.createNotificationChannels(channelsArg)
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.MoxxyNotificationsApi.deleteNotificationChannels", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idsArg = args[0] as List<String>
var wrapped: List<Any?>
try {
api.deleteNotificationChannels(idsArg)
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.MoxxyNotificationsApi.showMessagingNotification", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val notificationArg = args[0] as MessagingNotification
var wrapped: List<Any?>
try {
api.showMessagingNotification(notificationArg)
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.MoxxyNotificationsApi.showNotification", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val notificationArg = args[0] as RegularNotification
var wrapped: List<Any?>
try {
api.showNotification(notificationArg)
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.MoxxyNotificationsApi.dismissNotification", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val idArg = args[0].let { if (it is Int) it.toLong() else it as Long }
var wrapped: List<Any?>
try {
api.dismissNotification(idArg)
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.MoxxyNotificationsApi.setNotificationSelfAvatar", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val pathArg = args[0] as String
var wrapped: List<Any?>
try {
api.setNotificationSelfAvatar(pathArg)
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.MoxxyNotificationsApi.setNotificationI18n", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val dataArg = args[0] as NotificationI18nData
var wrapped: List<Any?>
try {
api.setNotificationI18n(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.MoxxyNotificationsApi.notificationStub", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val eventArg = args[0] as NotificationEvent
var wrapped: List<Any?>
try {
api.notificationStub(eventArg)
wrapped = listOf<Any?>(null)
} catch (exception: Throwable) {
wrapped = wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

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.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<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 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<List<String>>) -> Unit)
/** Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. */
fun pickFileWithData(type: FilePickerType, callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by MoxxyPickerApi. */
val codec: MessageCodec<Any?> 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<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
val multipleArg = args[1] as Boolean
api.pickFiles(typeArg, multipleArg) { result: Result<List<String>> ->
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.MoxxyPickerApi.pickFileWithData", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val typeArg = FilePickerType.ofRaw(args[0] as Int)!!
api.pickFileWithData(typeArg) { 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)
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17.34,20l-3.54,-3.54l1.41,-1.41l2.12,2.12l4.24,-4.24L23,14.34L17.34,20zM12,17c0,-3.87 3.13,-7 7,-7c1.08,0 2.09,0.25 3,0.68V4c0,-1.1 -0.9,-2 -2,-2H4C2.9,2 2,2.9 2,4v18l4,-4h6v0c0,-0.17 0.01,-0.33 0.03,-0.5C12.01,17.34 12,17.17 12,17z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>

View File

@ -0,0 +1,7 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Media files -->
<files-path name="media" path="media/" />
<!-- Media thumbnails -->
<cache-path name="thumbnails" path="thumbnails/" />
</paths>

View File

@ -1,6 +1,20 @@
package org.moxxy.moxxy_native_example package org.moxxy.moxxy_native_example
import android.content.Intent
import android.os.Bundle
import android.util.Log
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
class MainActivity: 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}")
}
} }

View File

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:moxxy_native/moxxy_native.dart'; import 'package:moxxy_native/moxxy_native.dart';
@ -21,7 +19,8 @@ class MyApp extends StatelessWidget {
children: [ children: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi().pickFiles(FilePickerType.image, false); final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.image, false);
// ignore: avoid_print // ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },
@ -29,7 +28,8 @@ class MyApp extends StatelessWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi().pickFiles(FilePickerType.imageAndVideo, true); final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.imageAndVideo, true);
// ignore: avoid_print // ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },
@ -37,7 +37,8 @@ class MyApp extends StatelessWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final result = await MoxxyPickerApi().pickFiles(FilePickerType.generic, true); final result = await MoxxyPickerApi()
.pickFiles(FilePickerType.generic, true);
// ignore: avoid_print // ignore: avoid_print
print('User picked: $result'); print('User picked: $result');
}, },

View File

@ -130,6 +130,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.8.2" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -193,4 +241,4 @@ packages:
version: "2.1.4" version: "2.1.4"
sdks: sdks:
dart: ">=2.19.6 <3.0.0" dart: ">=2.19.6 <3.0.0"
flutter: ">=2.5.0" flutter: ">=2.8.0"

View File

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

View File

@ -1 +1,2 @@
export 'pigeon/picker.dart'; export 'pigeon/notifications.g.dart';
export 'pigeon/picker.g.dart';

View File

@ -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 <Object?>[
body,
mime,
path,
];
}
static NotificationMessageContent decode(Object result) {
result as List<Object?>;
return NotificationMessageContent(
body: result[0] as String?,
mime: result[1] as String?,
path: result[2] as String?,
);
}
}
class NotificationMessage {
NotificationMessage({
this.groupId,
this.sender,
this.jid,
required this.content,
required this.timestamp,
this.avatarPath,
});
/// The grouping key for the notification.
String? groupId;
/// The sender of the message.
String? sender;
/// The jid of the sender.
String? jid;
/// The body of the message.
NotificationMessageContent content;
/// Milliseconds since epoch.
int timestamp;
/// The path to the avatar to use
String? avatarPath;
Object encode() {
return <Object?>[
groupId,
sender,
jid,
content.encode(),
timestamp,
avatarPath,
];
}
static NotificationMessage decode(Object result) {
result as List<Object?>;
return NotificationMessage(
groupId: result[0] as String?,
sender: result[1] as String?,
jid: result[2] as String?,
content: NotificationMessageContent.decode(result[3]! as List<Object?>),
timestamp: result[4]! as int,
avatarPath: result[5] as String?,
);
}
}
class MessagingNotification {
MessagingNotification({
required this.title,
required this.id,
required this.channelId,
required this.jid,
required this.messages,
required this.isGroupchat,
this.groupId,
this.extra,
});
/// The title of the conversation.
String title;
/// The id of the notification.
int id;
/// The id of the notification channel the notification should appear on.
String channelId;
/// The JID of the chat in which the notifications happen.
String jid;
/// Messages to show.
List<NotificationMessage?> messages;
/// Flag indicating whether this notification is from a groupchat or not.
bool isGroupchat;
/// The id for notification grouping.
String? groupId;
/// Additional data to include.
Map<String?, String?>? extra;
Object encode() {
return <Object?>[
title,
id,
channelId,
jid,
messages,
isGroupchat,
groupId,
extra,
];
}
static MessagingNotification decode(Object result) {
result as List<Object?>;
return MessagingNotification(
title: result[0]! as String,
id: result[1]! as int,
channelId: result[2]! as String,
jid: result[3]! as String,
messages: (result[4] as List<Object?>?)!.cast<NotificationMessage?>(),
isGroupchat: result[5]! as bool,
groupId: result[6] as String?,
extra: (result[7] as Map<Object?, Object?>?)?.cast<String?, String?>(),
);
}
}
class RegularNotification {
RegularNotification({
required this.title,
required this.body,
required this.channelId,
this.groupId,
required this.id,
required this.icon,
});
/// The title of the notification.
String title;
/// The body of the notification.
String body;
/// The id of the channel to show the notification on.
String channelId;
/// The id for notification grouping.
String? groupId;
/// The id of the notification.
int id;
/// The icon to use.
NotificationIcon icon;
Object encode() {
return <Object?>[
title,
body,
channelId,
groupId,
id,
icon.index,
];
}
static RegularNotification decode(Object result) {
result as List<Object?>;
return RegularNotification(
title: result[0]! as String,
body: result[1]! as String,
channelId: result[2]! as String,
groupId: result[3] as String?,
id: result[4]! as int,
icon: NotificationIcon.values[result[5]! as int],
);
}
}
class NotificationEvent {
NotificationEvent({
required this.id,
required this.jid,
required this.type,
this.payload,
this.extra,
});
/// The notification id.
int id;
/// The JID the notification was for.
String jid;
/// The type of event.
NotificationEventType type;
/// An optional payload.
/// - type == NotificationType.reply: The reply message text.
/// Otherwise: undefined.
String? payload;
/// Extra data. Only set when type == NotificationType.reply.
Map<String?, String?>? extra;
Object encode() {
return <Object?>[
id,
jid,
type.index,
payload,
extra,
];
}
static NotificationEvent decode(Object result) {
result as List<Object?>;
return NotificationEvent(
id: result[0]! as int,
jid: result[1]! as String,
type: NotificationEventType.values[result[2]! as int],
payload: result[3] as String?,
extra: (result[4] as Map<Object?, Object?>?)?.cast<String?, String?>(),
);
}
}
class NotificationI18nData {
NotificationI18nData({
required this.reply,
required this.markAsRead,
required this.you,
});
/// The content of the reply button.
String reply;
/// The content of the "mark as read" button.
String markAsRead;
/// The text to show when *you* reply.
String you;
Object encode() {
return <Object?>[
reply,
markAsRead,
you,
];
}
static NotificationI18nData decode(Object result) {
result as List<Object?>;
return NotificationI18nData(
reply: result[0]! as String,
markAsRead: result[1]! as String,
you: result[2]! as String,
);
}
}
class NotificationGroup {
NotificationGroup({
required this.id,
required this.description,
});
String id;
String description;
Object encode() {
return <Object?>[
id,
description,
];
}
static NotificationGroup decode(Object result) {
result as List<Object?>;
return NotificationGroup(
id: result[0]! as String,
description: result[1]! as String,
);
}
}
class NotificationChannel {
NotificationChannel({
required this.title,
required this.description,
required this.id,
required this.importance,
required this.showBadge,
this.groupId,
required this.vibration,
required this.enableLights,
});
String title;
String description;
String id;
NotificationChannelImportance importance;
bool showBadge;
String? groupId;
bool vibration;
bool enableLights;
Object encode() {
return <Object?>[
title,
description,
id,
importance.index,
showBadge,
groupId,
vibration,
enableLights,
];
}
static NotificationChannel decode(Object result) {
result as List<Object?>;
return NotificationChannel(
title: result[0]! as String,
description: result[1]! as String,
id: result[2]! as String,
importance: NotificationChannelImportance.values[result[3]! as int],
showBadge: result[4]! as bool,
groupId: result[5] as String?,
vibration: result[6]! as bool,
enableLights: result[7]! as bool,
);
}
}
class _MoxxyNotificationsApiCodec extends StandardMessageCodec {
const _MoxxyNotificationsApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is MessagingNotification) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else if (value is NotificationChannel) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is NotificationEvent) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is NotificationGroup) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is NotificationI18nData) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is NotificationMessage) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is NotificationMessageContent) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else if (value is RegularNotification) {
buffer.putUint8(135);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return MessagingNotification.decode(readValue(buffer)!);
case 129:
return NotificationChannel.decode(readValue(buffer)!);
case 130:
return NotificationEvent.decode(readValue(buffer)!);
case 131:
return NotificationGroup.decode(readValue(buffer)!);
case 132:
return NotificationI18nData.decode(readValue(buffer)!);
case 133:
return NotificationMessage.decode(readValue(buffer)!);
case 134:
return NotificationMessageContent.decode(readValue(buffer)!);
case 135:
return RegularNotification.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class MoxxyNotificationsApi {
/// Constructor for [MoxxyNotificationsApi]. 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.
MoxxyNotificationsApi({BinaryMessenger? binaryMessenger})
: _binaryMessenger = binaryMessenger;
final BinaryMessenger? _binaryMessenger;
static const MessageCodec<Object?> codec = _MoxxyNotificationsApiCodec();
/// Notification APIs
Future<void> createNotificationGroups(
List<NotificationGroup?> arg_groups) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationGroups',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_groups]) 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> deleteNotificationGroups(List<String?> arg_ids) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.deleteNotificationGroups',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_ids]) 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> createNotificationChannels(
List<NotificationChannel?> arg_channels) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.createNotificationChannels',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_channels]) 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> deleteNotificationChannels(List<String?> arg_ids) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.deleteNotificationChannels',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_ids]) 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> showMessagingNotification(
MessagingNotification arg_notification) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.showMessagingNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_notification]) 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> showNotification(RegularNotification arg_notification) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.showNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_notification]) 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> dismissNotification(int arg_id) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.dismissNotification',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_id]) 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> setNotificationSelfAvatar(String arg_path) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.setNotificationSelfAvatar',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_path]) 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> setNotificationI18n(NotificationI18nData arg_data) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.setNotificationI18n',
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> notificationStub(NotificationEvent arg_event) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyNotificationsApi.notificationStub',
codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_event]) 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

@ -11,10 +11,13 @@ import 'package:flutter/services.dart';
enum FilePickerType { enum FilePickerType {
/// Pick only image(s) /// Pick only image(s)
image, image,
/// Pick only video(s) /// Pick only video(s)
video, video,
/// Pick image(s) and video(s) /// Pick image(s) and video(s)
imageAndVideo, imageAndVideo,
/// Pick any kind of file(s) /// Pick any kind of file(s)
generic, generic,
} }
@ -37,12 +40,13 @@ class MoxxyPickerApi {
/// ///
/// [multiple] controls whether multiple files can be picked (true) or just a single file /// [multiple] controls whether multiple files can be picked (true) or just a single file
/// is enough (false). /// is enough (false).
Future<List<String?>> pickFiles(FilePickerType arg_type, bool arg_multiple) async { Future<List<String?>> pickFiles(
FilePickerType arg_type, bool arg_multiple) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFiles', codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList = await channel
await channel.send(<Object?>[arg_type.index, arg_multiple]) as List<Object?>?; .send(<Object?>[arg_type.index, arg_multiple]) as List<Object?>?;
if (replyList == null) { if (replyList == null) {
throw PlatformException( throw PlatformException(
code: 'channel-error', code: 'channel-error',
@ -67,7 +71,8 @@ class MoxxyPickerApi {
/// Like [pickFiles] but sets multiple to false and returns the raw binary data from the file. /// Like [pickFiles] but sets multiple to false and returns the raw binary data from the file.
Future<Uint8List?> pickFileWithData(FilePickerType arg_type) async { Future<Uint8List?> pickFileWithData(FilePickerType arg_type) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFileWithData', codec, 'dev.flutter.pigeon.moxxy_native.MoxxyPickerApi.pickFileWithData',
codec,
binaryMessenger: _binaryMessenger); binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = final List<Object?>? replyList =
await channel.send(<Object?>[arg_type.index]) as List<Object?>?; await channel.send(<Object?>[arg_type.index]) as List<Object?>?;

206
pigeon/notifications.dart Normal file
View File

@ -0,0 +1,206 @@
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',
kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.notifications',
),
),
)
class NotificationMessageContent {
const NotificationMessageContent(
this.body,
this.mime,
this.path,
);
/// The textual body of the message.
final String? body;
/// The path and mime type of the media to show.
final String? mime;
final String? path;
}
class NotificationMessage {
const NotificationMessage(
this.sender,
this.content,
this.jid,
this.timestamp,
this.avatarPath, {
this.groupId,
});
/// The grouping key for the notification.
final String? groupId;
/// The sender of the message.
final String? sender;
/// The jid of the sender.
final String? jid;
/// The body of the message.
final NotificationMessageContent content;
/// Milliseconds since epoch.
final int timestamp;
/// The path to the avatar to use
final String? avatarPath;
}
class MessagingNotification {
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;
/// The id of the notification.
final int id;
/// The id of the notification channel the notification should appear on.
final String channelId;
/// The JID of the chat in which the notifications happen.
final String jid;
/// Messages to show.
final List<NotificationMessage?> messages;
/// Flag indicating whether this notification is from a groupchat or not.
final bool isGroupchat;
/// The id for notification grouping.
final String? groupId;
/// Additional data to include.
final Map<String?, String?>? extra;
}
enum NotificationIcon {
warning,
error,
none,
}
class RegularNotification {
const RegularNotification(
this.title, this.body, this.channelId, this.id, this.icon,
{this.groupId});
/// The title of the notification.
final String title;
/// The body of the notification.
final String body;
/// The id of the channel to show the notification on.
final String channelId;
/// The id for notification grouping.
final String? groupId;
/// The id of the notification.
final int id;
/// The icon to use.
final NotificationIcon icon;
}
enum NotificationEventType {
markAsRead,
reply,
open,
}
class NotificationEvent {
const NotificationEvent(
this.id,
this.jid,
this.type,
this.payload,
this.extra,
);
/// The notification id.
final int id;
/// The JID the notification was for.
final String jid;
/// The type of event.
final NotificationEventType type;
/// An optional payload.
/// - type == NotificationType.reply: The reply message text.
/// Otherwise: undefined.
final String? payload;
/// Extra data. Only set when type == NotificationType.reply.
final Map<String?, String?>? extra;
}
class NotificationI18nData {
const NotificationI18nData(this.reply, this.markAsRead, this.you);
/// The content of the reply button.
final String reply;
/// The content of the "mark as read" button.
final String markAsRead;
/// The text to show when *you* reply.
final String you;
}
class NotificationGroup {
const NotificationGroup(this.id, this.description);
final String id;
final String description;
}
enum NotificationChannelImportance { MIN, HIGH, DEFAULT }
class NotificationChannel {
const NotificationChannel(
this.id,
this.title,
this.description, {
this.importance = NotificationChannelImportance.DEFAULT,
this.showBadge = true,
this.groupId,
this.vibration = true,
this.enableLights = true,
});
final String title;
final String description;
final String id;
final NotificationChannelImportance importance;
final bool showBadge;
final String? groupId;
final bool vibration;
final bool enableLights;
}
@HostApi()
abstract class MoxxyNotificationsApi {
/// Notification APIs
void createNotificationGroups(List<NotificationGroup> groups);
void deleteNotificationGroups(List<String> ids);
void createNotificationChannels(List<NotificationChannel> channels);
void deleteNotificationChannels(List<String> ids);
void showMessagingNotification(MessagingNotification notification);
void showNotification(RegularNotification notification);
void dismissNotification(int id);
void setNotificationSelfAvatar(String path);
void setNotificationI18n(NotificationI18nData data);
// Stubs for generating event classes
void notificationStub(NotificationEvent event);
}

View File

@ -2,10 +2,10 @@ import 'package:pigeon/pigeon.dart';
@ConfigurePigeon( @ConfigurePigeon(
PigeonOptions( PigeonOptions(
dartOut: 'lib/pigeon/picker.dart', dartOut: 'lib/pigeon/picker.g.dart',
kotlinOut: 'android/src/main/kotlin/org/moxxy/moxxy_native/generated/PickerApi.kt', kotlinOut: 'android/src/main/kotlin/org/moxxy/moxxy_native/picker/PickerApi.kt',
kotlinOptions: KotlinOptions( kotlinOptions: KotlinOptions(
package: 'org.moxxy.moxxy_native.generated', package: 'org.moxxy.moxxy_native.picker',
), ),
), ),
) )