diff --git a/library/external/textdrawable/build.gradle b/library/external/textdrawable/build.gradle index 5eb27bf6aa3..dcaf2d76ccd 100644 --- a/library/external/textdrawable/build.gradle +++ b/library/external/textdrawable/build.gradle @@ -1,7 +1,5 @@ apply plugin: 'com.android.library' -apply plugin: 'com.android.library' - android { namespace "com.amulyakhare.textdrawable" diff --git a/library/ui-strings/src/main/res/values-ru/strings_sc.xml b/library/ui-strings/src/main/res/values-ru/strings_sc.xml index d14411c18f5..4e510848b74 100644 --- a/library/ui-strings/src/main/res/values-ru/strings_sc.xml +++ b/library/ui-strings/src/main/res/values-ru/strings_sc.xml @@ -180,4 +180,4 @@ Очистить выделение при прокрутке Разрешить резервный сервер звонков ⚠️ Эта настройка по умолчанию (если не изменена конфигурацией Вашего домашнего сервера) включает доступ к \"Scalar\", менеджеру интеграций от Element. К сожалению, он является проприетарным, т.е. его исходый код не открытый и не может быть проверен пользователями или разработчиками SchildiChat. - \ No newline at end of file + diff --git a/library/ui-strings/src/main/res/values/strings_sc.xml b/library/ui-strings/src/main/res/values/strings_sc.xml index 148d267d8cf..9adfff72804 100644 --- a/library/ui-strings/src/main/res/values/strings_sc.xml +++ b/library/ui-strings/src/main/res/values/strings_sc.xml @@ -239,6 +239,8 @@ Will use %s as assist when your homeserver does not offer one (your IP address will be seen by the stun server during a call ) + Ring for group calls + Open App ⚠️ This setting by default (unless overridden by your homeserver\'s configuration) enables access to \"scalar\", Element\'s integration manager which is unfortunately proprietary, i.e. its source code is not open and can not be checked by the public or the SchildiChat developers. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 196b419598f..053af048151 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -23,7 +23,9 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult +import org.matrix.android.sdk.api.session.events.model.EventType.IS_JITSI_CALL import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.room.model.JitsiEventContent import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent @@ -498,6 +500,9 @@ fun Event.getPollContent(): MessagePollContent? { return getClearContent().toModel() } +fun Event.isJitsiEvent() = content?.toModel()?.type?.lowercase() == IS_JITSI_CALL && + content.toModel()?.name?.lowercase() == IS_JITSI_CALL + fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START.values + EventType.POLL_END.values + EventType.STATE_ROOM_BEACON_INFO.values diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 170254078f7..94e336679ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -108,6 +108,9 @@ object EventType { // Relation Events const val REACTION = "m.reaction" + // Jitsi call + const val IS_JITSI_CALL = "jitsi" + // Poll val POLL_START = StableUnstableId(stable = "m.poll.start", unstable = "org.matrix.msc3381.poll.start") val POLL_RESPONSE = StableUnstableId(stable = "m.poll.response", unstable = "org.matrix.msc3381.poll.response") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/JitsiEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/JitsiEventContent.kt new file mode 100644 index 00000000000..956c343ac96 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/JitsiEventContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.room.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the Jitsi call state event. + */ +@JsonClass(generateAdapter = true) +data class JitsiEventContent( + @Json(name = "type") val type: String? = null, + @Json(name = "name") val name: String? = null, +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt index d000d709a92..f9f701548f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushrules/ProcessEventForPushTask.kt @@ -64,6 +64,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor( EventType.MESSAGE, EventType.REDACTION, EventType.ENCRYPTED, + EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_MEMBER -> true else -> false } diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index ffaf462244d..c71c16c052e 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -79,6 +79,9 @@ class DebugVectorFeatures( override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) ?: vectorFeatures.isVoiceBroadcastEnabled() + override fun isJitsiCallNotificationEnabled(): Boolean = read(DebugFeatureKeys.jitsiCallNotificationsEnabled) + ?: vectorFeatures.isJitsiCallNotificationEnabled() + override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled) ?: vectorFeatures.isUnverifiedSessionsAlertEnabled() @@ -143,4 +146,5 @@ object DebugFeatureKeys { val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") + val jitsiCallNotificationsEnabled = booleanPreferencesKey("jitsi-call-notifications-enabled") } diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index c12af1f568d..6d83f32a047 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -51,6 +51,7 @@ false true false + false diff --git a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt index acf22504498..a99523017c3 100644 --- a/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt +++ b/vector/src/main/java/im/vector/app/core/di/SingletonEntryPoint.kt @@ -25,6 +25,7 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator +import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.pin.PinLocker import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.session.SessionListener @@ -41,6 +42,8 @@ interface SingletonEntryPoint { fun avatarRenderer(): AvatarRenderer + fun notificationUtils(): NotificationUtils + fun activeSessionHolder(): ActiveSessionHolder fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index 1fa2a68941c..e785f6b5ef1 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -42,6 +42,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.location.live.map.LiveLocationMapViewActivity import im.vector.app.features.notifications.NotificationDrawerManager +import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.pin.UnlockedActivity import im.vector.app.features.pin.lockscreen.crypto.LockScreenKeyRepository import im.vector.app.features.pin.lockscreen.pincode.PinCodeHelper @@ -88,7 +89,9 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity private const val EXTRA_NEXT_INTENT = "EXTRA_NEXT_INTENT" private const val EXTRA_INIT_SESSION = "EXTRA_INIT_SESSION" private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID" + private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" private const val ACTION_ROOM_DETAILS_FROM_SHORTCUT = "ROOM_DETAILS_FROM_SHORTCUT" + private const val ACTION_ROOM_DETAILS_JITSI_CALL = "ROOM_DETAILS_JITSI_CALL" // Special action to clear cache and/or clear credentials fun restartApp(activity: Activity, args: MainActivityArgs) { @@ -119,6 +122,20 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity } } + fun jitsiCallIntent( + context: Context, + roomId: String, + callId: String, + ): Intent { + return Intent(context, MainActivity::class.java).apply { + action = ACTION_ROOM_DETAILS_JITSI_CALL + flags = Intent.FLAG_ACTIVITY_NEW_TASK + + putExtra(EXTRA_ROOM_ID, roomId) + putExtra(EXTRA_CALL_ID, callId) + } + } + val allowList = listOf( HomeActivity::class.java.name, MainActivity::class.java.name, @@ -137,6 +154,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity private lateinit var args: MainActivityArgs @Inject lateinit var notificationDrawerManager: NotificationDrawerManager + @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var uiStateRepository: UiStateRepository @Inject lateinit var shortcutsHandler: ShortcutsHandler @Inject lateinit var pinCodeHelper: PinCodeHelper @@ -204,6 +222,17 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity setResult(RESULT_OK) finish() } else if (intent.action == ACTION_ROOM_DETAILS_FROM_SHORTCUT) { + startSyncing() + val roomId = intent.getStringExtra(EXTRA_ROOM_ID) + if (roomId?.isNotEmpty() == true) { + navigator.openRoom(this, roomId, trigger = ViewRoom.Trigger.Shortcut) + } + finish() + } else if (intent.action == ACTION_ROOM_DETAILS_JITSI_CALL) { + val callId = intent.getStringExtra(EXTRA_CALL_ID).orEmpty() + + notificationUtils.cancelNotificationMessage(callId, NotificationDrawerManager.JITSI_CALL_NOTIFICATION_ID) + startSyncing() val roomId = intent.getStringExtra(EXTRA_ROOM_ID) if (roomId?.isNotEmpty() == true) { diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 65c1d036554..f1b9c4007dc 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -41,6 +41,7 @@ interface VectorFeatures { */ fun isNewAppLayoutFeatureEnabled(): Boolean fun isVoiceBroadcastEnabled(): Boolean + fun isJitsiCallNotificationEnabled(): Boolean fun isUnverifiedSessionsAlertEnabled(): Boolean } @@ -58,5 +59,6 @@ class DefaultVectorFeatures : VectorFeatures { override fun forceUsageOfOpusEncoder(): Boolean = false override fun isNewAppLayoutFeatureEnabled(): Boolean = true override fun isVoiceBroadcastEnabled(): Boolean = true + override fun isJitsiCallNotificationEnabled(): Boolean = true override fun isUnverifiedSessionsAlertEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt index 161aa33d1df..ef28c258a53 100644 --- a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt @@ -20,7 +20,9 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import im.vector.app.core.extensions.singletonEntryPoint +import im.vector.app.core.services.CallAndroidService import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.notifications.NotificationDrawerManager import timber.log.Timber class CallHeadsUpActionReceiver : BroadcastReceiver() { @@ -33,9 +35,11 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { val webRtcCallManager = context.singletonEntryPoint().webRtcCallManager() + val notificationUtils = context.singletonEntryPoint().notificationUtils() when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { CALL_ACTION_REJECT -> { val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return + notificationUtils.cancelNotificationMessage(callId, NotificationDrawerManager.JITSI_CALL_NOTIFICATION_ID) onCallRejectClicked(webRtcCallManager, callId) } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt index 81b9844e36e..b63ec903a20 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventProcessor.kt @@ -41,17 +41,29 @@ class NotifiableEventProcessor @Inject constructor( .also { Timber.d("notification message removed due to being read") } else -> KEEP } + is NotifiableJitsiEvent -> { + if (it.isReceived != true) { + KEEP + } else { + REMOVE + } + } is SimpleNotifiableEvent -> when (it.type) { EventType.REDACTION -> REMOVE else -> KEEP } } - ProcessedEvent(type, it) + + val updatedEvent = if (it is NotifiableJitsiEvent) it.updateReceivedStatus() else it + ProcessedEvent(type, updatedEvent) } val removedEventsDiff = renderedEvents.filter { renderedEvent -> queuedEvents.none { it.eventId == renderedEvent.event.eventId } - }.map { ProcessedEvent(REMOVE, it.event) } + }.map { + val updatedEvent = if (it.event is NotifiableJitsiEvent) it.event.updateReceivedStatus() else it.event + ProcessedEvent(REMOVE, updatedEvent) + } return removedEventsDiff + processedEvents } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt index 70ab94e8d55..e93411bc233 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isJitsiEvent import org.matrix.android.sdk.api.session.events.model.supportsNotification import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom @@ -80,6 +81,9 @@ class NotifiableEventResolver @Inject constructor( event.supportsNotification() || event.type == EventType.ENCRYPTED -> { resolveMessageEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) } + event.isJitsiEvent() -> { + resolveJitsiEvent(timelineEvent, session, canBeReplaced = false, isNoisy = isNoisy) + } else -> { // If the event can be displayed, display it as is Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule") @@ -133,6 +137,51 @@ class NotifiableEventResolver @Inject constructor( } } + private fun resolveJitsiEvent(event: TimelineEvent, session: Session, canBeReplaced: Boolean, isNoisy: Boolean): NotifiableJitsiEvent?{ + // The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...) + val room = session.getRoom(event.root.roomId!! /*roomID cannot be null*/) + + return if (room != null) { + val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() + val roomName = room.roomSummary()?.displayName.orEmpty() + val senderDisplayName = event.senderInfo.disambiguatedDisplayName + + NotifiableJitsiEvent( + eventId = event.root.eventId.orEmpty(), + editedEventId = event.getEditedEventId(), + canBeReplaced = canBeReplaced, + timestamp = event.root.originServerTs ?: 0, + noisy = isNoisy, + senderName = senderDisplayName, + senderId = event.root.senderId, + body = body, + roomId = event.root.roomId!!, + threadId = event.root.getRootThreadEventId(), + roomName = roomName, + roomIsDirect = room.roomSummary()?.isDirect ?: false, + roomAvatarPath = session.contentUrlResolver() + .resolveThumbnail( + room.roomSummary()?.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE + ), + senderAvatarPath = session.contentUrlResolver() + .resolveThumbnail( + event.senderInfo.avatarUrl, + 250, + 250, + ContentUrlResolver.ThumbnailMethod.SCALE + ), + matrixID = session.myUserId, + soundName = null, + isReceived = null, + ) + } else { + null + } + } + fun TimelineEvent.getCaption(): String? = (getLastMessageContent() as? MessageWithAttachmentContent)?.getCaption() fun TimelineEvent.getFilename(): String? = (getLastMessageContent() as? MessageWithAttachmentContent)?.getFileName() diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableJitsiEvent.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableJitsiEvent.kt new file mode 100644 index 00000000000..ca43d31f319 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableJitsiEvent.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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 im.vector.app.features.notifications + +import org.matrix.android.sdk.api.session.events.model.EventType + +data class NotifiableJitsiEvent( + override val eventId: String, + override val editedEventId: String?, + override val canBeReplaced: Boolean, + val noisy: Boolean, + val timestamp: Long, + val senderName: String?, + val senderId: String?, + val body: String?, + val roomId: String, + val threadId: String?, + val roomName: String?, + val roomIsDirect: Boolean = false, + val roomAvatarPath: String? = null, + val senderAvatarPath: String? = null, + val matrixID: String? = null, + val soundName: String? = null, + // This is used for >N notification, as the result of a smart reply + val outGoingMessage: Boolean = false, + val outGoingMessageFailed: Boolean = false, + var isReceived: Boolean? = null, + override val isRedacted: Boolean = false, + override val isUpdated: Boolean = false +) : NotifiableEvent { + + val type: String = EventType.MESSAGE + val description: String = body ?: "" + val title: String = senderName ?: "" + + fun updateReceivedStatus() = this.copy( + isReceived = when (isReceived) { + null -> false + false -> true + true -> true + } + ) +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 247674dbe48..f37cfb05e81 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -240,5 +240,6 @@ class NotificationDrawerManager @Inject constructor( const val ROOM_MESSAGES_NOTIFICATION_ID = 1 const val ROOM_EVENT_NOTIFICATION_ID = 2 const val ROOM_INVITATION_NOTIFICATION_ID = 3 + const val JITSI_CALL_NOTIFICATION_ID = 4 } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt index 8aff9c3bf26..4478a523379 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationEventQueue.kt @@ -36,6 +36,7 @@ data class NotificationEventQueue( is InviteNotifiableEvent -> it.copy(isRedacted = true) is NotifiableMessageEvent -> it.copy(isRedacted = true) is SimpleNotifiableEvent -> it.copy(isRedacted = true) + is NotifiableJitsiEvent -> it.copy(isRedacted = true) } } } @@ -117,6 +118,7 @@ data class NotificationEventQueue( is InviteNotifiableEvent -> with.copy(isUpdated = true) is NotifiableMessageEvent -> with.copy(isUpdated = true) is SimpleNotifiableEvent -> with.copy(isUpdated = true) + is NotifiableJitsiEvent -> with.copy(isUpdated = true) } ) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt index 7292f650091..5bef05bf75d 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationFactory.kt @@ -20,6 +20,7 @@ import android.app.Notification import javax.inject.Inject private typealias ProcessedMessageEvents = List> +private typealias ProcessedJitsiEvents = List> class NotificationFactory @Inject constructor( private val notificationUtils: NotificationUtils, @@ -27,6 +28,28 @@ class NotificationFactory @Inject constructor( private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { + fun Map.toNotifications(): List { + return map { (roomId, events) -> + if (events.all { it.event.isReceived == true }) { + return emptyList() + } + + val eventToShow = events.first { it.event.isReceived == false } + + JitsiNotification.IncomingCall( + roomId = roomId, + eventId = eventToShow.event.eventId, + roomName = eventToShow.event.roomName.orEmpty(), + notification = notificationUtils.buildIncomingJitsiCallNotification( + callId = eventToShow.event.eventId.ifEmpty { roomId }, + signalingRoomId = roomId, + title = eventToShow.event.roomName.orEmpty(), + fromBg = true, + ) + ) + } + } + fun Map.toNotifications(myUserDisplayName: String, myUserAvatarUrl: String?): List { return map { (roomId, events) -> when { @@ -117,6 +140,15 @@ sealed interface RoomNotification { } } +sealed interface JitsiNotification { + data class IncomingCall( + val roomId: String, + val eventId: String, + val roomName: String, + val notification: Notification, + ) : JitsiNotification +} + sealed interface OneShotNotification { data class Removed(val key: String) : OneShotNotification data class Append(val notification: Notification, val meta: Meta) : OneShotNotification { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt index ab59b2e6d80..098d173413f 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationRenderer.kt @@ -15,19 +15,20 @@ */ package im.vector.app.features.notifications -import android.content.Context import androidx.annotation.WorkerThread +import im.vector.app.features.notifications.NotificationDrawerManager.Companion.JITSI_CALL_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_EVENT_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_INVITATION_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.ROOM_MESSAGES_NOTIFICATION_ID import im.vector.app.features.notifications.NotificationDrawerManager.Companion.SUMMARY_NOTIFICATION_ID +import im.vector.app.features.settings.VectorPreferences import timber.log.Timber import javax.inject.Inject class NotificationRenderer @Inject constructor( private val notificationDisplayer: NotificationDisplayer, private val notificationFactory: NotificationFactory, - private val appContext: Context + private val vectorPreferences: VectorPreferences, ) { @WorkerThread @@ -38,9 +39,10 @@ class NotificationRenderer @Inject constructor( useCompleteNotificationFormat: Boolean, eventsToProcess: List> ) { - val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() + val (roomEvents, simpleEvents, invitationEvents, jitsiEvents) = eventsToProcess.groupByType() with(notificationFactory) { val roomNotifications = roomEvents.toNotifications(myUserDisplayName, myUserAvatarUrl) + val jitsiNotifications = jitsiEvents.toNotifications() val invitationNotifications = invitationEvents.toNotifications(myUserId) val simpleNotifications = simpleEvents.toNotifications(myUserId) val summaryNotification = createSummaryNotification( @@ -69,6 +71,21 @@ class NotificationRenderer @Inject constructor( } } + Timber.d("Jitsi call notifications count = ${jitsiNotifications.size}") + if (vectorPreferences.isJitsiCallNotificationEnabled()) { + jitsiNotifications.forEach { wrapper -> + when (wrapper) { + is JitsiNotification.IncomingCall -> { + Timber.d("Updating jitsi call notification ${wrapper.eventId} for room ${wrapper.roomName}") + if (wrapper.eventId.isNotEmpty() || wrapper.roomId.isNotEmpty()) { + val tag = wrapper.eventId.ifEmpty { wrapper.roomId } + notificationDisplayer.showNotificationMessage(tag, JITSI_CALL_NOTIFICATION_ID, wrapper.notification) + } + } + } + } + } + invitationNotifications.forEach { wrapper -> when (wrapper) { is OneShotNotification.Removed -> { @@ -108,6 +125,7 @@ private fun List>.groupByType(): GroupedNotifica val roomIdToEventMap: MutableMap>> = LinkedHashMap() val simpleEvents: MutableList> = ArrayList() val invitationEvents: MutableList> = ArrayList() + val roomIdToJitsiEventMap: MutableMap>> = LinkedHashMap() forEach { when (val event = it.event) { is InviteNotifiableEvent -> invitationEvents.add(it.castedToEventType()) @@ -115,10 +133,14 @@ private fun List>.groupByType(): GroupedNotifica val roomEvents = roomIdToEventMap.getOrPut(event.roomId) { ArrayList() } roomEvents.add(it.castedToEventType()) } + is NotifiableJitsiEvent -> { + val jitsiEvents = roomIdToJitsiEventMap.getOrPut(event.roomId) { ArrayList() } + jitsiEvents.add(it.castedToEventType()) + } is SimpleNotifiableEvent -> simpleEvents.add(it.castedToEventType()) } } - return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents) + return GroupedNotificationEvents(roomIdToEventMap, simpleEvents, invitationEvents, roomIdToJitsiEventMap) } @Suppress("UNCHECKED_CAST") @@ -127,5 +149,6 @@ private fun ProcessedEvent.castedToEventT data class GroupedNotificationEvents( val roomEvents: Map>>, val simpleEvents: List>, - val invitationEvents: List> + val invitationEvents: List>, + val jitsiEvents: Map>>, ) diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index d04571835c7..564ee90b69c 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -366,6 +366,79 @@ class NotificationUtils @Inject constructor( return builder.build() } + /** + * Build an incoming jitsi call notification. + * This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow. + * + * @param callId id of the jitsi call + * @param signalingRoomId id of the room + * @param title title of the notification + * @param fromBg true if the app is in background when posting the notification + * @return the call notification. + */ + fun buildIncomingJitsiCallNotification( + callId: String, + signalingRoomId: String, + title: String, + fromBg: Boolean, + ): Notification { + val accentColor = ContextCompat.getColor(context, im.vector.lib.ui.styles.R.color.notification_accent_color) + val notificationChannel = if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID + val builder = NotificationCompat.Builder(context, notificationChannel) + .setContentTitle(ensureTitleNotEmpty(title)) + .apply { + setContentText(stringProvider.getString(CommonStrings.incoming_video_call)) + setSmallIcon(R.drawable.ic_call_answer_video) + } + .setCategory(NotificationCompat.CATEGORY_CALL) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) + .setLights(accentColor, 500, 500) + .setOngoing(true) + + val contentIntent = MainActivity.jitsiCallIntent( + context = context, + roomId = signalingRoomId, + callId = callId, + ) + + val contentPendingIntent = PendingIntent.getActivity( + context, + clock.epochMillis().toInt(), + contentIntent, + PendingIntentCompat.FLAG_IMMUTABLE + ) + + val answerCallPendingIntent = TaskStackBuilder.create(context) + .addNextIntentWithParentStack(HomeActivity.newIntent(context, firstStartMainActivity = false)) + .addNextIntent(contentIntent) + .getPendingIntent(clock.epochMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE) + + val rejectCallPendingIntent = buildRejectCallPendingIntent(callId) + + builder.addAction( + NotificationCompat.Action( + IconCompat.createWithResource(context, R.drawable.ic_call_hangup) + .setTint(ThemeUtils.getColor(context, android.R.attr.colorError)), + getActionText(CommonStrings.call_notification_reject, android.R.attr.colorError), + rejectCallPendingIntent + ) + ) + + builder.addAction( + NotificationCompat.Action( + R.drawable.ic_call_answer, + getActionText(CommonStrings.call_notification_open_app_action, android.R.attr.colorPrimary), + answerCallPendingIntent + ) + ) + if (fromBg) { + // Compat: Display the incoming call notification on the lock screen + builder.priority = NotificationCompat.PRIORITY_HIGH + builder.setFullScreenIntent(contentPendingIntent, true) + } + return builder.build() + } + fun buildOutgoingRingingCallNotification( call: WebRtcCall, title: String diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 627c0fd403c..5149bedd02f 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -85,6 +85,7 @@ class VectorPreferences @Inject constructor( const val SETTINGS_LABS_NEW_SESSION_MANAGER_KEY = "SETTINGS_LABS_NEW_SESSION_MANAGER_KEY" const val SETTINGS_LABS_CLIENT_INFO_RECORDING_KEY = "SETTINGS_LABS_CLIENT_INFO_RECORDING_KEY" const val SETTINGS_LABS_VOICE_BROADCAST_KEY = "SETTINGS_LABS_VOICE_BROADCAST_KEY" + const val SETTINGS_LABS_JITSI_CALL_NOTIFICATION_KEY = "SETTINGS_LABS_JITSI_CALL_NOTIFICATION_KEY" const val SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" @@ -1597,6 +1598,11 @@ class VectorPreferences @Inject constructor( ) } + fun isJitsiCallNotificationEnabled(): Boolean { + return vectorFeatures.isJitsiCallNotificationEnabled() && + defaultPrefs.getBoolean(SETTINGS_LABS_JITSI_CALL_NOTIFICATION_KEY, getDefault(im.vector.app.config.R.bool.settings_labs_enable_jitsi_call_notifications_default)) + } + fun showIpAddressInSessionManagerScreens(): Boolean { return defaultPrefs.getBoolean( SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 0b2c6f67c7e..752a41aa376 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -202,5 +202,11 @@ android:title="@string/labs_enable_voice_broadcast_title" app:isPreferenceVisible="@bool/settings_labs_enable_voice_broadcast_visible" /> + +