Merge branch 'develop' into feature/fga/log_tags_voip
This commit is contained in:
commit
e356e71431
1
changelog.d/3710.feature
Normal file
1
changelog.d/3710.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Show missed call notification.
|
1
changelog.d/3713.removal
Normal file
1
changelog.d/3713.removal
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add initialState support to CreateRoomParams (#3713)
|
1
changelog.d/3720.bugfix
Normal file
1
changelog.d/3720.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix a crash which can happen when user signs out
|
1
changelog.d/3721.misc
Normal file
1
changelog.d/3721.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Apply grammatical fixes to the Server ACL timeline messages.
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.call
|
package org.matrix.android.sdk.api.session.call
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
|
|
||||||
sealed class CallState {
|
sealed class CallState {
|
||||||
|
|
||||||
/** Idle, setting up objects */
|
/** Idle, setting up objects */
|
||||||
@ -42,6 +44,6 @@ sealed class CallState {
|
|||||||
* */
|
* */
|
||||||
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
|
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
|
||||||
|
|
||||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
/** Ended. Incoming/Outgoing call, the call is terminated */
|
||||||
object Terminated : CallState()
|
data class Ended(val reason: EndCallReason? = null) : CallState()
|
||||||
}
|
}
|
||||||
|
@ -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.CallCandidate
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
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.session.room.model.call.SdpType
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ interface MxCall : MxCallDetail {
|
|||||||
/**
|
/**
|
||||||
* End the call
|
* End the call
|
||||||
*/
|
*/
|
||||||
fun hangUp(reason: CallHangupContent.Reason? = null)
|
fun hangUp(reason: EndCallReason? = null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a call
|
* Start a call
|
||||||
|
@ -43,29 +43,5 @@ data class CallHangupContent(
|
|||||||
* or `invite_timeout` for when the other party did not answer in time.
|
* or `invite_timeout` for when the other party did not answer in time.
|
||||||
* One of: ["ice_failed", "invite_timeout"]
|
* One of: ["ice_failed", "invite_timeout"]
|
||||||
*/
|
*/
|
||||||
@Json(name = "reason") val reason: Reason? = null
|
@Json(name = "reason") val reason: EndCallReason? = null
|
||||||
) : CallSignalingContent {
|
) : 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -36,5 +36,10 @@ data class CallRejectContent(
|
|||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this message adheres to.
|
* 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
|
) : CallSignalingContent
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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.api.session.room.model.RoomJoinRulesAllowEntry
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
|
||||||
// TODO Give a way to include other initial states
|
|
||||||
open class CreateRoomParams {
|
open class CreateRoomParams {
|
||||||
/**
|
/**
|
||||||
* A public visibility indicates that the room will be shown in the published room list.
|
* 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<String, Any>()
|
val creationContent = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<CreateRoomStateEvent>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true to disable federation of this room.
|
* Set to true to disable federation of this room.
|
||||||
* Default: false
|
* Default: false
|
||||||
|
@ -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 = ""
|
||||||
|
)
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
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)
|
downloadKeysForUsersTask.execute(params)
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
||||||
|
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)
|
onKeysDownloadFailed(filteredUsers)
|
||||||
|
}
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||||
|
@ -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}")
|
Timber.tag(loggerTag.value).v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (call.state != CallState.Terminated) {
|
if (call.state !is CallState.Ended) {
|
||||||
activeCallHandler.removeCall(content.callId)
|
activeCallHandler.removeCall(content.callId)
|
||||||
callListenersDispatcher.onCallHangupReceived(content)
|
callListenersDispatcher.onCallHangupReceived(content)
|
||||||
}
|
}
|
||||||
|
@ -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.CallReplacesContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
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.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.session.room.model.call.SdpType
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
||||||
@ -145,7 +146,7 @@ internal class MxCallImpl(
|
|||||||
override fun reject() {
|
override fun reject() {
|
||||||
if (opponentVersion < 1) {
|
if (opponentVersion < 1) {
|
||||||
Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
|
Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
|
||||||
hangUp()
|
hangUp(EndCallReason.USER_HANGUP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Timber.tag(loggerTag.value).v("reject $callId")
|
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()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.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")
|
Timber.tag(loggerTag.value).v("hangup $callId")
|
||||||
CallHangupContent(
|
CallHangupContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
|
reason = reason,
|
||||||
version = MxCall.VOIP_PROTO_VERSION.toString()
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
state = CallState.Terminated
|
state = CallState.Ended(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun accept(sdpString: String) {
|
override fun accept(sdpString: String) {
|
||||||
|
@ -81,13 +81,14 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||||||
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
|
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
|
||||||
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
|
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
|
||||||
}
|
}
|
||||||
val initialStates = listOfNotNull(
|
val initialStates = (listOfNotNull(
|
||||||
buildEncryptionWithAlgorithmEvent(params),
|
buildEncryptionWithAlgorithmEvent(params),
|
||||||
buildHistoryVisibilityEvent(params),
|
buildHistoryVisibilityEvent(params),
|
||||||
buildAvatarEvent(params),
|
buildAvatarEvent(params),
|
||||||
buildGuestAccess(params),
|
buildGuestAccess(params),
|
||||||
buildJoinRulesRestricted(params)
|
buildJoinRulesRestricted(params)
|
||||||
)
|
)
|
||||||
|
+ buildCustomInitialStates(params))
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
return CreateRoomBody(
|
return CreateRoomBody(
|
||||||
@ -95,7 +96,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||||||
roomAliasName = params.roomAliasName,
|
roomAliasName = params.roomAliasName,
|
||||||
name = params.name,
|
name = params.name,
|
||||||
topic = params.topic,
|
topic = params.topic,
|
||||||
invitedUserIds = params.invitedUserIds.filter { it != userId },
|
invitedUserIds = params.invitedUserIds.filter { it != userId }.takeIf { it.isNotEmpty() },
|
||||||
invite3pids = invite3pids,
|
invite3pids = invite3pids,
|
||||||
creationContent = params.creationContent.takeIf { it.isNotEmpty() },
|
creationContent = params.creationContent.takeIf { it.isNotEmpty() },
|
||||||
initialStates = initialStates,
|
initialStates = initialStates,
|
||||||
@ -103,10 +104,19 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
|||||||
isDirect = params.isDirect,
|
isDirect = params.isDirect,
|
||||||
powerLevelContentOverride = params.powerLevelContentOverride,
|
powerLevelContentOverride = params.powerLevelContentOverride,
|
||||||
roomVersion = params.roomVersion
|
roomVersion = params.roomVersion
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildCustomInitialStates(params: CreateRoomParams): List<Event> {
|
||||||
|
return params.initialStates.map {
|
||||||
|
Event(
|
||||||
|
type = it.type,
|
||||||
|
stateKey = it.stateKey,
|
||||||
|
content = it.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
||||||
return params.avatarUri?.let { avatarUri ->
|
return params.avatarUri?.let { avatarUri ->
|
||||||
// First upload the image, ignoring any error
|
// First upload the image, ignoring any error
|
||||||
|
@ -43,7 +43,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
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'
|
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
|
@ -305,7 +305,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
def epoxy_version = '4.6.2'
|
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 arrow_version = "0.8.2"
|
||||||
def markwon_version = '4.1.2'
|
def markwon_version = '4.1.2'
|
||||||
def big_image_viewer_version = '1.8.0'
|
def big_image_viewer_version = '1.8.0'
|
||||||
@ -342,7 +342,7 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||||
implementation 'androidx.core:core-ktx:1.6.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 "androidx.transition:transition:1.4.1"
|
||||||
|
|
||||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
|
@ -38,6 +38,7 @@ import im.vector.app.features.notifications.NotificationUtils
|
|||||||
import im.vector.app.features.popup.IncomingCallAlert
|
import im.vector.app.features.popup.IncomingCallAlert
|
||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
import org.matrix.android.sdk.api.logger.LoggerTag
|
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 org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -49,7 +50,8 @@ private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP)
|
|||||||
class CallService : VectorService() {
|
class CallService : VectorService() {
|
||||||
|
|
||||||
private val connections = mutableMapOf<String, CallConnection>()
|
private val connections = mutableMapOf<String, CallConnection>()
|
||||||
private val knownCalls = mutableSetOf<String>()
|
private val knownCalls = mutableSetOf<CallInformation>()
|
||||||
|
private val connectedCallIds = mutableSetOf<String>()
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
private lateinit var notificationUtils: NotificationUtils
|
private lateinit var notificationUtils: NotificationUtils
|
||||||
@ -156,9 +158,9 @@ class CallService : VectorService() {
|
|||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
|
val callInformation = call.toCallInformation()
|
||||||
val isVideoCall = call.mxCall.isVideoCall
|
val isVideoCall = call.mxCall.isVideoCall
|
||||||
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
|
||||||
Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification")
|
Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification")
|
||||||
val incomingCallAlert = IncomingCallAlert(callId,
|
val incomingCallAlert = IncomingCallAlert(callId,
|
||||||
shouldBeDisplayedIn = { activity ->
|
shouldBeDisplayedIn = { activity ->
|
||||||
@ -168,7 +170,7 @@ class CallService : VectorService() {
|
|||||||
}
|
}
|
||||||
).apply {
|
).apply {
|
||||||
viewBinder = IncomingCallAlert.ViewBinder(
|
viewBinder = IncomingCallAlert.ViewBinder(
|
||||||
matrixItem = opponentMatrixItem,
|
matrixItem = callInformation.opponentMatrixItem,
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
isVideoCall = isVideoCall,
|
isVideoCall = isVideoCall,
|
||||||
onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) },
|
onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) },
|
||||||
@ -180,7 +182,7 @@ class CallService : VectorService() {
|
|||||||
alertManager.postVectorAlert(incomingCallAlert)
|
alertManager.postVectorAlert(incomingCallAlert)
|
||||||
val notification = notificationUtils.buildIncomingCallNotification(
|
val notification = notificationUtils.buildIncomingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId,
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId,
|
||||||
fromBg = fromBg
|
fromBg = fromBg
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
@ -188,23 +190,34 @@ class CallService : VectorService() {
|
|||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCallTerminated(intent: Intent) {
|
private fun handleCallTerminated(intent: Intent) {
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
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)
|
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$")
|
Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$")
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
knownCalls.remove(terminatedCall)
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
mediaSession?.isActive = false
|
mediaSession?.isActive = false
|
||||||
myStopSelf()
|
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) {
|
private fun showCallScreen(call: WebRtcCall, mode: String) {
|
||||||
@ -221,18 +234,18 @@ class CallService : VectorService() {
|
|||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
val callInformation = call.toCallInformation()
|
||||||
Timber.tag(loggerTag.value).v("displayOutgoingCallNotification : display the dedicated notification")
|
Timber.tag(loggerTag.value).v("displayOutgoingCallNotification : display the dedicated notification")
|
||||||
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
startForeground(callId.hashCode(), notification)
|
startForeground(callId.hashCode(), notification)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -241,21 +254,22 @@ class CallService : VectorService() {
|
|||||||
private fun displayCallInProgressNotification(intent: Intent) {
|
private fun displayCallInProgressNotification(intent: Intent) {
|
||||||
Timber.tag(loggerTag.value).v("displayCallInProgressNotification")
|
Timber.tag(loggerTag.value).v("displayCallInProgressNotification")
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
connectedCallIds.add(callId)
|
||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
|
||||||
alertManager.cancelAlert(callId)
|
alertManager.cancelAlert(callId)
|
||||||
|
val callInformation = call.toCallInformation()
|
||||||
val notification = notificationUtils.buildPendingCallNotification(
|
val notification = notificationUtils.buildPendingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
startForeground(callId.hashCode(), notification)
|
startForeground(callId.hashCode(), notification)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnexpectedState(callId: String?) {
|
private fun handleUnexpectedState(callId: String?) {
|
||||||
@ -265,7 +279,7 @@ class CallService : VectorService() {
|
|||||||
if (callId != null) {
|
if (callId != null) {
|
||||||
notificationManager.cancel(callId.hashCode())
|
notificationManager.cancel(callId.hashCode())
|
||||||
}
|
}
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
val notification = notificationUtils.buildCallEndedNotification(false)
|
||||||
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
mediaSession?.isActive = false
|
mediaSession?.isActive = false
|
||||||
@ -277,14 +291,31 @@ class CallService : VectorService() {
|
|||||||
connections[callConnection.callId] = callConnection
|
connections[callConnection.callId] = callConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? {
|
private fun WebRtcCall.toCallInformation(): CallInformation {
|
||||||
return vectorComponent().activeSessionHolder().getSafeActiveSession()?.let {
|
return CallInformation(
|
||||||
call.getOpponentAsMatrixItem(it)
|
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 {
|
companion object {
|
||||||
private const val DEFAULT_NOTIFICATION_ID = 6480
|
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_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"
|
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_CALL_ID = "EXTRA_CALL_ID"
|
||||||
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
|
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,
|
fun onIncomingCallRinging(context: Context,
|
||||||
callId: String,
|
callId: String,
|
||||||
@ -332,11 +365,13 @@ class CallService : VectorService() {
|
|||||||
ContextCompat.startForegroundService(context, intent)
|
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)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_CALL_TERMINATED
|
action = ACTION_CALL_TERMINATED
|
||||||
putExtra(EXTRA_CALL_ID, callId)
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
putExtra(EXTRA_END_CALL_REASON, endCallReason)
|
||||||
|
putExtra(EXTRA_END_CALL_REJECTED, rejected)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ class CallControlsView @JvmOverloads constructor(
|
|||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallState.Terminated,
|
is CallState.Ended,
|
||||||
null -> {
|
null -> {
|
||||||
views.ringingControls.isVisible = false
|
views.ringingControls.isVisible = false
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
|
@ -249,7 +249,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||||||
views.callConnectingProgress.isVisible = true
|
views.callConnectingProgress.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallState.Terminated -> {
|
is CallState.Ended -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
|
@ -57,7 +57,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
|||||||
private val call = callManager.getCallById(initialState.callId)
|
private val call = callManager.getCallById(initialState.callId)
|
||||||
private val callListener = object : WebRtcCall.Listener {
|
private val callListener = object : WebRtcCall.Listener {
|
||||||
override fun onStateUpdate(call: MxCall) {
|
override fun onStateUpdate(call: MxCall) {
|
||||||
if (call.state == CallState.Terminated) {
|
if (call.state is CallState.Ended) {
|
||||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.CallHangupContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
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.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.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
import org.threeten.bp.Duration
|
import org.threeten.bp.Duration
|
||||||
import org.webrtc.AudioSource
|
import org.webrtc.AudioSource
|
||||||
@ -102,7 +105,7 @@ class WebRtcCall(
|
|||||||
private val sessionProvider: Provider<Session?>,
|
private val sessionProvider: Provider<Session?>,
|
||||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||||
private val onCallEnded: (String) -> Unit
|
private val onCallEnded: (String, EndCallReason, Boolean) -> Unit
|
||||||
) : MxCall.StateListener {
|
) : MxCall.StateListener {
|
||||||
|
|
||||||
interface Listener : MxCall.StateListener {
|
interface Listener : MxCall.StateListener {
|
||||||
@ -230,7 +233,7 @@ class WebRtcCall(
|
|||||||
// Allow a short time for initial candidates to be gathered
|
// Allow a short time for initial candidates to be gathered
|
||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
if (mxCall.state == CallState.Terminated) {
|
if (mxCall.state is CallState.Ended) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (mxCall.state == CallState.CreateOffer) {
|
if (mxCall.state == CallState.CreateOffer) {
|
||||||
@ -288,7 +291,7 @@ class WebRtcCall(
|
|||||||
createCallId = CallIdGenerator.generate(),
|
createCallId = CallIdGenerator.generate(),
|
||||||
awaitCallId = null
|
awaitCallId = null
|
||||||
)
|
)
|
||||||
endCall(sendEndSignaling = false)
|
terminate(EndCallReason.REPLACED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,8 +313,8 @@ class WebRtcCall(
|
|||||||
createCallId = newCallId,
|
createCallId = newCallId,
|
||||||
awaitCallId = null
|
awaitCallId = null
|
||||||
)
|
)
|
||||||
endCall(sendEndSignaling = false)
|
terminate(EndCallReason.REPLACED)
|
||||||
transferTargetCall.endCall(sendEndSignaling = false)
|
transferTargetCall.terminate(EndCallReason.REPLACED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,7 +467,7 @@ class WebRtcCall(
|
|||||||
peerConnection?.awaitSetRemoteDescription(offerSdp)
|
peerConnection?.awaitSetRemoteDescription(offerSdp)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.tag(loggerTag.value).v("Failure putting remote description")
|
Timber.tag(loggerTag.value).v("Failure putting remote description")
|
||||||
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
|
endCall(reason = EndCallReason.UNKWOWN_ERROR)
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
// 2) Access camera + microphone, create local stream
|
// 2) Access camera + microphone, create local stream
|
||||||
@ -770,7 +773,7 @@ class WebRtcCall(
|
|||||||
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
|
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
|
||||||
Timber.tag(loggerTag.value).e("StreamObserver weird looking stream: $stream")
|
Timber.tag(loggerTag.value).e("StreamObserver weird looking stream: $stream")
|
||||||
// TODO maybe do something more??
|
// TODO maybe do something more??
|
||||||
endCall(true)
|
endCall(EndCallReason.UNKWOWN_ERROR)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (stream.audioTracks.size == 1) {
|
if (stream.audioTracks.size == 1) {
|
||||||
@ -798,11 +801,22 @@ class WebRtcCall(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
|
fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
if (mxCall.state == CallState.Terminated) {
|
if (mxCall.state is CallState.Ended) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
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
|
// Close tracks ASAP
|
||||||
localVideoTrack?.setEnabled(false)
|
localVideoTrack?.setEnabled(false)
|
||||||
localVideoTrack?.setEnabled(false)
|
localVideoTrack?.setEnabled(false)
|
||||||
@ -810,18 +824,9 @@ class WebRtcCall(
|
|||||||
val cameraManager = context.getSystemService<CameraManager>()!!
|
val cameraManager = context.getSystemService<CameraManager>()!!
|
||||||
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
||||||
}
|
}
|
||||||
val wasRinging = mxCall.state is CallState.LocalRinging
|
mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP)
|
||||||
mxCall.state = CallState.Terminated
|
|
||||||
release()
|
release()
|
||||||
onCallEnded(callId)
|
onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected)
|
||||||
if (sendEndSignaling) {
|
|
||||||
if (wasRinging) {
|
|
||||||
mxCall.reject()
|
|
||||||
} else {
|
|
||||||
mxCall.hangUp(reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call listener
|
// Call listener
|
||||||
@ -846,7 +851,7 @@ class WebRtcCall(
|
|||||||
try {
|
try {
|
||||||
peerConnection?.awaitSetRemoteDescription(sdp)
|
peerConnection?.awaitSetRemoteDescription(sdp)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
|
endCall(EndCallReason.UNKWOWN_ERROR)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
|
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) {
|
fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
val session = sessionProvider.get() ?: return@launch
|
val session = sessionProvider.get() ?: return@launch
|
||||||
|
@ -21,7 +21,12 @@ import org.matrix.android.sdk.api.util.MatrixItem
|
|||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
|
||||||
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
|
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
|
||||||
return session.getRoomSummary(nativeRoomId)?.otherMemberIds?.firstOrNull()?.let {
|
return session.getRoomSummary(nativeRoomId)?.let { roomSummary ->
|
||||||
session.getUser(it)?.toMatrixItem()
|
// Fallback to RoomSummary if there is no other member.
|
||||||
|
if (roomSummary.otherMemberIds.isEmpty()) {
|
||||||
|
roomSummary.toMatrixItem()
|
||||||
|
} else {
|
||||||
|
roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.lookup.CallUserMapper
|
||||||
import im.vector.app.features.call.utils.EglUtils
|
import im.vector.app.features.call.utils.EglUtils
|
||||||
import im.vector.app.features.call.vectorCallService
|
import im.vector.app.features.call.vectorCallService
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.push.fcm.FcmHelper
|
import im.vector.app.push.fcm.FcmHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
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.CallNegotiateContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
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.CallSelectAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.webrtc.DefaultVideoDecoderFactory
|
import org.webrtc.DefaultVideoDecoderFactory
|
||||||
import org.webrtc.DefaultVideoEncoderFactory
|
import org.webrtc.DefaultVideoEncoderFactory
|
||||||
import org.webrtc.PeerConnectionFactory
|
import org.webrtc.PeerConnectionFactory
|
||||||
@ -78,6 +81,9 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
private val callUserMapper: CallUserMapper?
|
private val callUserMapper: CallUserMapper?
|
||||||
get() = currentSession?.vectorCallService?.userMapper
|
get() = currentSession?.vectorCallService?.userMapper
|
||||||
|
|
||||||
|
private val sessionScope: CoroutineScope?
|
||||||
|
get() = currentSession?.coroutineScope
|
||||||
|
|
||||||
interface CurrentCallListener {
|
interface CurrentCallListener {
|
||||||
fun onCurrentCallChange(call: WebRtcCall?) {}
|
fun onCurrentCallChange(call: WebRtcCall?) {}
|
||||||
fun onAudioDevicesChange() {}
|
fun onAudioDevicesChange() {}
|
||||||
@ -235,12 +241,12 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
this.currentCall.setAndNotify(call)
|
this.currentCall.setAndNotify(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallEnded(callId: String) {
|
private fun onCallEnded(callId: String, endCallReason: EndCallReason, rejected: Boolean) {
|
||||||
Timber.tag(loggerTag.value).v("WebRtcPeerConnectionManager onCall ended: $callId")
|
Timber.tag(loggerTag.value).v("onCall ended: $callId")
|
||||||
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
|
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).v("On call ended for unknown call $callId")
|
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.signalingRoomId]?.remove(webRtcCall)
|
||||||
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
||||||
transferees.remove(callId)
|
transferees.remove(callId)
|
||||||
@ -332,8 +338,8 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
return webRtcCall
|
return webRtcCall
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) {
|
fun endCallForRoom(roomId: String) {
|
||||||
callsByRoomId[roomId]?.firstOrNull()?.endCall(originatedByMe)
|
callsByRoomId[roomId]?.firstOrNull()?.endCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||||
@ -389,7 +395,7 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
|
Timber.tag(loggerTag.value).w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
|
||||||
}
|
}
|
||||||
call.endCall(false)
|
call.onCallHangupReceived(callHangupContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
|
override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
|
||||||
@ -397,7 +403,7 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).w("onCallRejectReceived for non active call? ${callRejectContent.callId}")
|
Timber.tag(loggerTag.value).w("onCallRejectReceived for non active call? ${callRejectContent.callId}")
|
||||||
}
|
}
|
||||||
call.endCall(false)
|
call.onCallRejectReceived(callRejectContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
|
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
|
||||||
@ -405,12 +411,7 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
?: return Unit.also {
|
?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
|
Timber.tag(loggerTag.value).w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
|
||||||
}
|
}
|
||||||
val selectedPartyId = callSelectAnswerContent.selectedPartyId
|
call.onCallSelectedAnswerReceived(callSelectAnswerContent)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
|
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
|
||||||
@ -423,7 +424,7 @@ class WebRtcCallManager @Inject constructor(
|
|||||||
|
|
||||||
override fun onCallManagedByOtherSession(callId: String) {
|
override fun onCallManagedByOtherSession(callId: String) {
|
||||||
Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId")
|
Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId")
|
||||||
onCallEnded(callId)
|
onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
||||||
|
@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment
|
|||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
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.core.utils.startNotificationChannelSettingsIntent
|
||||||
import im.vector.app.features.call.VectorCallActivity
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.service.CallHeadsUpActionReceiver
|
import im.vector.app.features.call.service.CallHeadsUpActionReceiver
|
||||||
@ -298,12 +299,14 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||||||
.apply {
|
.apply {
|
||||||
if (call.mxCall.isVideoCall) {
|
if (call.mxCall.isVideoCall) {
|
||||||
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||||
} else {
|
} else {
|
||||||
setContentText(stringProvider.getString(R.string.incoming_voice_call))
|
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)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setLights(accentColor, 500, 500)
|
.setLights(accentColor, 500, 500)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
@ -339,8 +342,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||||||
builder.addAction(
|
builder.addAction(
|
||||||
NotificationCompat.Action(
|
NotificationCompat.Action(
|
||||||
R.drawable.ic_call_answer,
|
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),
|
getActionText(R.string.call_notification_answer, R.attr.colorPrimary),
|
||||||
answerCallPendingIntent
|
answerCallPendingIntent
|
||||||
)
|
)
|
||||||
@ -360,10 +361,15 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||||||
.setContentTitle(ensureTitleNotEmpty(title))
|
.setContentTitle(ensureTitleNotEmpty(title))
|
||||||
.apply {
|
.apply {
|
||||||
setContentText(stringProvider.getString(R.string.call_ring))
|
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)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
.setLights(accentColor, 500, 500)
|
.setLights(accentColor, 500, 500)
|
||||||
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
val contentIntent = VectorCallActivity.newIntent(
|
val contentIntent = VectorCallActivity.newIntent(
|
||||||
@ -407,11 +413,13 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||||||
.apply {
|
.apply {
|
||||||
if (call.mxCall.isVideoCall) {
|
if (call.mxCall.isVideoCall) {
|
||||||
setContentText(stringProvider.getString(R.string.video_call_in_progress))
|
setContentText(stringProvider.getString(R.string.video_call_in_progress))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||||
} else {
|
} else {
|
||||||
setContentText(stringProvider.getString(R.string.call_in_progress))
|
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)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
|
||||||
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
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
|
* 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)
|
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle(stringProvider.getString(R.string.call_ended))
|
.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)
|
.setTimeoutAfter(2000)
|
||||||
.setSmallIcon(R.drawable.ic_material_call_end_grey)
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
.build()
|
.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 {
|
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
|
||||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||||
.setGroup(stringProvider.getString(R.string.app_name))
|
.setGroup(stringProvider.getString(R.string.app_name))
|
||||||
|
10
vector/src/main/res/drawable/ic_missed_video_call.xml
Normal file
10
vector/src/main/res/drawable/ic_missed_video_call.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,3.2273C0,1.6457 1.3432,0.3636 3,0.3636H14C15.6569,0.3636 17,1.6457 17,3.2273V12.7727C17,14.3543 15.6569,15.6364 14,15.6364H3C1.3431,15.6364 0,14.3543 0,12.7727V3.2273ZM19,5.1364L22.3753,2.5589C23.0301,2.0589 24,2.5038 24,3.3042V12.6958C24,13.4962 23.0301,13.9412 22.3753,13.4412L19,10.8637V5.1364ZM5.5288,8.8219C5.5288,9.2423 5.1848,9.5863 4.7644,9.5863C4.344,9.5863 4,9.2423 4,8.8219V5.7644C4,5.344 4.344,5 4.7644,5H7.8219C8.2423,5 8.5863,5.344 8.5863,5.7644C8.5863,6.1848 8.2423,6.5288 7.8219,6.5288H6.5989L9.3125,9.2423L13.0961,5.4586C13.3942,5.1605 13.8758,5.1605 14.1739,5.4586C14.472,5.7567 14.472,6.2383 14.1739,6.5364L9.8475,10.8628C9.5494,11.1609 9.0679,11.1609 8.7697,10.8628L5.5288,7.6218V8.8219Z"
|
||||||
|
android:fillColor="#737D8C"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
@ -0,0 +1,4 @@
|
|||||||
|
<vector android:height="10.666667dp" android:viewportHeight="16"
|
||||||
|
android:viewportWidth="24" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#737D8C" android:fillType="evenOdd" android:pathData="M0,3.2273C0,1.6457 1.3432,0.3636 3,0.3636H14C15.6569,0.3636 17,1.6457 17,3.2273V12.7727C17,14.3543 15.6569,15.6364 14,15.6364H3C1.3431,15.6364 0,14.3543 0,12.7727V3.2273ZM19,5.1364L22.3753,2.5589C23.0301,2.0589 24,2.5038 24,3.3042V12.6958C24,13.4962 23.0301,13.9412 22.3753,13.4412L19,10.8637V5.1364ZM5.5288,8.8219C5.5288,9.2423 5.1848,9.5863 4.7644,9.5863C4.344,9.5863 4,9.2423 4,8.8219V5.7644C4,5.344 4.344,5 4.7644,5H7.8219C8.2423,5 8.5863,5.344 8.5863,5.7644C8.5863,6.1848 8.2423,6.5288 7.8219,6.5288H6.5989L9.3125,9.2423L13.0961,5.4586C13.3942,5.1605 13.8758,5.1605 14.1739,5.4586C14.472,5.7567 14.472,6.2383 14.1739,6.5364L9.8475,10.8628C9.5494,11.1609 9.0679,11.1609 8.7697,10.8628L5.5288,7.6218V8.8219Z"/>
|
||||||
|
</vector>
|
12
vector/src/main/res/drawable/ic_missed_voice_call.xml
Normal file
12
vector/src/main/res/drawable/ic_missed_voice_call.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M6,9C6.55,9 7,8.55 7,8V6.43L11.24,10.67C11.63,11.06 12.26,11.06 12.65,10.67L18.31,5.01C18.7,4.62 18.7,3.99 18.31,3.6C17.92,3.21 17.29,3.21 16.9,3.6L11.95,8.55L8.4,5H10C10.55,5 11,4.55 11,4C11,3.45 10.55,3 10,3H6C5.45,3 5,3.45 5,4V8C5,8.55 5.45,9 6,9Z"
|
||||||
|
android:fillColor="#818A98"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.0084,13.0065C10.3211,12.9416 6.8514,13.3795 6.0078,13.6013C5.9579,13.6144 5.9004,13.6291 5.8362,13.6455C4.541,13.9761 0.4827,15.0118 0.0442,18.2936C-0.2955,20.8362 1.4058,21.6058 2.2562,21.4886C2.8448,21.4148 4.5301,21.1483 6.0872,20.8689C7.6163,20.5946 7.6155,19.5859 7.615,18.9038C7.615,18.8913 7.615,18.8788 7.615,18.8665L7.615,17.4953C7.615,17.1461 7.9432,16.9442 8.3958,16.8896C9.9982,16.672 11.3359,16.6713 12.0055,16.6713L12.0112,16.6713C12.6807,16.6713 14.0018,16.672 15.6042,16.8896C16.0569,16.9442 16.385,17.1461 16.385,17.4953L16.385,18.8665C16.385,18.8789 16.385,18.8913 16.385,18.9038C16.3845,19.5859 16.3837,20.5946 17.9128,20.869C19.4699,21.1483 21.1552,21.4148 21.7438,21.4886C22.5942,21.6058 24.2955,20.8362 23.9558,18.2936C23.5173,15.0118 19.459,13.9761 18.1638,13.6455C18.0996,13.6291 18.0421,13.6145 17.9922,13.6013C17.1487,13.3795 13.6956,12.9416 12.0084,13.0065Z"
|
||||||
|
android:fillColor="#818A98"/>
|
||||||
|
</vector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="16dp" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#818A98" android:pathData="M6,9C6.55,9 7,8.55 7,8V6.43L11.24,10.67C11.63,11.06 12.26,11.06 12.65,10.67L18.31,5.01C18.7,4.62 18.7,3.99 18.31,3.6C17.92,3.21 17.29,3.21 16.9,3.6L11.95,8.55L8.4,5H10C10.55,5 11,4.55 11,4C11,3.45 10.55,3 10,3H6C5.45,3 5,3.45 5,4V8C5,8.55 5.45,9 6,9Z"/>
|
||||||
|
<path android:fillColor="#818A98" android:pathData="M12.0084,13.0065C10.3211,12.9416 6.8514,13.3795 6.0078,13.6013C5.9579,13.6144 5.9004,13.6291 5.8362,13.6455C4.541,13.9761 0.4827,15.0118 0.0442,18.2936C-0.2955,20.8362 1.4058,21.6058 2.2562,21.4886C2.8448,21.4148 4.5301,21.1483 6.0872,20.8689C7.6163,20.5946 7.6155,19.5859 7.615,18.9038C7.615,18.8913 7.615,18.8788 7.615,18.8665L7.615,17.4953C7.615,17.1461 7.9432,16.9442 8.3958,16.8896C9.9982,16.672 11.3359,16.6713 12.0055,16.6713L12.0112,16.6713C12.6807,16.6713 14.0018,16.672 15.6042,16.8896C16.0569,16.9442 16.385,17.1461 16.385,17.4953L16.385,18.8665C16.385,18.8789 16.385,18.8913 16.385,18.9038C16.3845,19.5859 16.3837,20.5946 17.9128,20.869C19.4699,21.1483 21.1552,21.4148 21.7438,21.4886C22.5942,21.6058 24.2955,20.8362 23.9558,18.2936C23.5173,15.0118 19.459,13.9761 18.1638,13.6455C18.0996,13.6291 18.0421,13.6145 17.9922,13.6013C17.1487,13.3795 13.6956,12.9416 12.0084,13.0065Z"/>
|
||||||
|
</vector>
|
@ -74,19 +74,19 @@
|
|||||||
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
|
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
|
||||||
<string name="notice_room_server_acl_set_title">%s set the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_set_title">%s set the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_set_title_by_you">You set the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_set_title_by_you">You set the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_set_banned">• Server matching %s are banned.</string>
|
<string name="notice_room_server_acl_set_banned">• Servers matching %s are banned.</string>
|
||||||
<string name="notice_room_server_acl_set_allowed">• Server matching %s are allowed.</string>
|
<string name="notice_room_server_acl_set_allowed">• Servers matching %s are allowed.</string>
|
||||||
<string name="notice_room_server_acl_set_ip_literals_allowed">• Server matching IP literals are allowed.</string>
|
<string name="notice_room_server_acl_set_ip_literals_allowed">• Servers matching IP literals are allowed.</string>
|
||||||
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Server matching IP literals are banned.</string>
|
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Servers matching IP literals are banned.</string>
|
||||||
|
|
||||||
<string name="notice_room_server_acl_updated_title">%s changed the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_updated_title">%s changed the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_updated_title_by_you">You changed the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_updated_title_by_you">You changed the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_updated_banned">• Server matching %s are now banned.</string>
|
<string name="notice_room_server_acl_updated_banned">• Servers matching %s are now banned.</string>
|
||||||
<string name="notice_room_server_acl_updated_was_banned">• Server matching %s were removed from the ban list.</string>
|
<string name="notice_room_server_acl_updated_was_banned">• Servers matching %s were removed from the ban list.</string>
|
||||||
<string name="notice_room_server_acl_updated_allowed">• Server matching %s are now allowed.</string>
|
<string name="notice_room_server_acl_updated_allowed">• Servers matching %s are now allowed.</string>
|
||||||
<string name="notice_room_server_acl_updated_was_allowed">• Server matching %s were removed from the allowed list.</string>
|
<string name="notice_room_server_acl_updated_was_allowed">• Servers matching %s were removed from the allowed list.</string>
|
||||||
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Server matching IP literals are now allowed.</string>
|
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Servers matching IP literals are now allowed.</string>
|
||||||
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Server matching IP literals are now banned.</string>
|
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Servers matching IP literals are now banned.</string>
|
||||||
<string name="notice_room_server_acl_updated_no_change">No change.</string>
|
<string name="notice_room_server_acl_updated_no_change">No change.</string>
|
||||||
<string name="notice_room_server_acl_allow_is_empty">🎉 All servers are banned from participating! This room can no longer be used.</string>
|
<string name="notice_room_server_acl_allow_is_empty">🎉 All servers are banned from participating! This room can no longer be used.</string>
|
||||||
|
|
||||||
@ -727,6 +727,14 @@
|
|||||||
<string name="call_connected">Call connected</string>
|
<string name="call_connected">Call connected</string>
|
||||||
<string name="call_connecting">Call connecting…</string>
|
<string name="call_connecting">Call connecting…</string>
|
||||||
<string name="call_ended">Call ended</string>
|
<string name="call_ended">Call ended</string>
|
||||||
|
<plurals name="missed_audio_call">
|
||||||
|
<item quantity="one">Missed audio call</item>
|
||||||
|
<item quantity="other">%d missed audio calls</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="missed_video_call">
|
||||||
|
<item quantity="one">Missed video call</item>
|
||||||
|
<item quantity="other">%d missed video calls</item>
|
||||||
|
</plurals>
|
||||||
<string name="call_ring">Calling…</string>
|
<string name="call_ring">Calling…</string>
|
||||||
<string name="incoming_call">Incoming Call</string>
|
<string name="incoming_call">Incoming Call</string>
|
||||||
<string name="incoming_video_call">Incoming Video Call</string>
|
<string name="incoming_video_call">Incoming Video Call</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user