Updated notifications manager to allow mark as read & reply on chat messages notifications

This commit is contained in:
Sylvain Berfini 2023-09-22 12:13:13 +02:00
parent 686503c83c
commit c314c4cba3
5 changed files with 231 additions and 3 deletions

View file

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

View file

@ -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<String, Notifiable> = 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()

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M228.44,89.34l-96,-64a8,8 0,0 0,-8.88 0l-96,64A8,8 0,0 0,24 96V200a16,16 0,0 0,16 16H216a16,16 0,0 0,16 -16V96A8,8 0,0 0,228.44 89.34ZM128,41.61l81.91,54.61 -67,47.78H113.11l-67,-47.78ZM40,200V111.53l65.9,47a8,8 0,0 0,4.65 1.49h34.9a8,8 0,0 0,4.65 -1.49l65.9,-47V200Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M227.32,28.68a16,16 0,0 0,-15.66 -4.08l-0.15,0L19.57,82.84a16,16 0,0 0,-2.42 29.84l85.62,40.55 40.55,85.62A15.86,15.86 0,0 0,157.74 248q0.69,0 1.38,-0.06a15.88,15.88 0,0 0,14 -11.51l58.2,-191.94c0,-0.05 0,-0.1 0,-0.15A16,16 0,0 0,227.32 28.68ZM157.83,231.85l-0.05,0.14L118.42,148.9l47.24,-47.25a8,8 0,0 0,-11.31 -11.31L107.1,137.58 24,98.22l0.14,0L216,40Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -43,6 +43,9 @@
<string name="notification_channel_incoming_call_name">&appName; incoming calls notifications</string>
<string name="notification_channel_service_name">&appName; service notification</string>
<string name="notification_channel_chat_name">&appName; instant messages notifications</string>
<string name="notification_chat_message_reaction_received">%s has reacted by %s to: %s</string>
<string name="notification_mark_message_as_read">Mark as read</string>
<string name="notification_reply_to_message">Reply</string>
<string name="bottom_navigation_contacts_label">Contacts</string>
<string name="bottom_navigation_calls_label">Calls</string>
@ -286,8 +289,6 @@
<string name="voip_call_state_connected">Active</string>
<string name="voip_call_state_paused">Paused</string>
<string name="voip_call_state_ended">Ended</string>
<string name="chat_message_reaction_received">%s has reacted by %s to: %s</string>
<!-- Keep <u></u> in following strings translations! -->
<string name="welcome_carousel_skip"><u>Skip</u></string>