Element Call ringing notifications (#2978)
- Add `ActiveCallManager` to handle incoming and ongoing calls. - Add ringing call notifications with full screen intents and missed call ones as part of the 'conversation' notifications. --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
parent
4867354fd4
commit
30a1367714
186 changed files with 2686 additions and 330 deletions
|
|
@ -22,6 +22,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
interface ActiveNotificationsProvider {
|
||||
|
|
@ -37,7 +38,6 @@ interface ActiveNotificationsProvider {
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultActiveNotificationsProvider @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
) : ActiveNotificationsProvider {
|
||||
override fun getAllNotifications(): List<StatusBarNotification> {
|
||||
return notificationManager.activeNotifications
|
||||
|
|
@ -48,22 +48,22 @@ class DefaultActiveNotificationsProvider @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getMembershipNotificationForSession(sessionId: SessionId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId }
|
||||
}
|
||||
|
||||
override fun getMessageNotificationsForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomMessagesNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getMembershipNotificationForRoom(sessionId: SessionId, roomId: RoomId): List<StatusBarNotification> {
|
||||
val notificationId = notificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
val notificationId = NotificationIdProvider.getRoomInvitationNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).filter { it.id == notificationId && it.tag == roomId.value }
|
||||
}
|
||||
|
||||
override fun getSummaryNotification(sessionId: SessionId): StatusBarNotification? {
|
||||
val summaryId = notificationIdProvider.getSummaryNotificationId(sessionId)
|
||||
val summaryId = NotificationIdProvider.getSummaryNotificationId(sessionId)
|
||||
return getNotificationsForSession(sessionId).find { it.id == summaryId }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkParser
|
|||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType
|
||||
|
|
@ -51,6 +52,7 @@ import io.element.android.libraries.push.impl.notifications.model.FallbackNotifi
|
|||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
|
@ -150,8 +152,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
NotificationContent.MessageLike.CallAnswer,
|
||||
NotificationContent.MessageLike.CallCandidates,
|
||||
NotificationContent.MessageLike.CallHangup,
|
||||
is NotificationContent.MessageLike.CallNotify -> { // TODO CallNotify will be handled separately in the future
|
||||
NotificationContent.MessageLike.CallHangup -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
null
|
||||
}
|
||||
|
|
@ -172,6 +173,44 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp)) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
roomName = roomDisplayName,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
timestamp = this.timestamp,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
description = stringProvider.getString(R.string.notification_incoming_call),
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
callNotifyType = content.type,
|
||||
senderId = content.senderId,
|
||||
senderAvatarUrl = senderAvatarUrl,
|
||||
)
|
||||
} else {
|
||||
// Create a simple message notification event
|
||||
buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
noisy = true,
|
||||
timestamp = this.timestamp,
|
||||
senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId),
|
||||
body = "☎️ ${stringProvider.getString(R.string.notification_incoming_call)}",
|
||||
roomName = roomDisplayName,
|
||||
roomIsDirect = isDirect,
|
||||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
type = EventType.CALL_NOTIFY,
|
||||
)
|
||||
}
|
||||
}
|
||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||
NotificationContent.MessageLike.KeyVerificationDone,
|
||||
|
|
@ -334,7 +373,8 @@ private fun buildNotifiableMessageEvent(
|
|||
outGoingMessage: Boolean = false,
|
||||
outGoingMessageFailed: Boolean = false,
|
||||
isRedacted: Boolean = false,
|
||||
isUpdated: Boolean = false
|
||||
isUpdated: Boolean = false,
|
||||
type: String = EventType.MESSAGE,
|
||||
) = NotifiableMessageEvent(
|
||||
sessionId = sessionId,
|
||||
senderId = senderId,
|
||||
|
|
@ -356,5 +396,6 @@ private fun buildNotifiableMessageEvent(
|
|||
outGoingMessage = outGoingMessage,
|
||||
outGoingMessageFailed = outGoingMessageFailed,
|
||||
isRedacted = isRedacted,
|
||||
isUpdated = isUpdated
|
||||
isUpdated = isUpdated,
|
||||
type = type,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,23 +24,27 @@ import androidx.core.graphics.drawable.toBitmap
|
|||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationBitmapLoader @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationBitmapLoader @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider,
|
||||
) {
|
||||
) : NotificationBitmapLoader {
|
||||
/**
|
||||
* Get icon of a room.
|
||||
* @param path mxc url
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
|
||||
override suspend fun getRoomBitmap(path: String?, imageLoader: ImageLoader): Bitmap? {
|
||||
if (path == null) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -67,7 +71,7 @@ class NotificationBitmapLoader @Inject constructor(
|
|||
* @param path mxc url
|
||||
* @param imageLoader Coil image loader
|
||||
*/
|
||||
suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
override suspend fun getUserIcon(path: String?, imageLoader: ImageLoader): IconCompat? {
|
||||
if (path == null || sdkIntProvider.get() < Build.VERSION_CODES.P) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.ThreadId
|
|||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
|
|
@ -53,7 +54,6 @@ private val loggerTag = LoggerTag("DefaultNotificationDrawerManager", LoggerTag.
|
|||
class DefaultNotificationDrawerManager @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val notificationRenderer: NotificationRenderer,
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
coroutineScope: CoroutineScope,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
|
|
@ -124,7 +124,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
* Clear all known message events for a [sessionId].
|
||||
*/
|
||||
override fun clearAllMessagesEvents(sessionId: SessionId) {
|
||||
notificationManager.cancel(null, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
notificationManager.cancel(null, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
* Can also be called when a notification for this room is dismissed by the user.
|
||||
*/
|
||||
override fun clearMessagesForRoom(sessionId: SessionId, roomId: RoomId) {
|
||||
notificationManager.cancel(roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
notificationManager.cancel(roomId.value, NotificationIdProvider.getRoomMessagesNotificationId(sessionId))
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
|
|||
* Clear the notifications for a single event.
|
||||
*/
|
||||
override fun clearEvent(sessionId: SessionId, eventId: EventId) {
|
||||
val id = notificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||
val id = NotificationIdProvider.getRoomEventNotificationId(sessionId)
|
||||
notificationManager.cancel(eventId.value, id)
|
||||
clearSummaryNotificationIfNeeded(sessionId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
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.push.api.notifications.OnMissedCallNotificationHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
||||
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
|
||||
private val notifiableEventResolver: NotifiableEventResolver,
|
||||
) : OnMissedCallNotificationHandler {
|
||||
override suspend fun addMissedCallNotification(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
) {
|
||||
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(sessionId, roomId, eventId)
|
||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +49,8 @@ interface NotificationDataFactory {
|
|||
@JvmName("toNotificationSimpleEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(simpleEvents: List<SimpleNotifiableEvent>): List<OneShotNotification>
|
||||
@JvmName("toNotificationFallbackEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification>
|
||||
|
||||
fun createSummaryNotification(
|
||||
|
|
@ -130,6 +132,8 @@ class DefaultNotificationDataFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@JvmName("toNotificationFallbackEvents")
|
||||
@Suppress("INAPPLICABLE_JVM_NAME")
|
||||
override fun toNotifications(fallback: List<FallbackNotifiableEvent>): List<OneShotNotification> {
|
||||
return fallback.map { event ->
|
||||
OneShotNotification(
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 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 io.element.android.libraries.matrix.api.core.SessionId
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
class NotificationIdProvider @Inject constructor() {
|
||||
fun getSummaryNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + SUMMARY_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getRoomMessagesNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + ROOM_MESSAGES_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getRoomEventNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + ROOM_EVENT_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getRoomInvitationNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + ROOM_INVITATION_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
fun getFallbackNotificationId(sessionId: SessionId): Int {
|
||||
return getOffset(sessionId) + FALLBACK_NOTIFICATION_ID
|
||||
}
|
||||
|
||||
private fun getOffset(sessionId: SessionId): Int {
|
||||
// Compute a int from a string with a low risk of collision.
|
||||
return abs(sessionId.value.hashCode() % 100_000) * 10
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FALLBACK_NOTIFICATION_ID = -1
|
||||
private const val SUMMARY_NOTIFICATION_ID = 0
|
||||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||
private const val ROOM_INVITATION_NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,12 @@ package io.element.android.libraries.push.impl.notifications
|
|||
import coil.ImageLoader
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -30,7 +32,6 @@ import javax.inject.Inject
|
|||
private val loggerTag = LoggerTag("NotificationRenderer", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
class NotificationRenderer @Inject constructor(
|
||||
private val notificationIdProvider: NotificationIdProvider,
|
||||
private val notificationDisplayer: NotificationDisplayer,
|
||||
private val notificationDataFactory: NotificationDataFactory,
|
||||
) {
|
||||
|
|
@ -59,14 +60,14 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Removing summary notification")
|
||||
notificationDisplayer.cancelNotificationMessage(
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId)
|
||||
)
|
||||
}
|
||||
|
||||
roomNotifications.forEach { notificationData ->
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.roomId.value,
|
||||
id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomMessagesNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -76,7 +77,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating invitation notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomInvitationNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -87,7 +88,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating simple notification ${notificationData.key}")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = notificationData.key,
|
||||
id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getRoomEventNotificationId(currentUser.userId),
|
||||
notification = notificationData.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +99,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Showing fallback notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = "FALLBACK",
|
||||
id = notificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getFallbackNotificationId(currentUser.userId),
|
||||
notification = fallbackNotifications.first().notification
|
||||
)
|
||||
}
|
||||
|
|
@ -108,7 +109,7 @@ class NotificationRenderer @Inject constructor(
|
|||
Timber.tag(loggerTag.value).d("Updating summary notification")
|
||||
notificationDisplayer.showNotificationMessage(
|
||||
tag = null,
|
||||
id = notificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
id = NotificationIdProvider.getSummaryNotificationId(currentUser.userId),
|
||||
notification = summaryNotification.notification
|
||||
)
|
||||
}
|
||||
|
|
@ -127,6 +128,8 @@ private fun List<NotifiableEvent>.groupByType(): GroupedNotificationEvents {
|
|||
is NotifiableMessageEvent -> roomEvents.add(event.castedToEventType())
|
||||
is SimpleNotifiableEvent -> simpleEvents.add(event.castedToEventType())
|
||||
is FallbackNotifiableEvent -> fallbackEvents.add(event.castedToEventType())
|
||||
// Nothing should be done for ringing call events as they're not handled here
|
||||
is NotifiableRingingCallEvent -> {}
|
||||
}
|
||||
}
|
||||
return GroupedNotificationEvents(roomEvents, simpleEvents, invitationEvents, fallbackEvents)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.squareup.anvil.annotations.ContributesBinding
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator
|
||||
import io.element.android.libraries.push.impl.notifications.factories.isSmartReplyError
|
||||
|
|
|
|||
|
|
@ -19,10 +19,15 @@ package io.element.android.libraries.push.impl.notifications.channels
|
|||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
|
|
@ -30,15 +35,51 @@ import io.element.android.libraries.push.impl.R
|
|||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
/* ==========================================================================================
|
||||
* IDs for channels
|
||||
* ========================================================================================== */
|
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
|
||||
internal const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
||||
internal const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
|
||||
|
||||
// Legacy channel
|
||||
private const val CALL_NOTIFICATION_CHANNEL_ID_V2 = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||
|
||||
internal const val CALL_NOTIFICATION_CHANNEL_ID_V3 = "CALL_NOTIFICATION_CHANNEL_ID_V3"
|
||||
internal const val RINGING_CALL_NOTIFICATION_CHANNEL_ID = "RINGING_CALL_NOTIFICATION_CHANNEL_ID"
|
||||
|
||||
/**
|
||||
* on devices >= android O, we need to define a channel for each notifications.
|
||||
*/
|
||||
interface NotificationChannels {
|
||||
/**
|
||||
* Get the channel for incoming call.
|
||||
* @param ring true if the device should ring when receiving the call.
|
||||
*/
|
||||
fun getChannelForIncomingCall(ring: Boolean): String
|
||||
|
||||
/**
|
||||
* Get the channel for messages.
|
||||
* @param noisy true if the notification should have sound and vibration.
|
||||
*/
|
||||
fun getChannelIdForMessage(noisy: Boolean): String
|
||||
|
||||
/**
|
||||
* Get the channel for test notifications.
|
||||
*/
|
||||
fun getChannelIdForTest(): String
|
||||
}
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
class NotificationChannels @Inject constructor(
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultNotificationChannels @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val stringProvider: StringProvider,
|
||||
) {
|
||||
) : NotificationChannels {
|
||||
init {
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
|
@ -75,6 +116,9 @@ class NotificationChannels @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Migration - Create new call channel
|
||||
notificationManager.deleteNotificationChannel(CALL_NOTIFICATION_CHANNEL_ID_V2)
|
||||
|
||||
/**
|
||||
* Default notification importance: shows everywhere, makes noise, but does not visually
|
||||
* intrude.
|
||||
|
|
@ -123,46 +167,52 @@ class NotificationChannels @Inject constructor(
|
|||
}
|
||||
)
|
||||
|
||||
// Register a channel for incoming and in progress call notifications with no ringing
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CALL_NOTIFICATION_CHANNEL_ID,
|
||||
CALL_NOTIFICATION_CHANNEL_ID_V3,
|
||||
stringProvider.getString(R.string.notification_channel_call).ifEmpty { "Call" },
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
.apply {
|
||||
description = stringProvider.getString(R.string.notification_channel_call)
|
||||
setSound(null, null)
|
||||
enableVibration(true)
|
||||
enableLights(true)
|
||||
lightColor = accentColor
|
||||
}
|
||||
)
|
||||
|
||||
// Register a channel for incoming call notifications which will ring the device when received
|
||||
val ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannelCompat.Builder(
|
||||
RINGING_CALL_NOTIFICATION_CHANNEL_ID,
|
||||
NotificationManagerCompat.IMPORTANCE_MAX,
|
||||
)
|
||||
.setName(stringProvider.getString(R.string.notification_channel_ringing_calls).ifEmpty { "Ringing calls" })
|
||||
.setVibrationEnabled(true)
|
||||
.setSound(
|
||||
ringtoneUri,
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setLegacyStreamType(AudioManager.STREAM_RING)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
||||
.build()
|
||||
)
|
||||
.setDescription(stringProvider.getString(R.string.notification_channel_ringing_calls))
|
||||
.setLightsEnabled(true)
|
||||
.setLightColor(accentColor)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getChannel(channelId: String): NotificationChannel? {
|
||||
return notificationManager.getNotificationChannel(channelId)
|
||||
override fun getChannelForIncomingCall(ring: Boolean): String {
|
||||
return if (ring) RINGING_CALL_NOTIFICATION_CHANNEL_ID else CALL_NOTIFICATION_CHANNEL_ID_V3
|
||||
}
|
||||
|
||||
fun getChannelForIncomingCall(fromBg: Boolean): NotificationChannel? {
|
||||
val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||
return getChannel(notificationChannel)
|
||||
}
|
||||
|
||||
fun getChannelIdForMessage(noisy: Boolean): String {
|
||||
override fun getChannelIdForMessage(noisy: Boolean): String {
|
||||
return if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
|
||||
}
|
||||
|
||||
fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
|
||||
|
||||
companion object {
|
||||
/* ==========================================================================================
|
||||
* IDs for channels
|
||||
* ========================================================================================== */
|
||||
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"
|
||||
private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
|
||||
private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"
|
||||
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
|
||||
private fun supportNotificationChannels() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
}
|
||||
override fun getChannelIdForTest(): String = NOISY_NOTIFICATION_CHANNEL_ID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,10 @@ import io.element.android.libraries.di.AppScope
|
|||
import io.element.android.libraries.di.ApplicationContext
|
||||
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.timeline.item.event.EventType
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.push.api.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationBitmapLoader
|
||||
import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug
|
||||
|
|
@ -129,12 +130,16 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
|
||||
val smallIcon = CommonDrawables.ic_notification_small
|
||||
|
||||
val channelId = notificationChannels.getChannelIdForMessage(roomInfo.shouldBing)
|
||||
val containsMissedCall = events.any { it.type == EventType.CALL_NOTIFY }
|
||||
val channelId = if (containsMissedCall) {
|
||||
notificationChannels.getChannelForIncomingCall(false)
|
||||
} else {
|
||||
notificationChannels.getChannelIdForMessage(noisy = roomInfo.shouldBing)
|
||||
}
|
||||
val builder = if (existingNotification != null) {
|
||||
NotificationCompat.Builder(context, existingNotification)
|
||||
} else {
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setOnlyAlertOnce(roomInfo.isUpdated)
|
||||
// A category allows groups of notifications to be ranked and filtered – per user or system settings.
|
||||
// For example, alarm notifications should display before promo notifications, or message from known contact
|
||||
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
|
||||
|
|
@ -210,6 +215,11 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
setLargeIcon(largeIcon)
|
||||
}
|
||||
setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId))
|
||||
|
||||
// If any of the events are of call notify type it means a missed call, set the category to the right value
|
||||
if (events.any { it.type == EventType.CALL_NOTIFY }) {
|
||||
setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
||||
}
|
||||
}
|
||||
.setTicker(tickerText)
|
||||
.build()
|
||||
|
|
@ -343,7 +353,6 @@ class DefaultNotificationCreator @Inject constructor(
|
|||
.setWhen(lastMessageTimestamp)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setSmallIcon(smallIcon)
|
||||
// set content text to support devices running API level < 24
|
||||
.setGroup(currentUser.userId.value)
|
||||
// set this notification as the summary for the group
|
||||
.setGroupSummary(true)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ data class NotifiableMessageEvent(
|
|||
val outGoingMessage: Boolean = false,
|
||||
val outGoingMessageFailed: Boolean = false,
|
||||
override val isRedacted: Boolean = false,
|
||||
override val isUpdated: Boolean = false
|
||||
) : NotifiableEvent {
|
||||
override val isUpdated: Boolean = false,
|
||||
val type: String = EventType.MESSAGE
|
||||
) : NotifiableEvent {
|
||||
override val description: String = body ?: ""
|
||||
|
||||
// Example of value:
|
||||
|
|
@ -69,9 +69,16 @@ fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationSta
|
|||
val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false
|
||||
return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) {
|
||||
null -> false
|
||||
else -> appNavigationState.isInForeground &&
|
||||
sessionId == currentSessionId &&
|
||||
roomId == currentRoomId &&
|
||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||
else -> {
|
||||
// Never ignore ringing call notifications
|
||||
if (this is NotifiableRingingCallEvent) {
|
||||
false
|
||||
} else {
|
||||
appNavigationState.isInForeground &&
|
||||
sessionId == currentSessionId &&
|
||||
roomId == currentRoomId &&
|
||||
(this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
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.UserId
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import java.time.Instant
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class NotifiableRingingCallEvent(
|
||||
override val sessionId: SessionId,
|
||||
override val roomId: RoomId,
|
||||
override val eventId: EventId,
|
||||
override val editedEventId: EventId?,
|
||||
override val description: String?,
|
||||
override val canBeReplaced: Boolean,
|
||||
override val isRedacted: Boolean,
|
||||
override val isUpdated: Boolean,
|
||||
val roomName: String?,
|
||||
val senderId: UserId,
|
||||
val senderDisambiguatedDisplayName: String?,
|
||||
val senderAvatarUrl: String?,
|
||||
val roomAvatarUrl: String? = null,
|
||||
val callNotifyType: CallNotifyType,
|
||||
val timestamp: Long,
|
||||
) : NotifiableEvent {
|
||||
companion object {
|
||||
fun shouldRing(callNotifyType: CallNotifyType, timestamp: Long): Boolean {
|
||||
val timeout = 10.seconds.inWholeMilliseconds
|
||||
val elapsed = Instant.now().toEpochMilli() - timestamp
|
||||
// Only ring if the type is RING and the elapsed time is less than the timeout
|
||||
return callNotifyType == CallNotifyType.RING && elapsed < timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,11 +17,15 @@
|
|||
package io.element.android.libraries.push.impl.push
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
||||
import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
|
@ -44,6 +48,8 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val buildMeta: BuildMeta,
|
||||
private val matrixAuthenticationService: MatrixAuthenticationService,
|
||||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
) : PushHandler {
|
||||
/**
|
||||
* Called when message is received.
|
||||
|
|
@ -91,19 +97,33 @@ class DefaultPushHandler @Inject constructor(
|
|||
return
|
||||
}
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
if (userPushStore.getNotificationEnabledForDevice().first()) {
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
if (notifiableEvent == null) {
|
||||
Timber.w("Unable to get a notification data")
|
||||
return
|
||||
when (notifiableEvent) {
|
||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||
is NotifiableRingingCallEvent -> handleRingingCallEvent(notifiableEvent)
|
||||
else -> onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
}
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
// TODO We need to check if this is an incoming call
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
|
||||
Timber.i("## handleInternal() : Incoming call.")
|
||||
elementCallEntryPoint.handleIncomingCall(
|
||||
callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId),
|
||||
eventId = notifiableEvent.eventId,
|
||||
senderId = notifiableEvent.senderId,
|
||||
roomName = notifiableEvent.roomName,
|
||||
senderName = notifiableEvent.senderDisambiguatedDisplayName,
|
||||
avatarUrl = notifiableEvent.roomAvatarUrl,
|
||||
timestamp = notifiableEvent.timestamp,
|
||||
notificationChannelId = notificationChannels.getChannelForIncomingCall(ring = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="notification_channel_call">"Call"</string>
|
||||
<string name="notification_channel_listening_for_events">"Listening for events"</string>
|
||||
<string name="notification_channel_noisy">"Noisy notifications"</string>
|
||||
<string name="notification_channel_ringing_calls">"Ringing calls"</string>
|
||||
<string name="notification_channel_silent">"Silent notifications"</string>
|
||||
<plurals name="notification_compat_summary_line_for_room">
|
||||
<item quantity="one">"%1$s: %2$d message"</item>
|
||||
|
|
@ -13,6 +14,7 @@
|
|||
<item quantity="other">"%d notifications"</item>
|
||||
</plurals>
|
||||
<string name="notification_fallback_content">"Notification"</string>
|
||||
<string name="notification_incoming_call">"Incoming call"</string>
|
||||
<string name="notification_inline_reply_failed">"** Failed to send - please open room"</string>
|
||||
<string name="notification_invitation_action_join">"Join"</string>
|
||||
<string name="notification_invitation_action_reject">"Reject"</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue