refactor: Move the notification code into its own file

This commit is contained in:
PapaTutuWawa 2023-09-08 21:56:09 +02:00
parent c22b35b4ac
commit 42ff70a966
6 changed files with 436 additions and 438 deletions

View File

@ -1,14 +1,12 @@
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
@ -19,17 +17,9 @@ import org.moxxy.moxxy_native.cryptography.CryptographyImplementation
import org.moxxy.moxxy_native.cryptography.MoxxyCryptographyApi import org.moxxy.moxxy_native.cryptography.MoxxyCryptographyApi
import org.moxxy.moxxy_native.media.MediaImplementation import org.moxxy.moxxy_native.media.MediaImplementation
import org.moxxy.moxxy_native.media.MoxxyMediaApi import org.moxxy.moxxy_native.media.MoxxyMediaApi
import org.moxxy.moxxy_native.notifications.MessagingNotification
import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi 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.NotificationEvent
import org.moxxy.moxxy_native.notifications.NotificationGroup import org.moxxy.moxxy_native.notifications.NotificationsImplementation
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.FilePickerType
import org.moxxy.moxxy_native.picker.MoxxyPickerApi import org.moxxy.moxxy_native.picker.MoxxyPickerApi
import org.moxxy.moxxy_native.picker.PickerResultListener import org.moxxy.moxxy_native.picker.PickerResultListener
@ -60,7 +50,7 @@ object NotificationCache {
var lastEvent: NotificationEvent? = null var lastEvent: NotificationEvent? = null
} }
class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNotificationsApi { class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi {
private var context: Context? = null private var context: Context? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var pickerListener: PickerResultListener private lateinit var pickerListener: PickerResultListener
@ -68,15 +58,17 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNot
private lateinit var contactsImplementation: ContactsImplementation private lateinit var contactsImplementation: ContactsImplementation
private lateinit var platformImplementation: PlatformImplementation private lateinit var platformImplementation: PlatformImplementation
private val mediaImplementation = MediaImplementation() private val mediaImplementation = MediaImplementation()
private lateinit var notificationsImplementation: NotificationsImplementation
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext context = flutterPluginBinding.applicationContext
contactsImplementation = ContactsImplementation(context!!) contactsImplementation = ContactsImplementation(context!!)
platformImplementation = PlatformImplementation(context!!) platformImplementation = PlatformImplementation(context!!)
notificationsImplementation = NotificationsImplementation(context!!)
// Register the pigeon handlers // Register the pigeon handlers
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this) MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, this) MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, notificationsImplementation)
MoxxyCryptographyApi.setUp(flutterPluginBinding.binaryMessenger, cryptographyImplementation) MoxxyCryptographyApi.setUp(flutterPluginBinding.binaryMessenger, cryptographyImplementation)
MoxxyContactsApi.setUp(flutterPluginBinding.binaryMessenger, contactsImplementation) MoxxyContactsApi.setUp(flutterPluginBinding.binaryMessenger, contactsImplementation)
MoxxyPlatformApi.setUp(flutterPluginBinding.binaryMessenger, platformImplementation) MoxxyPlatformApi.setUp(flutterPluginBinding.binaryMessenger, platformImplementation)
@ -167,54 +159,4 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNot
val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType)) val pickIntent = contract.createIntent(context!!, PickVisualMediaRequest(pickType))
activity?.startActivityForResult(pickIntent, PICK_FILE_WITH_DATA_REQUEST) 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,21 @@
package org.moxxy.moxxy_native.notifications
import android.content.Intent
import android.util.Log
import org.moxxy.moxxy_native.TAG
/*
* Extract all user-added extra key-value pairs from @intent.
* */
fun extractPayloadMapFromIntent(intent: Intent): Map<String?, String?> {
val extras = mutableMapOf<String?, String?>()
intent.extras?.keySet()!!.forEach {
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
}

View File

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

View File

@ -27,19 +27,6 @@ import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File import java.io.File
import java.time.Instant 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() { class NotificationReceiver : BroadcastReceiver() {
/* /*
* Dismisses the notification through which we received @intent. * Dismisses the notification through which we received @intent.

View File

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

View File

@ -0,0 +1,331 @@
package org.moxxy.moxxy_native.notifications
import android.app.Notification
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.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.TAG
import org.moxxy.moxxy_native.TAP_ACTION
import java.io.File
class NotificationsImplementation(private val context: Context) : MoxxyNotificationsApi {
override fun createNotificationGroups(groups: List<NotificationGroup>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (group in groups) {
notificationManager.createNotificationChannelGroup(
NotificationChannelGroup(group.id, group.description),
)
}
}
override fun deleteNotificationGroups(ids: List<String>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannelGroup(id)
}
}
override fun createNotificationChannels(channels: List<NotificationChannel>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (channel in channels) {
val importance = when (channel.importance) {
NotificationChannelImportance.DEFAULT -> NotificationManager.IMPORTANCE_DEFAULT
NotificationChannelImportance.MIN -> NotificationManager.IMPORTANCE_MIN
NotificationChannelImportance.HIGH -> NotificationManager.IMPORTANCE_HIGH
}
val notificationChannel =
android.app.NotificationChannel(channel.id, channel.title, importance).apply {
description = channel.description
enableVibration(channel.vibration)
enableLights(channel.enableLights)
setShowBadge(channel.showBadge)
if (channel.groupId != null) {
group = channel.groupId
}
}
notificationManager.createNotificationChannel(notificationChannel)
}
}
override fun deleteNotificationChannels(ids: List<String>) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
for (id in ids) {
notificationManager.deleteNotificationChannel(id)
}
}
override fun showMessagingNotification(notification: MessagingNotification) {
// Build the actions
// -> Reply action
val remoteInput = RemoteInput.Builder(REPLY_TEXT_KEY).apply {
setLabel(NotificationDataManager.getReply(context))
}.build()
val replyIntent = Intent(context, NotificationReceiver::class.java).apply {
action = REPLY_ACTION
putExtra(NOTIFICATION_EXTRA_JID_KEY, notification.jid)
putExtra(NOTIFICATION_EXTRA_ID_KEY, notification.id)
notification.extra?.forEach {
putExtra("payload_${it.key}", it.value)
}
}
val replyPendingIntent = PendingIntent.getBroadcast(
context.applicationContext,
0,
replyIntent,
PendingIntent.FLAG_MUTABLE,
)
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}")
}
}
override fun showNotification(notification: RegularNotification) {
val builtNotification = NotificationCompat.Builder(context, notification.channelId).apply {
setContentTitle(notification.title)
setContentText(notification.body)
when (notification.icon) {
NotificationIcon.ERROR -> setSmallIcon(R.drawable.error)
NotificationIcon.WARNING -> setSmallIcon(R.drawable.warning)
NotificationIcon.NONE -> {}
}
if (notification.groupId != null) {
setGroup(notification.groupId)
}
}.build()
// Post the notification
try {
NotificationManagerCompat.from(context)
.notify(notification.id.toInt(), builtNotification)
} catch (ex: SecurityException) {
// Should never happen as Moxxy checks for the permission before posting the notification
Log.e(TAG, "Failed to post notification: ${ex.message}")
}
}
override fun dismissNotification(id: Long) {
NotificationManagerCompat.from(context).cancel(id.toInt())
}
override fun setNotificationSelfAvatar(path: String) {
NotificationDataManager.setAvatarPath(context, path)
}
override fun setNotificationI18n(data: NotificationI18nData) {
NotificationDataManager.apply {
setYou(
context,
data.you,
)
setReply(
context,
data.reply,
)
setMarkAsRead(
context,
data.markAsRead,
)
}
}
override fun notificationStub(event: NotificationEvent) {
// N/A
}
}