feat: Move the notification code back into moxxy_native

This commit is contained in:
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 {
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"
}

View File

@@ -1,3 +1,19 @@
<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>

View File

@@ -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
const val PICK_FILE_WITH_DATA_REQUEST = 44

View File

@@ -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<List<String>>) -> Unit
) {
val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
AsyncRequestTracker.requestTracker[requestCode] = callback as (Result<Any>) -> 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<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)
}
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<Any>
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<List<String>>) -> Unit,
) {
val requestCode = if (multiple) PICK_FILES_REQUEST else PICK_FILE_REQUEST
AsyncRequestTracker.requestTracker[requestCode] = callback as (Result<Any>) -> 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<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)
}
}
}
}
}

View File

@@ -174,4 +174,4 @@ class PickerResultListener(private val context: Context) : ActivityResultListene
result!!(Result.success(pickedFiles))
return true
}
}
}

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>