diff --git a/changelog.d/3710.feature b/changelog.d/3710.feature new file mode 100644 index 0000000000..74134d7ee5 --- /dev/null +++ b/changelog.d/3710.feature @@ -0,0 +1 @@ +Show missed call notification. \ No newline at end of file diff --git a/changelog.d/3713.removal b/changelog.d/3713.removal new file mode 100644 index 0000000000..e797a408e7 --- /dev/null +++ b/changelog.d/3713.removal @@ -0,0 +1 @@ +Add initialState support to CreateRoomParams (#3713) \ No newline at end of file diff --git a/changelog.d/3720.bugfix b/changelog.d/3720.bugfix new file mode 100644 index 0000000000..18363b90e3 --- /dev/null +++ b/changelog.d/3720.bugfix @@ -0,0 +1 @@ +Fix a crash which can happen when user signs out \ No newline at end of file diff --git a/changelog.d/3721.misc b/changelog.d/3721.misc new file mode 100644 index 0000000000..8c3424c6ed --- /dev/null +++ b/changelog.d/3721.misc @@ -0,0 +1 @@ +Apply grammatical fixes to the Server ACL timeline messages. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt index 2dbd1c9b01..47a63b4a25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.call +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason + sealed class CallState { /** Idle, setting up objects */ @@ -42,6 +44,6 @@ sealed class CallState { * */ data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState() - /** Terminated. Incoming/Outgoing call, the call is terminated */ - object Terminated : CallState() + /** Ended. Incoming/Outgoing call, the call is terminated */ + data class Ended(val reason: EndCallReason? = null) : CallState() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index fcc9f7072d..dd23e81cc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.call import org.matrix.android.sdk.api.session.room.model.call.CallCandidate import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities -import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional @@ -69,7 +69,7 @@ interface MxCall : MxCallDetail { /** * End the call */ - fun hangUp(reason: CallHangupContent.Reason? = null) + fun hangUp(reason: EndCallReason? = null) /** * Start a call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index 9d6e1a7eae..31f801dd6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -43,29 +43,5 @@ data class CallHangupContent( * or `invite_timeout` for when the other party did not answer in time. * One of: ["ice_failed", "invite_timeout"] */ - @Json(name = "reason") val reason: Reason? = null -) : CallSignalingContent { - @JsonClass(generateAdapter = false) - enum class Reason { - @Json(name = "ice_failed") - ICE_FAILED, - - @Json(name = "ice_timeout") - ICE_TIMEOUT, - - @Json(name = "user_hangup") - USER_HANGUP, - - @Json(name = "replaced") - REPLACED, - - @Json(name = "user_media_failed") - USER_MEDIA_FAILED, - - @Json(name = "invite_timeout") - INVITE_TIMEOUT, - - @Json(name = "unknown_error") - UNKWOWN_ERROR - } -} + @Json(name = "reason") val reason: EndCallReason? = null +) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt index ea412fbe3e..1b9a7186e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt @@ -36,5 +36,10 @@ data class CallRejectContent( /** * Required. The version of the VoIP specification this message adheres to. */ - @Json(name = "version") override val version: String? + @Json(name = "version") override val version: String?, + + /** + * Optional error reason for the reject. + */ + @Json(name = "reason") val reason: EndCallReason? = null ) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt new file mode 100644 index 0000000000..60e038b2f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 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.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class EndCallReason { + @Json(name = "ice_failed") + ICE_FAILED, + + @Json(name = "ice_timeout") + ICE_TIMEOUT, + + @Json(name = "user_hangup") + USER_HANGUP, + + @Json(name = "replaced") + REPLACED, + + @Json(name = "user_media_failed") + USER_MEDIA_FAILED, + + @Json(name = "invite_timeout") + INVITE_TIMEOUT, + + @Json(name = "unknown_error") + UNKWOWN_ERROR, + + @Json(name = "user_busy") + USER_BUSY, + + @Json(name = "answered_elsewhere") + ANSWERED_ELSEWHERE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index ca8c66bb3b..c46d7d0fd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM -// TODO Give a way to include other initial states open class CreateRoomParams { /** * A public visibility indicates that the room will be shown in the published room list. @@ -103,6 +102,13 @@ open class CreateRoomParams { */ val creationContent = mutableMapOf() + /** + * A list of state events to set in the new room. This allows the user to override the default state events + * set in the new room. The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by preset, but gets overridden by name and topic keys. + */ + val initialStates = mutableListOf() + /** * Set to true to disable federation of this room. * Default: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt new file mode 100644 index 0000000000..fcfdc3e333 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 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.create + +import org.matrix.android.sdk.api.session.events.model.Content + +data class CreateRoomStateEvent( + /** + * Required. The type of event to send. + */ + val type: String, + + /** + * Required. The content of the event. + */ + val content: Content, + + /** + * The state_key of the state event. Defaults to an empty string. + */ + val stateKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 63f15aaf6e..79910c6de2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.auth.data.Credentials @@ -336,7 +337,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") - onKeysDownloadFailed(filteredUsers) + if (throwable is CancellationException) { + // the crypto module is getting closed, so we cannot access the DB anymore + Timber.w("The crypto module is closed, ignoring this error") + } else { + onKeysDownloadFailed(filteredUsers) + } throw throwable } Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 17e9aa9b7f..a71fdeae03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -169,7 +169,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa Timber.tag(loggerTag.value).v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}") return } - if (call.state != CallState.Terminated) { + if (call.state !is CallState.Ended) { activeCallHandler.removeCall(content.callId) callListenersDispatcher.onCallHangupReceived(content) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 1d831afcae..9fc84e6fe5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService @@ -145,7 +146,7 @@ internal class MxCallImpl( override fun reject() { if (opponentVersion < 1) { Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject") - hangUp() + hangUp(EndCallReason.USER_HANGUP) return } Timber.tag(loggerTag.value).v("reject $callId") @@ -156,20 +157,20 @@ internal class MxCallImpl( ) .let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } - state = CallState.Terminated + state = CallState.Ended(reason = EndCallReason.USER_HANGUP) } - override fun hangUp(reason: CallHangupContent.Reason?) { + override fun hangUp(reason: EndCallReason?) { Timber.tag(loggerTag.value).v("hangup $callId") CallHangupContent( callId = callId, partyId = ourPartyId, - reason = reason ?: CallHangupContent.Reason.USER_HANGUP, + reason = reason, version = MxCall.VOIP_PROTO_VERSION.toString() ) .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } - state = CallState.Terminated + state = CallState.Ended(reason) } override fun accept(sdpString: String) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 86d2b70da1..2c04759b22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -81,13 +81,14 @@ internal class CreateRoomBodyBuilder @Inject constructor( params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden } - val initialStates = listOfNotNull( + val initialStates = (listOfNotNull( buildEncryptionWithAlgorithmEvent(params), buildHistoryVisibilityEvent(params), buildAvatarEvent(params), buildGuestAccess(params), buildJoinRulesRestricted(params) ) + + buildCustomInitialStates(params)) .takeIf { it.isNotEmpty() } return CreateRoomBody( @@ -95,7 +96,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( roomAliasName = params.roomAliasName, name = params.name, topic = params.topic, - invitedUserIds = params.invitedUserIds.filter { it != userId }, + invitedUserIds = params.invitedUserIds.filter { it != userId }.takeIf { it.isNotEmpty() }, invite3pids = invite3pids, creationContent = params.creationContent.takeIf { it.isNotEmpty() }, initialStates = initialStates, @@ -103,10 +104,19 @@ internal class CreateRoomBodyBuilder @Inject constructor( isDirect = params.isDirect, powerLevelContentOverride = params.powerLevelContentOverride, roomVersion = params.roomVersion - ) } + private fun buildCustomInitialStates(params: CreateRoomParams): List { + return params.initialStates.map { + Event( + type = it.type, + stateKey = it.stateKey, + content = it.content + ) + } + } + private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? { return params.avatarUri?.let { avatarUri -> // First upload the image, ignoring any error diff --git a/multipicker/build.gradle b/multipicker/build.gradle index a993c452b0..a8a46a86ba 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -43,7 +43,7 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.3.0' - implementation "androidx.fragment:fragment-ktx:1.3.5" + implementation "androidx.fragment:fragment-ktx:1.3.6" implementation 'androidx.exifinterface:exifinterface:1.3.2' // Log diff --git a/vector/build.gradle b/vector/build.gradle index 0f21723a87..8d5125a323 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -305,7 +305,7 @@ android { dependencies { def epoxy_version = '4.6.2' - def fragment_version = '1.3.5' + def fragment_version = '1.3.6' def arrow_version = "0.8.2" def markwon_version = '4.1.2' def big_image_viewer_version = '1.8.0' @@ -342,7 +342,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation "androidx.sharetarget:sharetarget:1.1.0" implementation 'androidx.core:core-ktx:1.6.0' - implementation "androidx.media:media:1.3.1" + implementation "androidx.media:media:1.4.0" implementation "androidx.transition:transition:1.4.1" implementation "org.threeten:threetenbp:1.4.0:no-tzdb" diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 23a57e459e..916dc66718 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -38,6 +38,7 @@ import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.popup.IncomingCallAlert import im.vector.app.features.popup.PopupAlertManager import org.matrix.android.sdk.api.logger.LoggerTag +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.util.MatrixItem import timber.log.Timber @@ -49,7 +50,8 @@ private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP) class CallService : VectorService() { private val connections = mutableMapOf() - private val knownCalls = mutableSetOf() + private val knownCalls = mutableSetOf() + private val connectedCallIds = mutableSetOf() private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationUtils: NotificationUtils @@ -156,9 +158,9 @@ class CallService : VectorService() { val call = callManager.getCallById(callId) ?: return Unit.also { handleUnexpectedState(callId) } + val callInformation = call.toCallInformation() val isVideoCall = call.mxCall.isVideoCall val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) - val opponentMatrixItem = getOpponentMatrixItem(call) Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification") val incomingCallAlert = IncomingCallAlert(callId, shouldBeDisplayedIn = { activity -> @@ -168,7 +170,7 @@ class CallService : VectorService() { } ).apply { viewBinder = IncomingCallAlert.ViewBinder( - matrixItem = opponentMatrixItem, + matrixItem = callInformation.opponentMatrixItem, avatarRenderer = avatarRenderer, isVideoCall = isVideoCall, onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) }, @@ -180,7 +182,7 @@ class CallService : VectorService() { alertManager.postVectorAlert(incomingCallAlert) val notification = notificationUtils.buildIncomingCallNotification( call = call, - title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, + title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId, fromBg = fromBg ) if (knownCalls.isEmpty()) { @@ -188,23 +190,34 @@ class CallService : VectorService() { } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callId) + knownCalls.add(callInformation) } private fun handleCallTerminated(intent: Intent) { val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason + val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false) alertManager.cancelAlert(callId) - if (!knownCalls.remove(callId)) { + val terminatedCall = knownCalls.firstOrNull { it.callId == callId } + if (terminatedCall == null) { Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$") handleUnexpectedState(callId) return } - val notification = notificationUtils.buildCallEndedNotification() - notificationManager.notify(callId.hashCode(), notification) + knownCalls.remove(terminatedCall) if (knownCalls.isEmpty()) { mediaSession?.isActive = false myStopSelf() } + val wasConnected = connectedCallIds.remove(callId) + if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) { + val notification = notificationUtils.buildCallMissedNotification(terminatedCall) + notificationManager.cancel(callId.hashCode()) + notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), notification) + } else { + val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall) + notificationManager.notify(callId.hashCode(), notification) + } } private fun showCallScreen(call: WebRtcCall, mode: String) { @@ -221,18 +234,18 @@ class CallService : VectorService() { val call = callManager.getCallById(callId) ?: return Unit.also { handleUnexpectedState(callId) } - val opponentMatrixItem = getOpponentMatrixItem(call) + val callInformation = call.toCallInformation() Timber.tag(loggerTag.value).v("displayOutgoingCallNotification : display the dedicated notification") val notification = notificationUtils.buildOutgoingRingingCallNotification( call = call, - title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId + title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { startForeground(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callId) + knownCalls.add(callInformation) } /** @@ -241,21 +254,22 @@ class CallService : VectorService() { private fun displayCallInProgressNotification(intent: Intent) { Timber.tag(loggerTag.value).v("displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + connectedCallIds.add(callId) val call = callManager.getCallById(callId) ?: return Unit.also { handleUnexpectedState(callId) } - val opponentMatrixItem = getOpponentMatrixItem(call) alertManager.cancelAlert(callId) + val callInformation = call.toCallInformation() val notification = notificationUtils.buildPendingCallNotification( call = call, - title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId + title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId ) if (knownCalls.isEmpty()) { startForeground(callId.hashCode(), notification) } else { notificationManager.notify(callId.hashCode(), notification) } - knownCalls.add(callId) + knownCalls.add(callInformation) } private fun handleUnexpectedState(callId: String?) { @@ -265,7 +279,7 @@ class CallService : VectorService() { if (callId != null) { notificationManager.cancel(callId.hashCode()) } - val notification = notificationUtils.buildCallEndedNotification() + val notification = notificationUtils.buildCallEndedNotification(false) startForeground(DEFAULT_NOTIFICATION_ID, notification) if (knownCalls.isEmpty()) { mediaSession?.isActive = false @@ -277,14 +291,31 @@ class CallService : VectorService() { connections[callConnection.callId] = callConnection } - private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? { - return vectorComponent().activeSessionHolder().getSafeActiveSession()?.let { - call.getOpponentAsMatrixItem(it) - } + private fun WebRtcCall.toCallInformation(): CallInformation { + return CallInformation( + callId = this.callId, + nativeRoomId = this.nativeRoomId, + opponentUserId = this.mxCall.opponentUserId, + opponentMatrixItem = vectorComponent().activeSessionHolder().getSafeActiveSession()?.let { + this.getOpponentAsMatrixItem(it) + }, + isVideoCall = this.mxCall.isVideoCall, + isOutgoing = this.mxCall.isOutgoing + ) } + data class CallInformation( + val callId: String, + val nativeRoomId: String, + val opponentUserId: String, + val opponentMatrixItem: MatrixItem?, + val isVideoCall: Boolean, + val isOutgoing: Boolean + ) + companion object { private const val DEFAULT_NOTIFICATION_ID = 6480 + private const val MISSED_CALL_TAG = "MISSED_CALL_TAG" private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL" private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" @@ -297,6 +328,8 @@ class CallService : VectorService() { private const val EXTRA_CALL_ID = "EXTRA_CALL_ID" private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG" + private const val EXTRA_END_CALL_REJECTED = "EXTRA_END_CALL_REJECTED" + private const val EXTRA_END_CALL_REASON = "EXTRA_END_CALL_REASON" fun onIncomingCallRinging(context: Context, callId: String, @@ -332,11 +365,13 @@ class CallService : VectorService() { ContextCompat.startForegroundService(context, intent) } - fun onCallTerminated(context: Context, callId: String) { + fun onCallTerminated(context: Context, callId: String, endCallReason: EndCallReason, rejected: Boolean) { val intent = Intent(context, CallService::class.java) .apply { action = ACTION_CALL_TERMINATED putExtra(EXTRA_CALL_ID, callId) + putExtra(EXTRA_END_CALL_REASON, endCallReason) + putExtra(EXTRA_END_CALL_REJECTED, rejected) } ContextCompat.startForegroundService(context, intent) } diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 1a54551072..3742de6271 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -118,7 +118,7 @@ class CallControlsView @JvmOverloads constructor( views.connectedControls.isVisible = false } } - is CallState.Terminated, + is CallState.Ended, null -> { views.ringingControls.isVisible = false views.connectedControls.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 7a22ac3b8e..a1e3717329 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -199,7 +199,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true @@ -249,10 +249,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Ended -> { finish() } - null -> { + null -> { } } } diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index 0f37ccaa29..0217551260 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -57,7 +57,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: private val call = callManager.getCallById(initialState.callId) private val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - if (call.state == CallState.Terminated) { + if (call.state is CallState.Ended) { _viewEvents.post(CallTransferViewEvents.Dismiss) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 6c55b9380d..91d3ab7ddf 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -58,6 +58,9 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.threeten.bp.Duration import org.webrtc.AudioSource @@ -102,7 +105,7 @@ class WebRtcCall( private val sessionProvider: Provider, private val peerConnectionFactoryProvider: Provider, private val onCallBecomeActive: (WebRtcCall) -> Unit, - private val onCallEnded: (String) -> Unit + private val onCallEnded: (String, EndCallReason, Boolean) -> Unit ) : MxCall.StateListener { interface Listener : MxCall.StateListener { @@ -230,7 +233,7 @@ class WebRtcCall( // Allow a short time for initial candidates to be gathered delay(200) } - if (mxCall.state == CallState.Terminated) { + if (mxCall.state is CallState.Ended) { return@launch } if (mxCall.state == CallState.CreateOffer) { @@ -288,7 +291,7 @@ class WebRtcCall( createCallId = CallIdGenerator.generate(), awaitCallId = null ) - endCall(sendEndSignaling = false) + terminate(EndCallReason.REPLACED) } } @@ -310,8 +313,8 @@ class WebRtcCall( createCallId = newCallId, awaitCallId = null ) - endCall(sendEndSignaling = false) - transferTargetCall.endCall(sendEndSignaling = false) + terminate(EndCallReason.REPLACED) + transferTargetCall.terminate(EndCallReason.REPLACED) } } @@ -464,7 +467,7 @@ class WebRtcCall( peerConnection?.awaitSetRemoteDescription(offerSdp) } catch (failure: Throwable) { Timber.tag(loggerTag.value).v("Failure putting remote description") - endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR) + endCall(reason = EndCallReason.UNKWOWN_ERROR) return@withContext } // 2) Access camera + microphone, create local stream @@ -770,7 +773,7 @@ class WebRtcCall( if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) { Timber.tag(loggerTag.value).e("StreamObserver weird looking stream: $stream") // TODO maybe do something more?? - endCall(true) + endCall(EndCallReason.UNKWOWN_ERROR) return@launch } if (stream.audioTracks.size == 1) { @@ -798,32 +801,34 @@ class WebRtcCall( } } - fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) { sessionScope?.launch(dispatcher) { - if (mxCall.state == CallState.Terminated) { + if (mxCall.state is CallState.Ended) { return@launch } - // Close tracks ASAP - localVideoTrack?.setEnabled(false) - localVideoTrack?.setEnabled(false) - cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> - val cameraManager = context.getSystemService()!! - cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) - } - val wasRinging = mxCall.state is CallState.LocalRinging - mxCall.state = CallState.Terminated - release() - onCallEnded(callId) - if (sendEndSignaling) { - if (wasRinging) { - mxCall.reject() - } else { - mxCall.hangUp(reason) - } + val reject = mxCall.state is CallState.LocalRinging + terminate(EndCallReason.USER_HANGUP, reject) + if (reject) { + mxCall.reject() + } else { + mxCall.hangUp(reason) } } } + private suspend fun terminate(reason: EndCallReason? = null, rejected: Boolean = false) = withContext(dispatcher) { + // Close tracks ASAP + localVideoTrack?.setEnabled(false) + localVideoTrack?.setEnabled(false) + cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> + val cameraManager = context.getSystemService()!! + cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) + } + mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP) + release() + onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected) + } + // Call listener fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { @@ -846,7 +851,7 @@ class WebRtcCall( try { peerConnection?.awaitSetRemoteDescription(sdp) } catch (failure: Throwable) { - endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR) + endCall(EndCallReason.UNKWOWN_ERROR) return@launch } if (mxCall.opponentPartyId?.hasValue().orFalse()) { @@ -907,6 +912,29 @@ class WebRtcCall( } } + fun onCallHangupReceived(callHangupContent: CallHangupContent) { + sessionScope?.launch(dispatcher) { + terminate(callHangupContent.reason) + } + } + + fun onCallRejectReceived(callRejectContent: CallRejectContent) { + sessionScope?.launch(dispatcher) { + terminate(callRejectContent.reason, true) + } + } + + fun onCallSelectedAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) { + sessionScope?.launch(dispatcher) { + val selectedPartyId = callSelectAnswerContent.selectedPartyId + if (selectedPartyId != mxCall.ourPartyId) { + Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${mxCall.ourPartyId}.") + // The other party has picked somebody else's answer + terminate() + } + } + } + fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { sessionScope?.launch(dispatcher) { val session = sessionProvider.get() ?: return@launch diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt index c99d097707..ef9ef3ef9a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt @@ -21,7 +21,12 @@ import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? { - return session.getRoomSummary(nativeRoomId)?.otherMemberIds?.firstOrNull()?.let { - session.getUser(it)?.toMatrixItem() + return session.getRoomSummary(nativeRoomId)?.let { roomSummary -> + // Fallback to RoomSummary if there is no other member. + if (roomSummary.otherMemberIds.isEmpty()) { + roomSummary.toMatrixItem() + } else { + roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() } + } } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 1d09d30de2..73a6c07d6a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -29,7 +29,9 @@ import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.lookup.CallUserMapper import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.vectorCallService +import im.vector.app.features.session.coroutineScope import im.vector.app.push.fcm.FcmHelper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.asCoroutineDispatcher import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull @@ -46,6 +48,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.PeerConnectionFactory @@ -78,6 +81,9 @@ class WebRtcCallManager @Inject constructor( private val callUserMapper: CallUserMapper? get() = currentSession?.vectorCallService?.userMapper + private val sessionScope: CoroutineScope? + get() = currentSession?.coroutineScope + interface CurrentCallListener { fun onCurrentCallChange(call: WebRtcCall?) {} fun onAudioDevicesChange() {} @@ -235,12 +241,12 @@ class WebRtcCallManager @Inject constructor( this.currentCall.setAndNotify(call) } - private fun onCallEnded(callId: String) { - Timber.tag(loggerTag.value).v("WebRtcPeerConnectionManager onCall ended: $callId") + private fun onCallEnded(callId: String, endCallReason: EndCallReason, rejected: Boolean) { + Timber.tag(loggerTag.value).v("onCall ended: $callId") val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also { Timber.tag(loggerTag.value).v("On call ended for unknown call $callId") } - CallService.onCallTerminated(context, callId) + CallService.onCallTerminated(context, callId, endCallReason, rejected) callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall) transferees.remove(callId) @@ -332,8 +338,8 @@ class WebRtcCallManager @Inject constructor( return webRtcCall } - fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) { - callsByRoomId[roomId]?.firstOrNull()?.endCall(originatedByMe) + fun endCallForRoom(roomId: String) { + callsByRoomId[roomId]?.firstOrNull()?.endCall() } override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { @@ -389,7 +395,7 @@ class WebRtcCallManager @Inject constructor( ?: return Unit.also { Timber.tag(loggerTag.value).w("onCallHangupReceived for non active call? ${callHangupContent.callId}") } - call.endCall(false) + call.onCallHangupReceived(callHangupContent) } override fun onCallRejectReceived(callRejectContent: CallRejectContent) { @@ -397,7 +403,7 @@ class WebRtcCallManager @Inject constructor( ?: return Unit.also { Timber.tag(loggerTag.value).w("onCallRejectReceived for non active call? ${callRejectContent.callId}") } - call.endCall(false) + call.onCallRejectReceived(callRejectContent) } override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) { @@ -405,12 +411,7 @@ class WebRtcCallManager @Inject constructor( ?: return Unit.also { Timber.tag(loggerTag.value).w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") } - val selectedPartyId = callSelectAnswerContent.selectedPartyId - if (selectedPartyId != call.mxCall.ourPartyId) { - Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${call.mxCall.ourPartyId}.") - // The other party has picked somebody else's answer - call.endCall(false) - } + call.onCallSelectedAnswerReceived(callSelectAnswerContent) } override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { @@ -423,7 +424,7 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId") - onCallEnded(callId) + onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false) } override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { 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 439705c9d6..24a64f3cdb 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 @@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.services.CallService import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver @@ -298,12 +299,14 @@ class NotificationUtils @Inject constructor(private val context: Context, .apply { if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.incoming_video_call)) + setSmallIcon(R.drawable.ic_call_answer_video) } else { setContentText(stringProvider.getString(R.string.incoming_voice_call)) + setSmallIcon(R.drawable.ic_call_answer) } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setLights(accentColor, 500, 500) .setOngoing(true) @@ -339,8 +342,6 @@ class NotificationUtils @Inject constructor(private val context: Context, builder.addAction( NotificationCompat.Action( R.drawable.ic_call_answer, - // IconCompat.createWithResource(applicationContext, R.drawable.ic_call) - // .setTint(ContextCompat.getColor(applicationContext, R.color.vctr_positive_accent)), getActionText(R.string.call_notification_answer, R.attr.colorPrimary), answerCallPendingIntent ) @@ -360,10 +361,15 @@ class NotificationUtils @Inject constructor(private val context: Context, .setContentTitle(ensureTitleNotEmpty(title)) .apply { setContentText(stringProvider.getString(R.string.call_ring)) + if (call.mxCall.isVideoCall) { + setSmallIcon(R.drawable.ic_call_answer_video) + } else { + setSmallIcon(R.drawable.ic_call_answer) + } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) .setLights(accentColor, 500, 500) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setOngoing(true) val contentIntent = VectorCallActivity.newIntent( @@ -407,11 +413,13 @@ class NotificationUtils @Inject constructor(private val context: Context, .apply { if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.video_call_in_progress)) + setSmallIcon(R.drawable.ic_call_answer_video) } else { setContentText(stringProvider.getString(R.string.call_in_progress)) + setSmallIcon(R.drawable.ic_call_answer) } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setCategory(NotificationCompat.CATEGORY_CALL) val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) @@ -450,15 +458,51 @@ class NotificationUtils @Inject constructor(private val context: Context, /** * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended */ - fun buildCallEndedNotification(): Notification { + fun buildCallEndedNotification(isVideoCall: Boolean): Notification { return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(stringProvider.getString(R.string.call_ended)) + .apply { + if (isVideoCall) { + setSmallIcon(R.drawable.ic_call_answer_video) + } else { + setSmallIcon(R.drawable.ic_call_answer) + } + } .setTimeoutAfter(2000) - .setSmallIcon(R.drawable.ic_material_call_end_grey) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setCategory(NotificationCompat.CATEGORY_CALL) .build() } + /** + * Build notification for the CallService, when a call is missed + */ + fun buildCallMissedNotification(callInformation: CallService.CallInformation): Notification { + val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) + .setContentTitle(callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId) + .apply { + if (callInformation.isVideoCall) { + setContentText(stringProvider.getQuantityString(R.plurals.missed_video_call, 1, 1)) + setSmallIcon(R.drawable.ic_missed_video_call) + } else { + setContentText(stringProvider.getQuantityString(R.plurals.missed_audio_call, 1, 1)) + setSmallIcon(R.drawable.ic_missed_voice_call) + } + } + .setShowWhen(true) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) + .setAutoCancel(true) + .setCategory(NotificationCompat.CATEGORY_CALL) + + val contentPendingIntent = TaskStackBuilder.create(context) + .addNextIntentWithParentStack(HomeActivity.newIntent(context)) + .addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId))) + .getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT) + + builder.setContentIntent(contentPendingIntent) + return builder.build() + } + fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification { return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setGroup(stringProvider.getString(R.string.app_name)) diff --git a/vector/src/main/res/drawable/ic_missed_video_call.xml b/vector/src/main/res/drawable/ic_missed_video_call.xml new file mode 100644 index 0000000000..555e15a371 --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_video_call.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_missed_video_call_small.xml b/vector/src/main/res/drawable/ic_missed_video_call_small.xml new file mode 100644 index 0000000000..c703f0cfca --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_video_call_small.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/drawable/ic_missed_voice_call.xml b/vector/src/main/res/drawable/ic_missed_voice_call.xml new file mode 100644 index 0000000000..dc869fa8bc --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_voice_call.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_missed_voice_call_small.xml b/vector/src/main/res/drawable/ic_missed_voice_call_small.xml new file mode 100644 index 0000000000..21d8b309c6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_voice_call_small.xml @@ -0,0 +1,5 @@ + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ef25329eed..e3385765a8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -74,19 +74,19 @@ You upgraded here. %s set the server ACLs for this room. You set the server ACLs for this room. - • Server matching %s are banned. - • Server matching %s are allowed. - • Server matching IP literals are allowed. - • Server matching IP literals are banned. + • Servers matching %s are banned. + • Servers matching %s are allowed. + • Servers matching IP literals are allowed. + • Servers matching IP literals are banned. %s changed the server ACLs for this room. You changed the server ACLs for this room. - • Server matching %s are now banned. - • Server matching %s were removed from the ban list. - • Server matching %s are now allowed. - • Server matching %s were removed from the allowed list. - • Server matching IP literals are now allowed. - • Server matching IP literals are now banned. + • Servers matching %s are now banned. + • Servers matching %s were removed from the ban list. + • Servers matching %s are now allowed. + • Servers matching %s were removed from the allowed list. + • Servers matching IP literals are now allowed. + • Servers matching IP literals are now banned. No change. 🎉 All servers are banned from participating! This room can no longer be used. @@ -727,6 +727,14 @@ Call connected Call connecting… Call ended + + Missed audio call + %d missed audio calls + + + Missed video call + %d missed video calls + Calling… Incoming Call Incoming Video Call