Merge branch 'develop' of github.com:vector-im/element-android into feature/dla/fix_account_notifications_discrepancies

This commit is contained in:
David Langley 2021-07-26 22:55:27 +01:00
commit 9c139b3bd0
50 changed files with 748 additions and 438 deletions

View File

@ -1,3 +1,28 @@
Changes in Element 1.1.14 (2021-07-23)
======================================
Features ✨
----------
- Add low priority section in DM tab ([#3463](https://github.com/vector-im/element-android/issues/3463))
- Show missed call notification. ([#3710](https://github.com/vector-im/element-android/issues/3710))
Bugfixes 🐛
----------
- Don't use the transaction ID of the verification for the request ([#3589](https://github.com/vector-im/element-android/issues/3589))
- Avoid incomplete downloads in cache ([#3656](https://github.com/vector-im/element-android/issues/3656))
- Fix a crash which can happen when user signs out ([#3720](https://github.com/vector-im/element-android/issues/3720))
- Ensure OTKs are uploaded when the session is created ([#3724](https://github.com/vector-im/element-android/issues/3724))
SDK API changes ⚠️
------------------
- Add initialState support to CreateRoomParams (#3713) ([#3713](https://github.com/vector-im/element-android/issues/3713))
Other changes
-------------
- Apply grammatical fixes to the Server ACL timeline messages. ([#3721](https://github.com/vector-im/element-android/issues/3721))
- Add tags in the log, especially for VoIP, but can be used for other features in the future ([#3723](https://github.com/vector-im/element-android/issues/3723))
Changes in Element v1.1.13 (2021-07-19)
=======================================

View File

@ -56,7 +56,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'com.google.android.material:material:1.4.0'

View File

@ -1 +0,0 @@
Avoid incomplete downloads in cache

View File

@ -0,0 +1,2 @@
Main changes in this version: fix an issue about encrypted messages.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.1.14

View File

@ -52,7 +52,7 @@ android {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
// Pref theme
implementation 'androidx.preference:preference-ktx:1.1.1'

View File

@ -35,7 +35,7 @@ android {
dependencies {
implementation project(":matrix-sdk-android")
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version"

View File

@ -112,7 +112,7 @@ dependencies {
def lifecycle_version = '2.2.0'
def arch_version = '2.1.0'
def markwon_version = '3.1.0'
def daggerVersion = '2.37'
def daggerVersion = '2.38'
def work_version = '2.5.0'
def retrofit_version = '2.9.0'
@ -120,7 +120,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.appcompat:appcompat:1.3.0"
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@ -169,7 +169,7 @@ dependencies {
implementation 'com.otaliastudios:transcoder:0.10.3'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.5.1'

View File

@ -0,0 +1,34 @@
/*
* 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.logger
/**
* Parent class for custom logger tags. Can be used with Timber :
*
* val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
* Timber.tag(loggerTag.value).v("My log message")
*/
open class LoggerTag(_value: String, parentTag: LoggerTag? = null) {
object VOIP : LoggerTag("VOIP")
val value: String = if (parentTag == null) {
_value
} else {
"${parentTag.value}/$_value"
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -39,12 +39,6 @@ fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) =
.build()
}
enum class RoomCategoryFilter {
ONLY_DM,
ONLY_ROOMS,
ALL
}
/**
* This class can be used to filter room summaries to use with:
* [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
@ -59,11 +53,10 @@ data class RoomSummaryQueryParams(
val excludeType: List<String?>?,
val includeType: List<String?>?,
val activeSpaceFilter: ActiveSpaceFilter?,
var activeGroupId: String? = null
val activeGroupId: String? = null
) {
class Builder {
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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<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.
* Default: false

View File

@ -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 = ""
)

View File

@ -314,6 +314,12 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
// Open the store
cryptoStore.open()
if (!cryptoStore.areDeviceKeysUploaded()) {
// Schedule upload of OTK
oneTimeKeysUploader.updateOneTimeKeyCount(0)
}
// this can throw if no network
tryOrNull {
uploadDeviceKeys()
@ -905,7 +911,7 @@ internal class DefaultCryptoService @Inject constructor(
* Upload my user's device keys.
*/
private suspend fun uploadDeviceKeys() {
if (cryptoStore.getDeviceKeysUploaded()) {
if (cryptoStore.areDeviceKeysUploaded()) {
Timber.d("Keys already uploaded, nothing to do")
return
}

View File

@ -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")
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")

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.model.MXKey
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
@ -77,6 +78,10 @@ internal class OneTimeKeysUploader @Inject constructor(
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
if (oneTimeKeyCount == null) {
// Ask the server how many otk he has
oneTimeKeyCount = fetchOtkCount()
}
val oneTimeKeyCountFromSync = oneTimeKeyCount
if (oneTimeKeyCountFromSync != null) {
// We need to keep a pool of one time public keys on the server so that
@ -90,17 +95,22 @@ internal class OneTimeKeysUploader @Inject constructor(
// private keys clogging up our local storage.
// So we need some kind of engineering compromise to balance all of
// these factors.
try {
tryOrNull("Unable to upload OTK") {
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
} finally {
oneTimeKeyCheckInProgress = false
}
} else {
Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
oneTimeKeyCheckInProgress = false
lastOneTimeKeyCheck = 0
}
oneTimeKeyCheckInProgress = false
}
private suspend fun fetchOtkCount(): Int? {
return tryOrNull("Unable to get OTK count") {
val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null))
result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
}
}
/**

View File

@ -475,7 +475,7 @@ internal interface IMXCryptoStore {
fun getGossipingEvents(): List<Event>
fun setDeviceKeysUploaded(uploaded: Boolean)
fun getDeviceKeysUploaded(): Boolean
fun areDeviceKeysUploaded(): Boolean
fun tidyUpDataBase()
fun logDbUsageInfo()
}

View File

@ -937,7 +937,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun getDeviceKeysUploaded(): Boolean {
override fun areDeviceKeysUploaded(): Boolean {
return doWithRealm(realmConfiguration) {
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
} ?: false

View File

@ -68,7 +68,7 @@ internal class VerificationTransportToDevice(
contentMap.setObject(otherUserId, it, keyReq)
}
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) {
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## verification [$tx.transactionId] send toDevice request success")
@ -124,7 +124,7 @@ internal class VerificationTransportToDevice(
contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) {
.configureWith(SendToDeviceTask.Params(type, contentMap)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.")
@ -155,7 +155,7 @@ internal class VerificationTransportToDevice(
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
onDone?.invoke()
@ -176,7 +176,7 @@ internal class VerificationTransportToDevice(
val contentMap = MXUsersDevicesMap<Any>()
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")

View File

@ -20,11 +20,14 @@ import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("CallEventProcessor", LoggerTag.VOIP)
@SessionScope
internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
: EventInsertLiveProcessor {
@ -71,14 +74,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
}
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
val now = System.currentTimeMillis()
event.roomId ?: return Unit.also {
Timber.w("Event with no room id ${event.eventId}")
}
val age = now - (event.ageLocalTs ?: now)
if (age > 40_000) {
// Too old to ring?
return
Timber.tag(loggerTag.value).w("Event with no room id ${event.eventId}")
}
callSignalingHandler.onCallEvent(event)
}

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.call
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
@ -36,6 +37,9 @@ import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("CallSignalingHandler", LoggerTag.VOIP)
private const val MAX_AGE_TO_RING = 40_000
@SessionScope
internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
private val mxCallFactory: MxCallFactory,
@ -111,12 +115,12 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
return
}
if (call.isOutgoing) {
Timber.v("Got selectAnswer for an outbound call: ignoring")
Timber.tag(loggerTag.value).v("Got selectAnswer for an outbound call: ignoring")
return
}
val selectedPartyId = content.selectedPartyId
if (selectedPartyId == null) {
Timber.w("Got nonsensical select_answer with null selected_party_id: ignoring")
Timber.tag(loggerTag.value).w("Got nonsensical select_answer with null selected_party_id: ignoring")
return
}
callListenersDispatcher.onCallSelectAnswerReceived(content)
@ -130,7 +134,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
return
}
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
Timber.tag(loggerTag.value).v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
return
}
callListenersDispatcher.onCallIceCandidateReceived(call, content)
@ -163,10 +167,10 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
// party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
Timber.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
}
if (call.state != CallState.Terminated) {
if (call.state !is CallState.Ended) {
activeCallHandler.removeCall(content.callId)
callListenersDispatcher.onCallHangupReceived(content)
}
@ -180,12 +184,18 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
if (event.roomId == null || event.senderId == null) {
return
}
val now = System.currentTimeMillis()
val age = now - (event.ageLocalTs ?: now)
if (age > MAX_AGE_TO_RING) {
Timber.tag(loggerTag.value).w("Call invite is too old to ring.")
return
}
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
content.callId ?: return
if (invitedCallIds.contains(content.callId)) {
// Call is already known, maybe due to fast lane. Ignore
Timber.d("Ignoring already known call invite")
Timber.tag(loggerTag.value).d("Ignoring already known call invite")
return
}
val incomingCall = mxCallFactory.createIncomingCall(
@ -214,7 +224,8 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
} else {
if (call.opponentPartyId != null) {
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
Timber.tag(loggerTag.value)
.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
return
}
mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
@ -231,7 +242,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
activeCallHandler.getCallWithId(it)
}
if (currentCall == null) {
Timber.v("Call with id $callId is null")
Timber.tag(loggerTag.value).v("Call with id $callId is null")
}
return currentCall
}

View File

@ -21,7 +21,6 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
@SessionScope
@ -51,7 +50,6 @@ internal class DefaultCallSignalingService @Inject constructor(
}
override fun getCallWithId(callId: String): MxCall? {
Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}")
return activeCallHandler.getCallWithId(callId)
}

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.call.model
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.call.CallIdGenerator
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
@ -38,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
@ -47,6 +49,8 @@ import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProces
import timber.log.Timber
import java.math.BigDecimal
private val loggerTag = LoggerTag("MxCallImpl", LoggerTag.VOIP)
internal class MxCallImpl(
override val callId: String,
override val isOutgoing: Boolean,
@ -93,7 +97,7 @@ internal class MxCallImpl(
try {
it.onStateUpdate(this)
} catch (failure: Throwable) {
Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
Timber.tag(loggerTag.value).d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
}
}
}
@ -109,7 +113,7 @@ internal class MxCallImpl(
override fun offerSdp(sdpString: String) {
if (!isOutgoing) return
Timber.v("## VOIP offerSdp $callId")
Timber.tag(loggerTag.value).v("offerSdp $callId")
state = CallState.Dialing
CallInviteContent(
callId = callId,
@ -124,7 +128,7 @@ internal class MxCallImpl(
}
override fun sendLocalCallCandidates(candidates: List<CallCandidate>) {
Timber.v("Send local call canditates $callId: $candidates")
Timber.tag(loggerTag.value).v("Send local call canditates $callId: $candidates")
CallCandidatesContent(
callId = callId,
partyId = ourPartyId,
@ -141,11 +145,11 @@ internal class MxCallImpl(
override fun reject() {
if (opponentVersion < 1) {
Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
hangUp()
Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
hangUp(EndCallReason.USER_HANGUP)
return
}
Timber.v("## VOIP reject $callId")
Timber.tag(loggerTag.value).v("reject $callId")
CallRejectContent(
callId = callId,
partyId = ourPartyId,
@ -153,24 +157,24 @@ 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?) {
Timber.v("## VOIP hangup $callId")
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) {
Timber.v("## VOIP accept $callId")
Timber.tag(loggerTag.value).v("accept $callId")
if (isOutgoing) return
state = CallState.Answering
CallAnswerContent(
@ -185,7 +189,7 @@ internal class MxCallImpl(
}
override fun negotiate(sdpString: String, type: SdpType) {
Timber.v("## VOIP negotiate $callId")
Timber.tag(loggerTag.value).v("negotiate $callId")
CallNegotiateContent(
callId = callId,
partyId = ourPartyId,
@ -198,7 +202,7 @@ internal class MxCallImpl(
}
override fun selectAnswer() {
Timber.v("## VOIP select answer $callId")
Timber.tag(loggerTag.value).v("select answer $callId")
if (isOutgoing) return
state = CallState.Answering
CallSelectAnswerContent(
@ -219,7 +223,7 @@ internal class MxCallImpl(
val profileInfo = try {
getProfileInfoTask.execute(profileInfoParams)
} catch (failure: Throwable) {
Timber.v("Fail fetching profile info of $targetUserId while transferring call")
Timber.tag(loggerTag.value).v("Fail fetching profile info of $targetUserId while transferring call")
null
}
CallReplacesContent(

View File

@ -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<Event> {
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

View File

@ -300,8 +300,8 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
}
}
if (queryParams.activeGroupId != null) {
query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!)
queryParams.activeGroupId?.let { activeGroupId ->
query.contains(RoomSummaryEntityFields.GROUP_IDS, activeGroupId)
}
return query
}

View File

@ -42,8 +42,8 @@ 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.appcompat:appcompat:1.3.1'
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation 'androidx.exifinterface:exifinterface:1.3.2'
// Log

View File

@ -14,7 +14,7 @@ kapt {
// Note: 2 digits max for each value
ext.versionMajor = 1
ext.versionMinor = 1
ext.versionPatch = 14
ext.versionPatch = 15
static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct'
@ -305,13 +305,13 @@ 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'
def glide_version = '4.12.0'
def moshi_version = '1.12.0'
def daggerVersion = '2.37'
def daggerVersion = '2.38'
def autofill_version = "1.1.0"
def work_version = '2.5.0'
def arch_version = '2.1.0'
@ -337,12 +337,12 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation "androidx.fragment:fragment-ktx:$fragment_version"
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"
@ -360,7 +360,7 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.6.0'
// Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'

View File

@ -37,16 +37,21 @@ import im.vector.app.features.home.AvatarRenderer
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
private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP)
/**
* Foreground service to manage calls
*/
class CallService : VectorService() {
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 notificationUtils: NotificationUtils
@ -91,7 +96,7 @@ class CallService : VectorService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.v("## VOIP onStartCommand $intent")
Timber.tag(loggerTag.value).v("onStartCommand $intent")
if (mediaSession == null) {
mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply {
setCallback(mediaSessionButtonCallback)
@ -148,15 +153,15 @@ class CallService : VectorService() {
*
*/
private fun displayIncomingCallNotification(intent: Intent) {
Timber.v("## VOIP displayIncomingCallNotification $intent")
Timber.tag(loggerTag.value).v("displayIncomingCallNotification $intent")
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
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.v("displayIncomingCallNotification : display the dedicated notification")
Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification")
val incomingCallAlert = IncomingCallAlert(callId,
shouldBeDisplayedIn = { activity ->
if (activity is VectorCallActivity) {
@ -165,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) },
@ -177,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()) {
@ -185,23 +190,32 @@ 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)) {
Timber.v("Call terminated for unknown call $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)
val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall)
notificationManager.notify(callId.hashCode(), notification)
if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) {
val missedCallNotification = notificationUtils.buildCallMissedNotification(terminatedCall)
notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), missedCallNotification)
}
}
private fun showCallScreen(call: WebRtcCall, mode: String) {
@ -218,51 +232,52 @@ class CallService : VectorService() {
val call = callManager.getCallById(callId) ?: return Unit.also {
handleUnexpectedState(callId)
}
val opponentMatrixItem = getOpponentMatrixItem(call)
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
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)
}
/**
* Display a call in progress notification.
*/
private fun displayCallInProgressNotification(intent: Intent) {
Timber.v("## VOIP displayCallInProgressNotification")
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?) {
Timber.v("Fallback to clear everything")
Timber.tag(loggerTag.value).v("Fallback to clear everything")
callRingPlayerIncoming?.stop()
callRingPlayerOutgoing?.stop()
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
@ -274,14 +289,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"
@ -294,6 +326,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,
@ -329,11 +363,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)
}

View File

@ -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

View File

@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.RoomDetailArgs
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
@ -71,6 +72,8 @@ data class CallArgs(
val isVideoCall: Boolean
) : Parcelable
private val loggerTag = LoggerTag("VectorCallActivity", LoggerTag.VOIP)
class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallControlsView.InteractionListener {
override fun getBinding() = ActivityCallBinding.inflate(layoutInflater)
@ -113,11 +116,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
if (intent.hasExtra(MvRx.KEY_ARG)) {
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
} else {
Timber.e("## VOIP missing callArgs for VectorCall Activity")
Timber.tag(loggerTag.value).e("missing callArgs for VectorCall Activity")
finish()
}
Timber.v("## VOIP EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
turnScreenOnAndKeyguardOff()
}
@ -160,7 +163,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
private fun renderState(state: VectorCallViewState) {
Timber.v("## VOIP renderState call $state")
Timber.tag(loggerTag.value).v("renderState call $state")
if (state.callState is Fail) {
finish()
return
@ -246,7 +249,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true
}
}
is CallState.Terminated -> {
is CallState.Ended -> {
finish()
}
null -> {
@ -309,7 +312,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
private fun start() {
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
Timber.v("## VOIP rootEglBase is null")
Timber.tag(loggerTag.value).v("rootEglBase is null")
finish()
}
@ -335,7 +338,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
private fun handleViewEvents(event: VectorCallViewEvents?) {
Timber.v("## VOIP handleViewEvents $event")
Timber.tag(loggerTag.value).v("handleViewEvents $event")
when (event) {
VectorCallViewEvents.DismissNoCall -> {
finish()
@ -357,7 +360,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
private fun onErrorTimoutConnect(turn: TurnServerResponse?) {
Timber.d("## VOIP onErrorTimoutConnect $turn")
Timber.tag(loggerTag.value).d("onErrorTimoutConnect $turn")
// TODO ask to use default stun, etc...
MaterialAlertDialogBuilder(this)
.setTitle(R.string.call_failed_no_connection)
@ -437,7 +440,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
// Needed to let you answer call when phone is locked
private fun turnScreenOnAndKeyguardOff() {
Timber.v("## VOIP turnScreenOnAndKeyguardOff")
Timber.tag(loggerTag.value).v("turnScreenOnAndKeyguardOff")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
@ -458,7 +461,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
}
private fun turnScreenOffAndKeyguardOn() {
Timber.v("## VOIP turnScreenOnAndKeyguardOn")
Timber.tag(loggerTag.value).v("turnScreenOnAndKeyguardOn")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)

View File

@ -25,9 +25,12 @@ import android.media.AudioManager
import androidx.core.content.getSystemService
import im.vector.app.core.services.BluetoothHeadsetReceiver
import im.vector.app.core.services.WiredHeadsetStateReceiver
import org.matrix.android.sdk.api.logger.LoggerTag
import timber.log.Timber
import java.util.HashSet
private val loggerTag = LoggerTag("API21AudioDeviceDetector", LoggerTag.VOIP)
internal class API21AudioDeviceDetector(private val context: Context,
private val audioManager: AudioManager,
private val callAudioManager: CallAudioManager
@ -62,17 +65,17 @@ internal class API21AudioDeviceDetector(private val context: Context,
}
private fun isBluetoothHeadsetOn(): Boolean {
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
Timber.tag(loggerTag.value).v("AudioManager isBluetoothHeadsetOn")
try {
if (connectedBlueToothHeadset == null) return false.also {
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
Timber.tag(loggerTag.value).v("AudioManager no connected bluetooth headset")
}
if (!audioManager.isBluetoothScoAvailableOffCall) return false.also {
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
Timber.tag(loggerTag.value).v("AudioManager isBluetoothScoAvailableOffCall false")
}
return true
} catch (failure: Throwable) {
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
Timber.e("AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
return false
}
}
@ -91,11 +94,11 @@ internal class API21AudioDeviceDetector(private val context: Context,
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
val bm: BluetoothManager? = context.getSystemService()
val adapter = bm?.adapter
Timber.d("## VOIP Bluetooth adapter $adapter")
Timber.tag(loggerTag.value).d("Bluetooth adapter $adapter")
bluetoothAdapter = adapter
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceDisconnected(profile: Int) {
Timber.d("## VOIP onServiceDisconnected $profile")
Timber.tag(loggerTag.value).d("onServiceDisconnected $profile")
if (profile == BluetoothProfile.HEADSET) {
connectedBlueToothHeadset = null
onAudioDeviceChange()
@ -103,7 +106,7 @@ internal class API21AudioDeviceDetector(private val context: Context,
}
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
Timber.tag(loggerTag.value).d("onServiceConnected $profile , proxy:$proxy")
if (profile == BluetoothProfile.HEADSET) {
connectedBlueToothHeadset = proxy
onAudioDeviceChange()
@ -122,12 +125,12 @@ internal class API21AudioDeviceDetector(private val context: Context,
}
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
Timber.v("onHeadsetEvent $event")
Timber.tag(loggerTag.value).v("onHeadsetEvent $event")
onAudioDeviceChange()
}
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
Timber.v("onBTHeadsetEvent $event")
Timber.tag(loggerTag.value).v("onBTHeadsetEvent $event")
onAudioDeviceChange()
}
}

View File

@ -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)
}
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.call.webrtc
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.webrtc.DataChannel
@ -25,10 +26,12 @@ import org.webrtc.PeerConnection
import org.webrtc.RtpReceiver
import timber.log.Timber
private val loggerTag = LoggerTag("PeerConnectionObserver", LoggerTag.VOIP)
class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer {
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
Timber.v("## VOIP StreamObserver onConnectionChange: $newState")
Timber.tag(loggerTag.value).v("StreamObserver onConnectionChange: $newState")
when (newState) {
/**
* Every ICE transport used by the connection is either in use (state "connected" or "completed")
@ -79,20 +82,20 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnectio
}
override fun onIceCandidate(iceCandidate: IceCandidate) {
Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate")
Timber.tag(loggerTag.value).v("StreamObserver onIceCandidate: $iceCandidate")
webRtcCall.onIceCandidate(iceCandidate)
}
override fun onDataChannel(dc: DataChannel) {
Timber.v("## VOIP StreamObserver onDataChannel: ${dc.state()}")
Timber.tag(loggerTag.value).v("StreamObserver onDataChannel: ${dc.state()}")
}
override fun onIceConnectionReceivingChange(receiving: Boolean) {
Timber.v("## VOIP StreamObserver onIceConnectionReceivingChange: $receiving")
Timber.tag(loggerTag.value).v("StreamObserver onIceConnectionReceivingChange: $receiving")
}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
Timber.v("## VOIP StreamObserver onIceConnectionChange IceConnectionState:$newState")
Timber.tag(loggerTag.value).v("StreamObserver onIceConnectionChange IceConnectionState:$newState")
when (newState) {
/**
* the ICE agent is gathering addresses or is waiting to be given remote candidates through
@ -145,29 +148,29 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnectio
}
override fun onAddStream(stream: MediaStream) {
Timber.v("## VOIP StreamObserver onAddStream: $stream")
Timber.tag(loggerTag.value).v("StreamObserver onAddStream: $stream")
webRtcCall.onAddStream(stream)
}
override fun onRemoveStream(stream: MediaStream) {
Timber.v("## VOIP StreamObserver onRemoveStream")
Timber.tag(loggerTag.value).v("StreamObserver onRemoveStream")
webRtcCall.onRemoveStream()
}
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
Timber.v("## VOIP StreamObserver onIceGatheringChange: $newState")
Timber.tag(loggerTag.value).v("StreamObserver onIceGatheringChange: $newState")
}
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
Timber.v("## VOIP StreamObserver onSignalingChange: $newState")
Timber.tag(loggerTag.value).v("StreamObserver onSignalingChange: $newState")
}
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
Timber.v("## VOIP StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}")
Timber.tag(loggerTag.value).v("StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}")
}
override fun onRenegotiationNeeded() {
Timber.v("## VOIP StreamObserver onRenegotiationNeeded")
Timber.tag(loggerTag.value).v("StreamObserver onRenegotiationNeeded")
webRtcCall.onRenegotiationNeeded(restartIce = false)
}
@ -178,6 +181,6 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnectio
* gets a new set of tracks because the media element being captured loaded a new source.
*/
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
Timber.v("## VOIP StreamObserver onAddTrack")
Timber.tag(loggerTag.value).v("StreamObserver onAddTrack")
}
}

View File

@ -1,70 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.webrtc
import org.webrtc.DataChannel
import org.webrtc.IceCandidate
import org.webrtc.MediaStream
import org.webrtc.PeerConnection
import org.webrtc.RtpReceiver
import timber.log.Timber
abstract class PeerConnectionObserverAdapter : PeerConnection.Observer {
override fun onIceCandidate(p0: IceCandidate?) {
Timber.v("## VOIP onIceCandidate $p0")
}
override fun onDataChannel(p0: DataChannel?) {
Timber.v("## VOIP onDataChannel $p0")
}
override fun onIceConnectionReceivingChange(p0: Boolean) {
Timber.v("## VOIP onIceConnectionReceivingChange $p0")
}
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
Timber.v("## VOIP onIceConnectionChange $p0")
}
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
Timber.v("## VOIP onIceConnectionChange $p0")
}
override fun onAddStream(mediaStream: MediaStream?) {
Timber.v("## VOIP onAddStream $mediaStream")
}
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
Timber.v("## VOIP onSignalingChange $p0")
}
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
Timber.v("## VOIP onIceCandidatesRemoved $p0")
}
override fun onRemoveStream(mediaStream: MediaStream?) {
Timber.v("## VOIP onRemoveStream $mediaStream")
}
override fun onRenegotiationNeeded() {
Timber.v("## VOIP onRenegotiationNeeded")
}
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
Timber.v("## VOIP onAddTrack $p0 / out: $p1")
}
}

View File

@ -45,6 +45,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallIdGenerator
import org.matrix.android.sdk.api.session.call.CallState
@ -57,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
@ -88,6 +92,8 @@ private const val AUDIO_TRACK_ID = "${STREAM_ID}a0"
private const val VIDEO_TRACK_ID = "${STREAM_ID}v0"
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
private val loggerTag = LoggerTag("WebRtcCall", LoggerTag.VOIP)
class WebRtcCall(
val mxCall: MxCall,
// This is where the call is placed from an ui perspective.
@ -99,7 +105,7 @@ class WebRtcCall(
private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallBecomeActive: (WebRtcCall) -> Unit,
private val onCallEnded: (String) -> Unit
private val onCallEnded: (String, EndCallReason, Boolean) -> Unit
) : MxCall.StateListener {
interface Listener : MxCall.StateListener {
@ -192,7 +198,7 @@ class WebRtcCall(
.subscribe {
// omit empty :/
if (it.isNotEmpty()) {
Timber.v("## Sending local ice candidates to call")
Timber.tag(loggerTag.value).v("Sending local ice candidates to call")
// it.forEach { peerConnection?.addIceCandidate(it) }
mxCall.sendLocalCallCandidates(it.mapToCallCandidate())
}
@ -210,7 +216,7 @@ class WebRtcCall(
fun onRenegotiationNeeded(restartIce: Boolean) {
sessionScope?.launch(dispatcher) {
if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) {
Timber.v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event")
Timber.tag(loggerTag.value).v("Opponent does not support renegotiation: ignoring onRenegotiationNeeded event")
return@launch
}
val constraints = MediaConstraints()
@ -218,7 +224,7 @@ class WebRtcCall(
constraints.mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
}
val peerConnection = peerConnection ?: return@launch
Timber.v("## VOIP creating offer...")
Timber.tag(loggerTag.value).v("creating offer...")
makingOffer = true
try {
val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch
@ -227,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) {
@ -238,7 +244,7 @@ class WebRtcCall(
}
} catch (failure: Throwable) {
// Need to handle error properly.
Timber.v("Failure while creating offer")
Timber.tag(loggerTag.value).v("Failure while creating offer")
} finally {
makingOffer = false
}
@ -267,7 +273,7 @@ class WebRtcCall(
}
}
}
Timber.v("## VOIP creating peer connection...with iceServers $iceServers ")
Timber.tag(loggerTag.value).v("creating peer connection...with iceServers $iceServers ")
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
}
@ -285,7 +291,7 @@ class WebRtcCall(
createCallId = CallIdGenerator.generate(),
awaitCallId = null
)
endCall(sendEndSignaling = false)
terminate(EndCallReason.REPLACED)
}
}
@ -307,14 +313,14 @@ class WebRtcCall(
createCallId = newCallId,
awaitCallId = null
)
endCall(sendEndSignaling = false)
transferTargetCall.endCall(sendEndSignaling = false)
terminate(EndCallReason.REPLACED)
transferTargetCall.terminate(EndCallReason.REPLACED)
}
}
fun acceptIncomingCall() {
sessionScope?.launch {
Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
Timber.tag(loggerTag.value).v("acceptIncomingCall from state ${mxCall.state}")
if (mxCall.state == CallState.LocalRinging) {
internalAcceptIncomingCall()
}
@ -333,7 +339,7 @@ class WebRtcCall(
sender.dtmf()?.insertDtmf(digit, 100, 70)
return@launch
} catch (failure: Throwable) {
Timber.v("Fail to send Dtmf digit")
Timber.tag(loggerTag.value).v("Fail to send Dtmf digit")
}
}
}
@ -342,7 +348,7 @@ class WebRtcCall(
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
localSurfaceRenderers.addIfNeeded(localViewRenderer)
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
when (mode) {
@ -389,7 +395,7 @@ class WebRtcCall(
}
private suspend fun detachRenderersInternal(renderers: List<SurfaceViewRenderer>?) = withContext(dispatcher) {
Timber.v("## VOIP detachRenderers")
Timber.tag(loggerTag.value).v("detachRenderers")
if (renderers.isNullOrEmpty()) {
// remove all sinks
localSurfaceRenderers.forEach {
@ -422,12 +428,12 @@ class WebRtcCall(
// 2. Access camera (if video call) + microphone, create local stream
createLocalStream()
attachViewRenderersInternal()
Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource")
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
Timber.v("## VOIP adding remote ice candidate $it")
Timber.tag(loggerTag.value).v("adding remote ice candidate $it")
peerConnection?.addIceCandidate(it)
}, {
Timber.v("## VOIP failed to add remote ice candidate $it")
Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it")
})
// Now we wait for negotiation callback
}
@ -453,15 +459,15 @@ class WebRtcCall(
SessionDescription(SessionDescription.Type.OFFER, it)
}
if (offerSdp == null) {
Timber.v("We don't have any offer to process")
Timber.tag(loggerTag.value).v("We don't have any offer to process")
return@withContext
}
Timber.v("Offer sdp for invite: ${offerSdp.description}")
Timber.tag(loggerTag.value).v("Offer sdp for invite: ${offerSdp.description}")
try {
peerConnection?.awaitSetRemoteDescription(offerSdp)
} catch (failure: Throwable) {
Timber.v("Failure putting remote description")
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
Timber.tag(loggerTag.value).v("Failure putting remote description")
endCall(reason = EndCallReason.UNKWOWN_ERROR)
return@withContext
}
// 2) Access camera + microphone, create local stream
@ -472,12 +478,12 @@ class WebRtcCall(
createAnswer()?.also {
mxCall.accept(it.description)
}
Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource")
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
Timber.v("## VOIP adding remote ice candidate $it")
Timber.tag(loggerTag.value).v("adding remote ice candidate $it")
peerConnection?.addIceCandidate(it)
}, {
Timber.v("## VOIP failed to add remote ice candidate $it")
Timber.tag(loggerTag.value).v("failed to add remote ice candidate $it")
})
}
@ -489,7 +495,7 @@ class WebRtcCall(
private fun createLocalStream() {
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
Timber.v("Create local stream for call ${mxCall.callId}")
Timber.tag(loggerTag.value).v("Create local stream for call ${mxCall.callId}")
configureAudioTrack(peerConnectionFactory)
// add video track if needed
if (mxCall.isVideoCall) {
@ -502,7 +508,7 @@ class WebRtcCall(
val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource)
audioTrack.setEnabled(true)
Timber.v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}")
Timber.tag(loggerTag.value).v("Add audio track $AUDIO_TRACK_ID to call ${mxCall.callId}")
peerConnection?.addTrack(audioTrack, listOf(STREAM_ID))
localAudioSource = audioSource
localAudioTrack = audioTrack
@ -544,7 +550,7 @@ class WebRtcCall(
override fun onCameraClosed() {
super.onCameraClosed()
Timber.v("onCameraClosed")
Timber.tag(loggerTag.value).v("onCameraClosed")
// This could happen if you open the camera app in chat
// We then register in order to restart capture as soon as the camera is available again
videoCapturerIsInError = true
@ -552,16 +558,16 @@ class WebRtcCall(
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
override fun onCameraUnavailable(cameraId: String) {
super.onCameraUnavailable(cameraId)
Timber.v("On camera unavailable: $cameraId")
Timber.tag(loggerTag.value).v("On camera unavailable: $cameraId")
}
override fun onCameraAccessPrioritiesChanged() {
super.onCameraAccessPrioritiesChanged()
Timber.v("onCameraAccessPrioritiesChanged")
Timber.tag(loggerTag.value).v("onCameraAccessPrioritiesChanged")
}
override fun onCameraAvailable(cameraId: String) {
Timber.v("On camera available: $cameraId")
Timber.tag(loggerTag.value).v("On camera available: $cameraId")
if (cameraId == camera.name) {
videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
cameraManager?.unregisterAvailabilityCallback(this)
@ -574,7 +580,7 @@ class WebRtcCall(
val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
Timber.v("## VOIP Local video source created")
Timber.tag(loggerTag.value).v("Local video source created")
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
// HD
@ -582,7 +588,7 @@ class WebRtcCall(
this.videoCapturer = videoCapturer
val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource)
Timber.v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}")
Timber.tag(loggerTag.value).v("Add video track $VIDEO_TRACK_ID to call ${mxCall.callId}")
videoTrack.setEnabled(true)
peerConnection?.addTrack(videoTrack, listOf(STREAM_ID))
localVideoSource = videoSource
@ -592,7 +598,7 @@ class WebRtcCall(
fun setCaptureFormat(format: CaptureFormat) {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP setCaptureFormat $format")
Timber.tag(loggerTag.value).v("setCaptureFormat $format")
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureFormat = format
}
@ -686,14 +692,14 @@ class WebRtcCall(
fun switchCamera() {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP switchCamera")
Timber.tag(loggerTag.value).v("switchCamera")
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
val oppositeCamera = getOppositeCameraIfAny() ?: return@launch
videoCapturer?.switchCamera(
object : CameraVideoCapturer.CameraSwitchHandler {
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
Timber.tag(loggerTag.value).v("onCameraSwitchDone isFront $isFrontCamera")
cameraInUse = oppositeCamera
localSurfaceRenderers.forEach {
it.get()?.setMirror(isFrontCamera)
@ -704,7 +710,7 @@ class WebRtcCall(
}
override fun onCameraSwitchError(errorDescription: String?) {
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
Timber.tag(loggerTag.value).v("onCameraSwitchError isFront $errorDescription")
}
}, oppositeCamera.name
)
@ -713,7 +719,7 @@ class WebRtcCall(
}
private suspend fun createAnswer(): SessionDescription? {
Timber.w("## VOIP createAnswer")
Timber.tag(loggerTag.value).w("createAnswer")
val peerConnection = peerConnection ?: return null
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
@ -724,7 +730,7 @@ class WebRtcCall(
peerConnection.awaitSetLocalDescription(localDescription)
localDescription
} catch (failure: Throwable) {
Timber.v("Fail to create answer")
Timber.tag(loggerTag.value).v("Fail to create answer")
null
}
}
@ -765,9 +771,9 @@ class WebRtcCall(
sessionScope?.launch(dispatcher) {
// reportError("Weird-looking stream: " + stream);
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
Timber.e("## VOIP StreamObserver weird looking stream: $stream")
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) {
@ -795,11 +801,22 @@ 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
}
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)
@ -807,18 +824,9 @@ class WebRtcCall(
val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
}
val wasRinging = mxCall.state is CallState.LocalRinging
mxCall.state = CallState.Terminated
mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP)
release()
onCallEnded(callId)
if (sendEndSignaling) {
if (wasRinging) {
mxCall.reject()
} else {
mxCall.hangUp(reason)
}
}
}
onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected)
}
// Call listener
@ -829,7 +837,7 @@ class WebRtcCall(
if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) {
return@forEach
}
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}")
Timber.tag(loggerTag.value).v("onCallIceCandidateReceived for call ${mxCall.callId} sdp: ${it.candidate}")
val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate)
remoteCandidateSource.onNext(iceCandidate)
}
@ -838,12 +846,12 @@ class WebRtcCall(
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
sessionScope?.launch(dispatcher) {
Timber.v("## VOIP onCallAnswerReceived ${callAnswerContent.callId}")
Timber.tag(loggerTag.value).v("onCallAnswerReceived ${callAnswerContent.callId}")
val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp)
try {
peerConnection?.awaitSetRemoteDescription(sdp)
} catch (failure: Throwable) {
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
endCall(EndCallReason.UNKWOWN_ERROR)
return@launch
}
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
@ -858,7 +866,7 @@ class WebRtcCall(
val type = description?.type
val sdpText = description?.sdp
if (type == null || sdpText == null) {
Timber.i("Ignoring invalid m.call.negotiate event")
Timber.tag(loggerTag.value).i("Ignoring invalid m.call.negotiate event")
return@launch
}
val peerConnection = peerConnection ?: return@launch
@ -873,7 +881,7 @@ class WebRtcCall(
ignoreOffer = !polite && offerCollision
if (ignoreOffer) {
Timber.i("Ignoring colliding negotiate event because we're impolite")
Timber.tag(loggerTag.value).i("Ignoring colliding negotiate event because we're impolite")
return@launch
}
val prevOnHold = computeIsLocalOnHold()
@ -886,7 +894,7 @@ class WebRtcCall(
}
}
} catch (failure: Throwable) {
Timber.e(failure, "Failed to complete negotiation")
Timber.tag(loggerTag.value).e(failure, "Failed to complete negotiation")
}
val nowOnHold = computeIsLocalOnHold()
wasLocalOnHold = nowOnHold
@ -904,12 +912,35 @@ 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
val newAssertedIdentity = callAssertedIdentityContent.assertedIdentity ?: return@launch
if (newAssertedIdentity.id == null && newAssertedIdentity.displayName == null) {
Timber.v("Asserted identity received with no relevant information, skip")
Timber.tag(loggerTag.value).v("Asserted identity received with no relevant information, skip")
return@launch
}
remoteAssertedIdentity = newAssertedIdentity

View File

@ -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() }
}
}
}

View File

@ -29,10 +29,13 @@ 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
import org.matrix.android.sdk.api.logger.LoggerTag
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallState
@ -45,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
@ -60,6 +64,8 @@ import javax.inject.Singleton
* Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes
* Use app context
*/
private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
@Singleton
class WebRtcCallManager @Inject constructor(
private val context: Context,
@ -75,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() {}
@ -184,7 +193,7 @@ class WebRtcCallManager @Inject constructor(
fun getAdvertisedCalls() = advertisedCalls
fun headSetButtonTapped() {
Timber.v("## VOIP headSetButtonTapped")
Timber.tag(loggerTag.value).v("headSetButtonTapped")
val call = getCurrentCall() ?: return
if (call.mxCall.state is CallState.LocalRinging) {
call.acceptIncomingCall()
@ -197,12 +206,12 @@ class WebRtcCallManager @Inject constructor(
private fun createPeerConnectionFactoryIfNeeded() {
if (peerConnectionFactory != null) return
Timber.v("## VOIP createPeerConnectionFactory")
Timber.tag(loggerTag.value).v("createPeerConnectionFactory")
val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also {
Timber.e("## VOIP No EGL BASE")
Timber.tag(loggerTag.value).e("No EGL BASE")
}
Timber.v("## VOIP PeerConnectionFactory.initialize")
Timber.tag(loggerTag.value).v("PeerConnectionFactory.initialize")
PeerConnectionFactory.initialize(PeerConnectionFactory
.InitializationOptions.builder(context.applicationContext)
.createInitializationOptions()
@ -216,7 +225,7 @@ class WebRtcCallManager @Inject constructor(
/* enableH264HighProfile */
true)
val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...")
Timber.tag(loggerTag.value).v("PeerConnectionFactory.createPeerConnectionFactory ...")
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(options)
.setVideoEncoderFactory(defaultVideoEncoderFactory)
@ -225,19 +234,19 @@ class WebRtcCallManager @Inject constructor(
}
private fun onCallActive(call: WebRtcCall) {
Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}")
Timber.tag(loggerTag.value).v("WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}")
val currentCall = getCurrentCall().takeIf { it != call }
currentCall?.updateRemoteOnHold(onHold = true)
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
this.currentCall.setAndNotify(call)
}
private fun onCallEnded(callId: String) {
Timber.v("## VOIP 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.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.nativeRoomId]?.remove(webRtcCall)
transferees.remove(callId)
@ -247,7 +256,7 @@ class WebRtcCallManager @Inject constructor(
}
// There is no active calls
if (getCurrentCall() == null) {
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
Timber.tag(loggerTag.value).v("Dispose peerConnectionFactory as there is no need to keep one")
peerConnectionFactory?.dispose()
peerConnectionFactory = null
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
@ -265,13 +274,13 @@ class WebRtcCallManager @Inject constructor(
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
Timber.tag(loggerTag.value).v("startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
Timber.w("## VOIP you already have a call in this room")
Timber.tag(loggerTag.value).w("you already have a call in this room")
return
}
if (getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) {
Timber.w("## VOIP cannot start outgoing call")
Timber.tag(loggerTag.value).w("cannot start outgoing call")
// Just ignore, maybe we could answer from other session?
return
}
@ -294,10 +303,10 @@ class WebRtcCallManager @Inject constructor(
}
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}")
Timber.tag(loggerTag.value).v("onCallIceCandidateReceived for call ${mxCall.callId}")
val call = callsByCallId[iceCandidatesContent.callId]
?: return Unit.also {
Timber.w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}")
Timber.tag(loggerTag.value).w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}")
}
call.onCallIceCandidateReceived(iceCandidatesContent)
}
@ -329,19 +338,19 @@ 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) {
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
Timber.tag(loggerTag.value).v("onCallInviteReceived callId ${mxCall.callId}")
val nativeRoomId = callUserMapper?.nativeRoomForVirtualRoom(mxCall.roomId) ?: mxCall.roomId
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
Timber.w("## VOIP you already have a call in this room")
Timber.tag(loggerTag.value).w("you already have a call in this room")
return
}
if ((getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) {
Timber.w("## VOIP receiving incoming call but cannot handle it")
Timber.tag(loggerTag.value).w("receiving incoming call but cannot handle it")
// Just ignore, maybe we could answer from other session?
return
}
@ -370,7 +379,7 @@ class WebRtcCallManager @Inject constructor(
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
val call = callsByCallId[callAnswerContent.callId]
?: return Unit.also {
Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}")
Timber.tag(loggerTag.value).w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}")
}
val mxCall = call.mxCall
// Update service state
@ -384,43 +393,38 @@ class WebRtcCallManager @Inject constructor(
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
val call = callsByCallId[callHangupContent.callId]
?: return Unit.also {
Timber.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) {
val call = callsByCallId[callRejectContent.callId]
?: return Unit.also {
Timber.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) {
val call = callsByCallId[callSelectAnswerContent.callId]
?: return Unit.also {
Timber.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)
Timber.tag(loggerTag.value).w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
}
call.onCallSelectedAnswerReceived(callSelectAnswerContent)
}
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
val call = callsByCallId[callNegotiateContent.callId]
?: return Unit.also {
Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}")
Timber.tag(loggerTag.value).w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}")
}
call.onCallNegotiateReceived(callNegotiateContent)
}
override fun onCallManagedByOtherSession(callId: String) {
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
onCallEnded(callId)
Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId")
onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false)
}
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
@ -429,7 +433,7 @@ class WebRtcCallManager @Inject constructor(
}
val call = callsByCallId[callAssertedIdentityContent.callId]
?: return Unit.also {
Timber.w("onCallAssertedIdentityReceived for non active call? ${callAssertedIdentityContent.callId}")
Timber.tag(loggerTag.value).w("onCallAssertedIdentityReceived for non active call? ${callAssertedIdentityContent.callId}")
}
call.onCallAssertedIdentityReceived(callAssertedIdentityContent)
}

View File

@ -20,4 +20,6 @@ import im.vector.app.features.home.RoomListDisplayMode
interface RoomListSectionBuilder {
fun buildSections(mode: RoomListDisplayMode) : List<RoomsSection>
fun dispose()
}

View File

@ -24,9 +24,8 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.showInvites
import io.reactivex.disposables.Disposable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.session.Session
@ -35,16 +34,16 @@ import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.rx.asObservable
class GroupRoomListSectionBuilder(
val session: Session,
val stringProvider: StringProvider,
val viewModelScope: CoroutineScope,
val appStateHandler: AppStateHandler,
class RoomListSectionBuilderGroup(
private val session: Session,
private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler,
private val autoAcceptInvites: AutoAcceptInvites,
val onDisposable: (Disposable) -> Unit,
val onUdpatable: (UpdatableLivePageResult) -> Unit
private val onUpdatable: (UpdatableLivePageResult) -> Unit
) : RoomListSectionBuilder {
private val disposables = CompositeDisposable()
override fun buildSections(mode: RoomListDisplayMode): List<RoomsSection> {
val activeGroupAwareQueries = mutableListOf<UpdatableLivePageResult>()
val sections = mutableListOf<RoomsSection>()
@ -52,7 +51,7 @@ class GroupRoomListSectionBuilder(
when (mode) {
RoomListDisplayMode.PEOPLE -> {
// 3 sections Invites / Fav / Dms
// 4 sections Invites / Fav / Dms / Low Priority
buildPeopleSections(sections, activeGroupAwareQueries, actualGroupId)
}
RoomListDisplayMode.ROOMS -> {
@ -69,7 +68,7 @@ class GroupRoomListSectionBuilder(
val name = stringProvider.getString(R.string.bottom_action_rooms)
session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
onUdpatable(updatableFilterLivePageResult)
onUpdatable(updatableFilterLivePageResult)
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
}
}
@ -88,6 +87,7 @@ class GroupRoomListSectionBuilder(
it.activeGroupId = actualGroupId
}
}
addSection(
sections,
activeGroupAwareQueries,
@ -111,8 +111,9 @@ class GroupRoomListSectionBuilder(
}
}
}.also {
onDisposable.invoke(it)
disposables.add(it)
}
return sections
}
@ -218,7 +219,19 @@ class GroupRoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, null, null)
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, null)
it.activeGroupId = actualGroupId
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.low_priority_header,
false
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, true, null)
it.activeGroupId = actualGroupId
}
}
@ -231,7 +244,6 @@ class GroupRoomListSectionBuilder(
withQueryParams(
{ query.invoke(it) },
{ roomQueryParams ->
val name = stringProvider.getString(nameRes)
session.getFilteredPagedRoomSummariesLive(roomQueryParams)
.also {
@ -246,8 +258,9 @@ class GroupRoomListSectionBuilder(
?.notificationCount
?.postValue(session.getNotificationCountForRooms(roomQueryParams))
}.also {
onDisposable.invoke(it)
disposables.add(it)
}
sections.add(
RoomsSection(
sectionName = name,
@ -267,4 +280,8 @@ class GroupRoomListSectionBuilder(
.build()
.let { block(it) }
}
override fun dispose() {
disposables.dispose()
}
}

View File

@ -30,7 +30,7 @@ import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.invite.showInvites
import im.vector.app.space
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
@ -46,19 +46,20 @@ import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
import org.matrix.android.sdk.rx.asObservable
class SpaceRoomListSectionBuilder(
val session: Session,
val stringProvider: StringProvider,
val appStateHandler: AppStateHandler,
val viewModelScope: CoroutineScope,
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
class RoomListSectionBuilderSpace(
private val session: Session,
private val stringProvider: StringProvider,
private val appStateHandler: AppStateHandler,
private val viewModelScope: CoroutineScope,
private val autoAcceptInvites: AutoAcceptInvites,
val onDisposable: (Disposable) -> Unit,
val onUdpatable: (UpdatableLivePageResult) -> Unit,
val onlyOrphansInHome: Boolean = false
private val onUpdatable: (UpdatableLivePageResult) -> Unit,
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
private val onlyOrphansInHome: Boolean = false
) : RoomListSectionBuilder {
val pagedListConfig = PagedList.Config.Builder()
private val disposables = CompositeDisposable()
private val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10)
.setInitialLoadSizeHint(20)
.setEnablePlaceholders(true)
@ -70,12 +71,15 @@ class SpaceRoomListSectionBuilder(
val activeSpaceAwareQueries = mutableListOf<RoomListViewModel.ActiveSpaceQueryUpdater>()
when (mode) {
RoomListDisplayMode.PEOPLE -> {
// 4 sections Invites / Fav / Dms / Low Priority
buildDmSections(sections, activeSpaceAwareQueries)
}
RoomListDisplayMode.ROOMS -> {
// 6 sections invites / Fav / Rooms / Low Priority / Server notice / Suggested rooms
buildRoomsSections(sections, activeSpaceAwareQueries)
}
RoomListDisplayMode.FILTERED -> {
// Used when searching for rooms
withQueryParams(
{
it.memberships = Membership.activeMemberships()
@ -84,7 +88,7 @@ class SpaceRoomListSectionBuilder(
val name = stringProvider.getString(R.string.bottom_action_rooms)
session.getFilteredPagedRoomSummariesLive(qpm)
.let { updatableFilterLivePageResult ->
onUdpatable(updatableFilterLivePageResult)
onUpdatable(updatableFilterLivePageResult)
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
}
}
@ -134,13 +138,14 @@ class SpaceRoomListSectionBuilder(
updater.updateForSpaceId(selectedSpace?.roomId)
}
}.also {
onDisposable.invoke(it)
disposables.add(it)
}
return sections
}
private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
private fun buildRoomsSections(sections: MutableList<RoomsSection>,
activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
if (autoAcceptInvites.showInvites()) {
addSection(
sections = sections,
@ -248,7 +253,7 @@ class SpaceRoomListSectionBuilder(
}.subscribe {
liveSuggestedRooms.postValue(it)
}.also {
onDisposable.invoke(it)
disposables.add(it)
}
sections.add(
RoomsSection(
@ -259,9 +264,11 @@ class SpaceRoomListSectionBuilder(
)
}
private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
private fun buildDmSections(sections: MutableList<RoomsSection>,
activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
if (autoAcceptInvites.showInvites()) {
addSection(sections = sections,
addSection(
sections = sections,
activeSpaceUpdaters = activeSpaceAwareQueries,
nameRes = R.string.invitations_header,
notifyOfLocalEcho = true,
@ -273,7 +280,8 @@ class SpaceRoomListSectionBuilder(
}
}
addSection(sections,
addSection(
sections,
activeSpaceAwareQueries,
R.string.bottom_action_favourites,
false,
@ -284,7 +292,8 @@ class SpaceRoomListSectionBuilder(
it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
}
addSection(sections,
addSection(
sections,
activeSpaceAwareQueries,
R.string.bottom_action_people_x,
false,
@ -292,7 +301,19 @@ class SpaceRoomListSectionBuilder(
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, null, null)
it.roomTagQueryFilter = RoomTagQueryFilter(false, false, null)
}
addSection(
sections,
activeSpaceAwareQueries,
R.string.low_priority_header,
false,
RoomListViewModel.SpaceFilterStrategy.ALL_IF_SPACE_NULL
) {
it.memberships = listOf(Membership.JOIN)
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
it.roomTagQueryFilter = RoomTagQueryFilter(false, true, null)
}
}
@ -306,7 +327,6 @@ class SpaceRoomListSectionBuilder(
withQueryParams(
{ query.invoke(it) },
{ roomQueryParams ->
val name = stringProvider.getString(nameRes)
session.getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
@ -349,7 +369,6 @@ class SpaceRoomListSectionBuilder(
}
}.livePagedList
.let { livePagedList ->
// use it also as a source to update count
livePagedList.asObservable()
.observeOn(Schedulers.computation())
@ -366,7 +385,7 @@ class SpaceRoomListSectionBuilder(
}
)
}.also {
onDisposable.invoke(it)
disposables.add(it)
}
sections.add(
@ -410,4 +429,8 @@ class SpaceRoomListSectionBuilder(
RoomListViewModel.SpaceFilterStrategy.NONE -> this
}
}
override fun dispose() {
disposables.dispose()
}
}

View File

@ -120,38 +120,32 @@ class RoomListViewModel @Inject constructor(
}
}
val sections: List<RoomsSection> by lazy {
if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
SpaceRoomListSectionBuilder(
private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
RoomListSectionBuilderSpace(
session,
stringProvider,
appStateHandler,
viewModelScope,
autoAcceptInvites,
{
updatableQuery = it
},
suggestedRoomJoiningState,
autoAcceptInvites,
{
it.disposeOnClear()
},
{
updatableQuery = it
},
vectorPreferences.labsSpacesOnlyOrphansInHome()
).buildSections(initialState.displayMode)
)
} else {
GroupRoomListSectionBuilder(
RoomListSectionBuilderGroup(
session,
stringProvider,
viewModelScope,
appStateHandler,
autoAcceptInvites,
{
it.disposeOnClear()
},
{
autoAcceptInvites
) {
updatableQuery = it
}
).buildSections(initialState.displayMode)
}
val sections: List<RoomsSection> by lazy {
roomListSectionBuilder.buildSections(initialState.displayMode)
}
override fun handle(action: RoomListAction) {
@ -341,4 +335,9 @@ class RoomListViewModel @Inject constructor(
_viewEvents.post(value)
}
}
override fun onCleared() {
super.onCleared()
roomListSectionBuilder.dispose()
}
}

View File

@ -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,52 @@ 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))
.setTimeoutAfter(2000)
.setSmallIcon(R.drawable.ic_material_call_end_grey)
.apply {
if (isVideoCall) {
setSmallIcon(R.drawable.ic_call_answer_video)
} else {
setSmallIcon(R.drawable.ic_call_answer)
}
}
// This is a trick to make the previous notification with same id disappear as cancel notification is not working with Foreground Service.
.setTimeoutAfter(1)
.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))

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -74,19 +74,19 @@
<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_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_allowed">• Server 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_not_allowed">• Server matching IP literals 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">• Servers matching %s 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">• 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_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_was_banned">• Server 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_was_allowed">• Server 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_not_allowed">• Server matching IP literals 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">• Servers matching %s were removed from the ban list.</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">• Servers matching %s were removed from the allowed list.</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">• 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_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_connecting">Call connecting…</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="incoming_call">Incoming Call</string>
<string name="incoming_video_call">Incoming Video Call</string>