Extract NotificationBroadcastReceiverHandler to its own file

This commit is contained in:
Benoit Marty
2024-06-05 09:44:17 +02:00
committed by Benoit Marty
parent dbafc9d4c4
commit 74bb287443
2 changed files with 202 additions and 181 deletions

View File

@@ -19,33 +19,9 @@ package io.element.android.libraries.push.impl.notifications
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationBroadcastReceiver", LoggerTag.NotificationLoggerTag)
/**
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.).
*/
@@ -66,160 +42,3 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
const val KEY_TEXT_REPLY = "key_text_reply"
}
}
class NotificationBroadcastReceiverHandler @Inject constructor(
private val appCoroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
private val sessionPreferencesStore: SessionPreferencesStoreFactory,
private val notificationDrawerManager: NotificationDrawerManager,
private val actionIds: NotificationActionIds,
private val systemClock: SystemClock,
private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val stringProvider: StringProvider,
) {
fun onReceive(intent: Intent) {
val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId)
val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId)
val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId)
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
when (intent.action) {
actionIds.smartReply -> if (roomId != null) {
handleSmartReply(sessionId, roomId, threadId, intent)
}
actionIds.dismissRoom -> if (roomId != null) {
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
}
actionIds.dismissSummary ->
notificationDrawerManager.clearAllMessagesEvents(sessionId)
actionIds.dismissInvite -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
}
actionIds.dismissEvent -> if (eventId != null) {
notificationDrawerManager.clearEvent(sessionId, eventId)
}
actionIds.markRoomRead -> if (roomId != null) {
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleJoinRoom(sessionId, roomId)
}
actionIds.reject -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleRejectRoom(sessionId, roomId)
}
}
}
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.joinRoom(roomId)
}
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.getRoom(roomId)?.leave()
}
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
val receiptType = if (isSendPublicReadReceiptsEnabled) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
}
private fun handleSmartReply(
sessionId: SessionId,
roomId: RoomId,
threadId: ThreadId?,
intent: Intent,
) = appCoroutineScope.launch {
val message = getReplyMessage(intent)
if (message.isNullOrBlank()) {
// ignore this event
// Can this happen? should we update notification?
return@launch
}
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.getRoom(roomId)?.let { room ->
sendMatrixEvent(
sessionId = sessionId,
roomId = roomId,
threadId = threadId,
room = room,
message = message,
)
}
}
private suspend fun sendMatrixEvent(
sessionId: SessionId,
roomId: RoomId,
threadId: ThreadId?,
room: MatrixRoom,
message: String,
) {
// Create a new event to be displayed in the notification drawer, right now
val notifiableMessageEvent = NotifiableMessageEvent(
sessionId = sessionId,
roomId = roomId,
// Generate a Fake event id
eventId = EventId("\$" + UUID.randomUUID().toString()),
editedEventId = null,
canBeReplaced = false,
senderId = sessionId,
noisy = false,
timestamp = systemClock.epochMillis(),
senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId).getOrNull()?.disambiguatedDisplayName
?: stringProvider.getString(R.string.notification_sender_me),
body = message,
imageUriString = null,
threadId = threadId,
roomName = room.displayName,
roomIsDirect = room.isDirect,
outGoingMessage = true,
)
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
if (threadId != null) {
room.liveTimeline.replyMessage(
eventId = threadId.asEventId(),
body = message,
htmlBody = null,
mentions = emptyList(),
fromNotification = true,
)
} else {
room.liveTimeline.sendMessage(
body = message,
htmlBody = null,
mentions = emptyList()
)
}.onFailure {
Timber.e(it, "Failed to send smart reply message")
onNotifiableEventReceived.onNotifiableEventReceived(
notifiableMessageEvent.copy(
outGoingMessageFailed = true
)
)
}
}
private fun getReplyMessage(intent: Intent?): String? {
if (intent != null) {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
if (remoteInput != null) {
return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString()
}
}
return null
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.push.impl.notifications
import android.content.Intent
import androidx.core.app.RemoteInput
import io.element.android.features.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.impl.R
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
import io.element.android.libraries.push.impl.push.OnNotifiableEventReceived
import io.element.android.services.toolbox.api.strings.StringProvider
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag)
class NotificationBroadcastReceiverHandler @Inject constructor(
private val appCoroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
private val sessionPreferencesStore: SessionPreferencesStoreFactory,
private val notificationDrawerManager: NotificationDrawerManager,
private val actionIds: NotificationActionIds,
private val systemClock: SystemClock,
private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val stringProvider: StringProvider,
) {
fun onReceive(intent: Intent) {
val sessionId = intent.extras?.getString(NotificationBroadcastReceiver.KEY_SESSION_ID)?.let(::SessionId) ?: return
val roomId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_ROOM_ID)?.let(::RoomId)
val threadId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_THREAD_ID)?.let(::ThreadId)
val eventId = intent.getStringExtra(NotificationBroadcastReceiver.KEY_EVENT_ID)?.let(::EventId)
Timber.tag(loggerTag.value).d("onReceive: ${intent.action} ${intent.data} for: ${roomId?.value}/${eventId?.value}")
when (intent.action) {
actionIds.smartReply -> if (roomId != null) {
handleSmartReply(sessionId, roomId, threadId, intent)
}
actionIds.dismissRoom -> if (roomId != null) {
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
}
actionIds.dismissSummary ->
notificationDrawerManager.clearAllMessagesEvents(sessionId)
actionIds.dismissInvite -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
}
actionIds.dismissEvent -> if (eventId != null) {
notificationDrawerManager.clearEvent(sessionId, eventId)
}
actionIds.markRoomRead -> if (roomId != null) {
notificationDrawerManager.clearMessagesForRoom(sessionId, roomId)
handleMarkAsRead(sessionId, roomId)
}
actionIds.join -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleJoinRoom(sessionId, roomId)
}
actionIds.reject -> if (roomId != null) {
notificationDrawerManager.clearMembershipNotificationForRoom(sessionId, roomId)
handleRejectRoom(sessionId, roomId)
}
}
}
private fun handleJoinRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.joinRoom(roomId)
}
private fun handleRejectRoom(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.getRoom(roomId)?.leave()
}
private fun handleMarkAsRead(sessionId: SessionId, roomId: RoomId) = appCoroutineScope.launch {
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
val isSendPublicReadReceiptsEnabled = sessionPreferencesStore.get(sessionId, this).isSendPublicReadReceiptsEnabled().first()
val receiptType = if (isSendPublicReadReceiptsEnabled) {
ReceiptType.READ
} else {
ReceiptType.READ_PRIVATE
}
client.getRoom(roomId)?.markAsRead(receiptType = receiptType)
}
private fun handleSmartReply(
sessionId: SessionId,
roomId: RoomId,
threadId: ThreadId?,
intent: Intent,
) = appCoroutineScope.launch {
val message = getReplyMessage(intent)
if (message.isNullOrBlank()) {
// ignore this event
// Can this happen? should we update notification?
return@launch
}
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return@launch
client.getRoom(roomId)?.let { room ->
sendMatrixEvent(
sessionId = sessionId,
roomId = roomId,
threadId = threadId,
room = room,
message = message,
)
}
}
private suspend fun sendMatrixEvent(
sessionId: SessionId,
roomId: RoomId,
threadId: ThreadId?,
room: MatrixRoom,
message: String,
) {
// Create a new event to be displayed in the notification drawer, right now
val notifiableMessageEvent = NotifiableMessageEvent(
sessionId = sessionId,
roomId = roomId,
// Generate a Fake event id
eventId = EventId("\$" + UUID.randomUUID().toString()),
editedEventId = null,
canBeReplaced = false,
senderId = sessionId,
noisy = false,
timestamp = systemClock.epochMillis(),
senderDisambiguatedDisplayName = room.getUpdatedMember(sessionId)
.getOrNull()?.disambiguatedDisplayName
?: stringProvider.getString(R.string.notification_sender_me),
body = message,
imageUriString = null,
threadId = threadId,
roomName = room.displayName,
roomIsDirect = room.isDirect,
outGoingMessage = true,
)
onNotifiableEventReceived.onNotifiableEventReceived(notifiableMessageEvent)
if (threadId != null) {
room.liveTimeline.replyMessage(
eventId = threadId.asEventId(),
body = message,
htmlBody = null,
mentions = emptyList(),
fromNotification = true,
)
} else {
room.liveTimeline.sendMessage(
body = message,
htmlBody = null,
mentions = emptyList()
)
}.onFailure {
Timber.e(it, "Failed to send smart reply message")
onNotifiableEventReceived.onNotifiableEventReceived(
notifiableMessageEvent.copy(
outGoingMessageFailed = true
)
)
}
}
private fun getReplyMessage(intent: Intent?): String? {
if (intent != null) {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
if (remoteInput != null) {
return remoteInput.getCharSequence(NotificationBroadcastReceiver.KEY_TEXT_REPLY)?.toString()
}
}
return null
}
}