From c314c4cba352b4cd5696d99639f6fae72b1580fa Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 22 Sep 2023 12:13:13 +0200 Subject: [PATCH] Updated notifications manager to allow mark as read & reply on chat messages notifications --- .../NotificationBroadcastReceiver.kt | 79 +++++++++++ .../notifications/NotificationsManager.kt | 132 +++++++++++++++++- .../res/drawable/envelope_simple_open.xml | 9 ++ .../main/res/drawable/paper_plane_tilt.xml | 9 ++ app/src/main/res/values/strings.xml | 5 +- 5 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/envelope_simple_open.xml create mode 100644 app/src/main/res/drawable/paper_plane_tilt.xml diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt index faa7af7c4..75703f5d9 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt @@ -19,6 +19,8 @@ */ package org.linphone.notifications +import android.app.NotificationManager +import android.app.RemoteInput import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -39,6 +41,8 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) { handleCallIntent(intent) + } else if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { + handleChatIntent(context, intent, notificationId) } } @@ -68,4 +72,79 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } } } + + private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int) { + val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) + if (remoteSipAddress == null) { + Log.e( + "$TAG Remote SIP address is null for notification id $notificationId" + ) + return + } + val localIdentity = intent.getStringExtra(NotificationsManager.INTENT_LOCAL_IDENTITY) + if (localIdentity == null) { + Log.e( + "$TAG Local identity is null for notification id $notificationId" + ) + return + } + + val reply = getMessageText(intent)?.toString() + if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + if (reply == null) { + Log.e("$TAG Couldn't get reply text") + return + } + } + + coreContext.postOnCoreThread { core -> + val remoteAddress = core.interpretUrl(remoteSipAddress, false) + if (remoteAddress == null) { + Log.e( + "$TAG Couldn't interpret remote address $remoteSipAddress" + ) + return@postOnCoreThread + } + + val localAddress = core.interpretUrl(localIdentity, false) + if (localAddress == null) { + Log.e( + "$TAG Couldn't interpret local address $localIdentity" + ) + return@postOnCoreThread + } + + val room = core.searchChatRoom(null, localAddress, remoteAddress, arrayOfNulls(0)) + if (room == null) { + Log.e( + "$TAG Couldn't find chat room for remote address $remoteSipAddress and local address $localIdentity" + ) + return@postOnCoreThread + } + + if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + val msg = room.createMessageFromUtf8(reply) + msg.userData = notificationId + msg.addListener(coreContext.notificationsManager.chatListener) + msg.send() + Log.i("$TAG Reply sent for notif id $notificationId") + } else if (intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { + room.markAsRead() + if (!coreContext.notificationsManager.dismissChatNotification(room)) { + Log.w( + "$TAG Notifications Manager failed to cancel notification" + ) + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + notificationManager.cancel(NotificationsManager.CHAT_TAG, notificationId) + } + } + } + } + + private fun getMessageText(intent: Intent): CharSequence? { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + return remoteInput?.getCharSequence(NotificationsManager.KEY_TEXT_REPLY) + } } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 5d1b41447..16cd178b4 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -38,6 +38,7 @@ import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person +import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import androidx.core.content.LocusIdCompat import androidx.core.graphics.drawable.IconCompat @@ -48,6 +49,8 @@ import org.linphone.contacts.getPerson import org.linphone.core.Address import org.linphone.core.Call import org.linphone.core.ChatMessage +import org.linphone.core.ChatMessageListener +import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatMessageReaction import org.linphone.core.ChatRoom import org.linphone.core.Core @@ -66,10 +69,16 @@ class NotificationsManager @MainThread constructor(private val context: Context) const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION" const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION" + const val INTENT_REPLY_MESSAGE_NOTIF_ACTION = "org.linphone.REPLY_ACTION" + const val INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION = "org.linphone.MARK_AS_READ_ACTION" const val INTENT_CALL_ID = "CALL_ID" const val INTENT_NOTIF_ID = "NOTIFICATION_ID" + const val KEY_TEXT_REPLY = "key_text_reply" + const val INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY" + const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" + const val CHAT_TAG = "Chat" const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" } @@ -220,6 +229,37 @@ class NotificationsManager @MainThread constructor(private val context: Context) } } + val chatListener: ChatMessageListener = object : ChatMessageListenerStub() { + @WorkerThread + override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { + message.userData ?: return + val id = message.userData as Int + Log.i("$TAG Reply message state changed [$state] for id $id") + + if (state != ChatMessage.State.InProgress) { + // No need to be called here twice + message.removeListener(this) + } + + if (state == ChatMessage.State.Delivered || state == ChatMessage.State.Displayed) { + val address = message.chatRoom.peerAddress.asStringUriOnly() + val notifiable = chatNotificationsMap[address] + if (notifiable != null) { + if (notifiable.notificationId != id) { + Log.w("$TAG ID doesn't match: ${notifiable.notificationId} != $id") + } + displayReplyMessageNotification(message, notifiable) + } else { + Log.e("$TAG Couldn't find notification for chat room $address") + cancelNotification(id, CHAT_TAG) + } + } else if (state == ChatMessage.State.NotDelivered) { + Log.e("$TAG Reply wasn't delivered") + cancelNotification(id, CHAT_TAG) + } + } + } + private var coreService: CoreForegroundService? = null private val callNotificationsMap: HashMap = HashMap() @@ -448,7 +488,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) val displayName = contact?.name ?: LinphoneUtils.getDisplayName(address) val originalMessage = getTextDescribingMessage(message) - val text = AppUtils.getString(R.string.chat_message_reaction_received).format( + val text = AppUtils.getString(R.string.notification_chat_message_reaction_received).format( displayName, reaction, originalMessage @@ -737,6 +777,8 @@ class NotificationsManager @MainThread constructor(private val context: Context) .setWhen(System.currentTimeMillis()) .setShowWhen(true) .setStyle(style) + .addAction(getReplyMessageAction(notifiable)) + .addAction(getMarkMessageAsReadAction(notifiable)) .setShortcutId(id) .setLocusId(LocusIdCompat(id)) @@ -815,6 +857,94 @@ class NotificationsManager @MainThread constructor(private val context: Context) ) } + @WorkerThread + private fun displayReplyMessageNotification(message: ChatMessage, notifiable: Notifiable) { + Log.i( + "$TAG Updating message notification with reply for notification ${notifiable.notificationId}" + ) + + val text = message.contents.find { content -> content.isText }?.utf8Text ?: "" + val senderAddress = message.fromAddress + val reply = NotifiableMessage( + text, + null, + notifiable.myself ?: LinphoneUtils.getDisplayName(senderAddress), + System.currentTimeMillis(), + isOutgoing = true + ) + notifiable.messages.add(reply) + + val chatRoom = message.chatRoom + val me = coreContext.contactsManager.getMePerson(chatRoom.localAddress) + val notification = createMessageNotification( + notifiable, + LinphoneUtils.getChatRoomId(chatRoom), + me + ) + notify(notifiable.notificationId, notification, CHAT_TAG) + } + + @AnyThread + private fun getReplyMessageAction(notifiable: Notifiable): NotificationCompat.Action { + val replyLabel = + context.resources.getString(R.string.notification_reply_to_message) + val remoteInput = + RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() + + val replyIntent = Intent(context, NotificationBroadcastReceiver::class.java) + replyIntent.action = INTENT_REPLY_MESSAGE_NOTIF_ACTION + replyIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) + replyIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + + // PendingIntents attached to actions with remote inputs must be mutable + val replyPendingIntent = PendingIntent.getBroadcast( + context, + notifiable.notificationId, + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + return NotificationCompat.Action.Builder( + R.drawable.paper_plane_tilt, + context.getString(R.string.notification_reply_to_message), + replyPendingIntent + ) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .setShowsUserInterface(false) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .build() + } + + @AnyThread + private fun getMarkMessageAsReadPendingIntent(notifiable: Notifiable): PendingIntent { + val markAsReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) + markAsReadIntent.action = INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION + markAsReadIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) + markAsReadIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + + return PendingIntent.getBroadcast( + context, + notifiable.notificationId, + markAsReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + @AnyThread + private fun getMarkMessageAsReadAction(notifiable: Notifiable): NotificationCompat.Action { + val markAsReadPendingIntent = getMarkMessageAsReadPendingIntent(notifiable) + return NotificationCompat.Action.Builder( + R.drawable.envelope_simple_open, + context.getString(R.string.notification_mark_message_as_read), + markAsReadPendingIntent + ) + .setShowsUserInterface(false) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .build() + } + @WorkerThread private fun getPerson(friend: Friend?, displayName: String, picture: Bitmap?): Person { return friend?.getPerson() diff --git a/app/src/main/res/drawable/envelope_simple_open.xml b/app/src/main/res/drawable/envelope_simple_open.xml new file mode 100644 index 000000000..e5c8589ab --- /dev/null +++ b/app/src/main/res/drawable/envelope_simple_open.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/paper_plane_tilt.xml b/app/src/main/res/drawable/paper_plane_tilt.xml new file mode 100644 index 000000000..f24c657d7 --- /dev/null +++ b/app/src/main/res/drawable/paper_plane_tilt.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c75b6a8c..f6c7e411d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,9 @@ &appName; incoming calls notifications &appName; service notification &appName; instant messages notifications + %s has reacted by %s to: %s + Mark as read + Reply Contacts Calls @@ -286,8 +289,6 @@ Active Paused Ended - - %s has reacted by %s to: %s Skip