diff --git a/CHANGES.md b/CHANGES.md index 79e1e07520..a5ef37fb55 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) ======================================= diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 0ed883cc95..c393c5f483 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -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' diff --git a/changelog.d/3656.bugfix b/changelog.d/3656.bugfix deleted file mode 100644 index 30d451558f..0000000000 --- a/changelog.d/3656.bugfix +++ /dev/null @@ -1 +0,0 @@ -Avoid incomplete downloads in cache diff --git a/fastlane/metadata/android/en-US/changelogs/40101140.txt b/fastlane/metadata/android/en-US/changelogs/40101140.txt new file mode 100644 index 0000000000..ee04069968 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40101140.txt @@ -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 \ No newline at end of file diff --git a/library/ui-styles/build.gradle b/library/ui-styles/build.gradle index 40360bbbe1..4d34195962 100644 --- a/library/ui-styles/build.gradle +++ b/library/ui-styles/build.gradle @@ -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' diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 0d4aa7fc84..899432b498 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -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" diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 484a2ddfe6..96c9fdb1e6 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt new file mode 100644 index 0000000000..51f9b50699 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt @@ -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" + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt index 2dbd1c9b01..47a63b4a25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.api.session.call +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason + sealed class CallState { /** Idle, setting up objects */ @@ -42,6 +44,6 @@ sealed class CallState { * */ data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState() - /** Terminated. Incoming/Outgoing call, the call is terminated */ - object Terminated : CallState() + /** Ended. Incoming/Outgoing call, the call is terminated */ + data class Ended(val reason: EndCallReason? = null) : CallState() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index fcc9f7072d..dd23e81cc6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.call import org.matrix.android.sdk.api.session.room.model.call.CallCandidate import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities -import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.EndCallReason import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional @@ -69,7 +69,7 @@ interface MxCall : MxCallDetail { /** * End the call */ - fun hangUp(reason: CallHangupContent.Reason? = null) + fun hangUp(reason: EndCallReason? = null) /** * Start a call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt index 88ec2de768..b440857518 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt @@ -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?, val includeType: List?, 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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index 9d6e1a7eae..31f801dd6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -43,29 +43,5 @@ data class CallHangupContent( * or `invite_timeout` for when the other party did not answer in time. * One of: ["ice_failed", "invite_timeout"] */ - @Json(name = "reason") val reason: Reason? = null -) : CallSignalingContent { - @JsonClass(generateAdapter = false) - enum class Reason { - @Json(name = "ice_failed") - ICE_FAILED, - - @Json(name = "ice_timeout") - ICE_TIMEOUT, - - @Json(name = "user_hangup") - USER_HANGUP, - - @Json(name = "replaced") - REPLACED, - - @Json(name = "user_media_failed") - USER_MEDIA_FAILED, - - @Json(name = "invite_timeout") - INVITE_TIMEOUT, - - @Json(name = "unknown_error") - UNKWOWN_ERROR - } -} + @Json(name = "reason") val reason: EndCallReason? = null +) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt index ea412fbe3e..1b9a7186e2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt @@ -36,5 +36,10 @@ data class CallRejectContent( /** * Required. The version of the VoIP specification this message adheres to. */ - @Json(name = "version") override val version: String? + @Json(name = "version") override val version: String?, + + /** + * Optional error reason for the reject. + */ + @Json(name = "reason") val reason: EndCallReason? = null ) : CallSignalingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt new file mode 100644 index 0000000000..60e038b2f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class EndCallReason { + @Json(name = "ice_failed") + ICE_FAILED, + + @Json(name = "ice_timeout") + ICE_TIMEOUT, + + @Json(name = "user_hangup") + USER_HANGUP, + + @Json(name = "replaced") + REPLACED, + + @Json(name = "user_media_failed") + USER_MEDIA_FAILED, + + @Json(name = "invite_timeout") + INVITE_TIMEOUT, + + @Json(name = "unknown_error") + UNKWOWN_ERROR, + + @Json(name = "user_busy") + USER_BUSY, + + @Json(name = "answered_elsewhere") + ANSWERED_ELSEWHERE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt index ca8c66bb3b..c46d7d0fd2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM -// TODO Give a way to include other initial states open class CreateRoomParams { /** * A public visibility indicates that the room will be shown in the published room list. @@ -103,6 +102,13 @@ open class CreateRoomParams { */ val creationContent = mutableMapOf() + /** + * A list of state events to set in the new room. This allows the user to override the default state events + * set in the new room. The expected format of the state events are an object with type, state_key and content keys set. + * Takes precedence over events set by preset, but gets overridden by name and topic keys. + */ + val initialStates = mutableListOf() + /** * Set to true to disable federation of this room. * Default: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt new file mode 100644 index 0000000000..fcfdc3e333 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.create + +import org.matrix.android.sdk.api.session.events.model.Content + +data class CreateRoomStateEvent( + /** + * Required. The type of event to send. + */ + val type: String, + + /** + * Required. The content of the event. + */ + val content: Content, + + /** + * The state_key of the state event. Defaults to an empty string. + */ + val stateKey: String = "" +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index d170ae3dd3..563c890950 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 63f15aaf6e..79910c6de2 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.crypto +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.auth.data.Credentials @@ -336,7 +337,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM downloadKeysForUsersTask.execute(params) } catch (throwable: Throwable) { Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error") - onKeysDownloadFailed(filteredUsers) + if (throwable is CancellationException) { + // the crypto module is getting closed, so we cannot access the DB anymore + Timber.w("The crypto module is closed, ignoring this error") + } else { + onKeysDownloadFailed(filteredUsers) + } throw throwable } Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt index 6695234d62..c4b62fe9fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt @@ -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) + } } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 181bd94cc7..3d12e74fcd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -475,7 +475,7 @@ internal interface IMXCryptoStore { fun getGossipingEvents(): List fun setDeviceKeysUploaded(uploaded: Boolean) - fun getDeviceKeysUploaded(): Boolean + fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun logDbUsageInfo() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 9ae93d61eb..9266f8fe7d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -937,7 +937,7 @@ internal class RealmCryptoStore @Inject constructor( } } - override fun getDeviceKeysUploaded(): Boolean { + override fun areDeviceKeysUploaded(): Boolean { return doWithRealm(realmConfiguration) { it.where().findFirst()?.deviceKeysSentToServer } ?: false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt index 0dbbe656c7..45f8143937 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt @@ -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 { 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 { 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() 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 { override fun onSuccess(data: Unit) { onDone?.invoke() @@ -176,7 +176,7 @@ internal class VerificationTransportToDevice( val contentMap = MXUsersDevicesMap() 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 { override fun onSuccess(data: Unit) { Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index 473adeb8d2..bdc254fc99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index b0901af719..59058bf976 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -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() ?: 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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index da1f84cc89..4a949e21a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index f101685a4b..9fc84e6fe5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -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) { - 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( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 86d2b70da1..2c04759b22 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -81,13 +81,14 @@ internal class CreateRoomBodyBuilder @Inject constructor( params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden } - val initialStates = listOfNotNull( + val initialStates = (listOfNotNull( buildEncryptionWithAlgorithmEvent(params), buildHistoryVisibilityEvent(params), buildAvatarEvent(params), buildGuestAccess(params), buildJoinRulesRestricted(params) ) + + buildCustomInitialStates(params)) .takeIf { it.isNotEmpty() } return CreateRoomBody( @@ -95,7 +96,7 @@ internal class CreateRoomBodyBuilder @Inject constructor( roomAliasName = params.roomAliasName, name = params.name, topic = params.topic, - invitedUserIds = params.invitedUserIds.filter { it != userId }, + invitedUserIds = params.invitedUserIds.filter { it != userId }.takeIf { it.isNotEmpty() }, invite3pids = invite3pids, creationContent = params.creationContent.takeIf { it.isNotEmpty() }, initialStates = initialStates, @@ -103,10 +104,19 @@ internal class CreateRoomBodyBuilder @Inject constructor( isDirect = params.isDirect, powerLevelContentOverride = params.powerLevelContentOverride, roomVersion = params.roomVersion - ) } + private fun buildCustomInitialStates(params: CreateRoomParams): List { + return params.initialStates.map { + Event( + type = it.type, + stateKey = it.stateKey, + content = it.content + ) + } + } + private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? { return params.avatarUri?.let { avatarUri -> // First upload the image, ignoring any error diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt index bff1af60ca..0b8c6df806 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt @@ -247,10 +247,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat queryParams.roomCategoryFilter?.let { when (it) { - RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) - RoomCategoryFilter.ALL -> { + RoomCategoryFilter.ALL -> { // nop } } @@ -274,15 +274,15 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it) } when (queryParams.roomCategoryFilter) { - RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) - RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) + RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) + RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false) RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0) - RoomCategoryFilter.ALL -> Unit // nop + RoomCategoryFilter.ALL -> Unit // nop } // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}") when (queryParams.activeSpaceFilter) { - is ActiveSpaceFilter.ActiveSpace -> { + is ActiveSpaceFilter.ActiveSpace -> { // It's annoying but for now realm java does not support querying in primitive list :/ // https://github.com/realm/realm-java/issues/5361 if (queryParams.activeSpaceFilter.currentSpaceId == null) { @@ -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 } diff --git a/multipicker/build.gradle b/multipicker/build.gradle index a993c452b0..04ce8a2aec 100644 --- a/multipicker/build.gradle +++ b/multipicker/build.gradle @@ -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 diff --git a/vector/build.gradle b/vector/build.gradle index 29f517a7e0..b57d7eef33 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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' diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 59eee14d37..d8cf8cf6b8 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -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() - private val knownCalls = mutableSetOf() + private val knownCalls = mutableSetOf() + private val connectedCallIds = mutableSetOf() 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) @@ -115,19 +120,19 @@ class CallService : VectorService() { callRingPlayerOutgoing?.start() displayOutgoingRingingCallNotification(intent) } - ACTION_ONGOING_CALL -> { + ACTION_ONGOING_CALL -> { callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() displayCallInProgressNotification(intent) } - ACTION_CALL_CONNECTING -> { + ACTION_CALL_CONNECTING -> { // lower notification priority displayCallInProgressNotification(intent) // stop ringing callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() } - ACTION_CALL_TERMINATED -> { + ACTION_CALL_TERMINATED -> { handleCallTerminated(intent) } else -> { @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 1a54551072..3742de6271 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -118,7 +118,7 @@ class CallControlsView @JvmOverloads constructor( views.connectedControls.isVisible = false } } - is CallState.Terminated, + is CallState.Ended, null -> { views.ringingControls.isVisible = false views.connectedControls.isVisible = false diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 7e84811102..a1e3717329 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -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(), CallControlsView.InteractionListener { override fun getBinding() = ActivityCallBinding.inflate(layoutInflater) @@ -113,11 +116,11 @@ class VectorCallActivity : VectorBaseActivity(), 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(), 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 @@ -196,7 +199,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true @@ -246,10 +249,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Ended -> { finish() } - null -> { + null -> { } } } @@ -309,7 +312,7 @@ class VectorCallActivity : VectorBaseActivity(), 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(), 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(), 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(), 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(), 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) diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt index 32b243aa2b..4f54f703b4 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt @@ -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() } } diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index 0f37ccaa29..0217551260 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -57,7 +57,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: private val call = callManager.getCallById(initialState.callId) private val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - if (call.state == CallState.Terminated) { + if (call.state is CallState.Ended) { _viewEvents.post(CallTransferViewEvents.Dismiss) } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt index f14bb2f849..99c26c5ebe 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt @@ -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) { - 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?) { - Timber.v("## VOIP StreamObserver onAddTrack") + Timber.tag(loggerTag.value).v("StreamObserver onAddTrack") } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt deleted file mode 100644 index 3d31f0e705..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserverAdapter.kt +++ /dev/null @@ -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?) { - 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?) { - Timber.v("## VOIP onAddTrack $p0 / out: $p1") - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 3259b0915f..91d3ab7ddf 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -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, private val peerConnectionFactoryProvider: Provider, private val onCallBecomeActive: (WebRtcCall) -> Unit, - private val onCallEnded: (String) -> Unit + private val onCallEnded: (String, EndCallReason, Boolean) -> Unit ) : MxCall.StateListener { interface Listener : MxCall.StateListener { @@ -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?) = 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,32 +801,34 @@ class WebRtcCall( } } - fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) { sessionScope?.launch(dispatcher) { - if (mxCall.state == CallState.Terminated) { + if (mxCall.state is CallState.Ended) { return@launch } - // Close tracks ASAP - localVideoTrack?.setEnabled(false) - localVideoTrack?.setEnabled(false) - cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> - val cameraManager = context.getSystemService()!! - cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) - } - val wasRinging = mxCall.state is CallState.LocalRinging - mxCall.state = CallState.Terminated - release() - onCallEnded(callId) - if (sendEndSignaling) { - if (wasRinging) { - mxCall.reject() - } else { - mxCall.hangUp(reason) - } + val reject = mxCall.state is CallState.LocalRinging + terminate(EndCallReason.USER_HANGUP, reject) + if (reject) { + mxCall.reject() + } else { + mxCall.hangUp(reason) } } } + private suspend fun terminate(reason: EndCallReason? = null, rejected: Boolean = false) = withContext(dispatcher) { + // Close tracks ASAP + localVideoTrack?.setEnabled(false) + localVideoTrack?.setEnabled(false) + cameraAvailabilityCallback?.let { cameraAvailabilityCallback -> + val cameraManager = context.getSystemService()!! + cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) + } + mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP) + release() + onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected) + } + // Call listener fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) { @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt index c99d097707..ef9ef3ef9a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt @@ -21,7 +21,12 @@ import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? { - return session.getRoomSummary(nativeRoomId)?.otherMemberIds?.firstOrNull()?.let { - session.getUser(it)?.toMatrixItem() + return session.getRoomSummary(nativeRoomId)?.let { roomSummary -> + // Fallback to RoomSummary if there is no other member. + if (roomSummary.otherMemberIds.isEmpty()) { + roomSummary.toMatrixItem() + } else { + roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() } + } } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 25463428e9..73a6c07d6a 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -29,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}") + Timber.tag(loggerTag.value).w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") } - val selectedPartyId = callSelectAnswerContent.selectedPartyId - if (selectedPartyId != call.mxCall.ourPartyId) { - Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${call.mxCall.ourPartyId}.") - // The other party has picked somebody else's answer - call.endCall(false) - } + call.onCallSelectedAnswerReceived(callSelectAnswerContent) } override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { 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,8 +433,8 @@ 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) + call.onCallAssertedIdentityReceived(callAssertedIdentityContent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt index 5267158000..019a6ceddf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt @@ -20,4 +20,6 @@ import im.vector.app.features.home.RoomListDisplayMode interface RoomListSectionBuilder { fun buildSections(mode: RoomListDisplayMode) : List + + fun dispose() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/GroupRoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt similarity index 90% rename from vector/src/main/java/im/vector/app/features/home/room/list/GroupRoomListSectionBuilder.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt index 106a02cd3c..f101669af3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/GroupRoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderGroup.kt @@ -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 { val activeGroupAwareQueries = mutableListOf() val sections = mutableListOf() @@ -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() + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceRoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt similarity index 90% rename from vector/src/main/java/im/vector/app/features/home/room/list/SpaceRoomListSectionBuilder.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt index 5a296ce7ed..13a6fc0d2d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/SpaceRoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilderSpace.kt @@ -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>>, +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>>, + 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() 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, activeSpaceAwareQueries: MutableList) { + private fun buildRoomsSections(sections: MutableList, + activeSpaceAwareQueries: MutableList) { 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, activeSpaceAwareQueries: MutableList) { + private fun buildDmSections(sections: MutableList, + activeSpaceAwareQueries: MutableList) { 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() + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index c5f166ea5b..845be0b18b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -120,40 +120,34 @@ class RoomListViewModel @Inject constructor( } } - val sections: List by lazy { - if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) { - SpaceRoomListSectionBuilder( - session, - stringProvider, - appStateHandler, - viewModelScope, - suggestedRoomJoiningState, - autoAcceptInvites, - { - it.disposeOnClear() - }, - { - updatableQuery = it - }, - vectorPreferences.labsSpacesOnlyOrphansInHome() - ).buildSections(initialState.displayMode) - } else { - GroupRoomListSectionBuilder( - session, - stringProvider, - viewModelScope, - appStateHandler, - autoAcceptInvites, - { - it.disposeOnClear() - }, - { - updatableQuery = it - } - ).buildSections(initialState.displayMode) + private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) { + RoomListSectionBuilderSpace( + session, + stringProvider, + appStateHandler, + viewModelScope, + autoAcceptInvites, + { + updatableQuery = it + }, + suggestedRoomJoiningState, + vectorPreferences.labsSpacesOnlyOrphansInHome() + ) + } else { + RoomListSectionBuilderGroup( + session, + stringProvider, + appStateHandler, + autoAcceptInvites + ) { + updatableQuery = it } } + val sections: List by lazy { + roomListSectionBuilder.buildSections(initialState.displayMode) + } + override fun handle(action: RoomListAction) { when (action) { is RoomListAction.SelectRoom -> handleSelectRoom(action) @@ -341,4 +335,9 @@ class RoomListViewModel @Inject constructor( _viewEvents.post(value) } } + + override fun onCleared() { + super.onCleared() + roomListSectionBuilder.dispose() + } } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 439705c9d6..cf13150e59 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.services.CallService import im.vector.app.core.utils.startNotificationChannelSettingsIntent import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.service.CallHeadsUpActionReceiver @@ -298,12 +299,14 @@ class NotificationUtils @Inject constructor(private val context: Context, .apply { if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.incoming_video_call)) + setSmallIcon(R.drawable.ic_call_answer_video) } else { setContentText(stringProvider.getString(R.string.incoming_voice_call)) + setSmallIcon(R.drawable.ic_call_answer) } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setLights(accentColor, 500, 500) .setOngoing(true) @@ -339,8 +342,6 @@ class NotificationUtils @Inject constructor(private val context: Context, builder.addAction( NotificationCompat.Action( R.drawable.ic_call_answer, - // IconCompat.createWithResource(applicationContext, R.drawable.ic_call) - // .setTint(ContextCompat.getColor(applicationContext, R.color.vctr_positive_accent)), getActionText(R.string.call_notification_answer, R.attr.colorPrimary), answerCallPendingIntent ) @@ -360,10 +361,15 @@ class NotificationUtils @Inject constructor(private val context: Context, .setContentTitle(ensureTitleNotEmpty(title)) .apply { setContentText(stringProvider.getString(R.string.call_ring)) + if (call.mxCall.isVideoCall) { + setSmallIcon(R.drawable.ic_call_answer_video) + } else { + setSmallIcon(R.drawable.ic_call_answer) + } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) .setCategory(NotificationCompat.CATEGORY_CALL) .setLights(accentColor, 500, 500) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setOngoing(true) val contentIntent = VectorCallActivity.newIntent( @@ -407,11 +413,13 @@ class NotificationUtils @Inject constructor(private val context: Context, .apply { if (call.mxCall.isVideoCall) { setContentText(stringProvider.getString(R.string.video_call_in_progress)) + setSmallIcon(R.drawable.ic_call_answer_video) } else { setContentText(stringProvider.getString(R.string.call_in_progress)) + setSmallIcon(R.drawable.ic_call_answer) } } - .setSmallIcon(R.drawable.incoming_call_notification_transparent) + .setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary)) .setCategory(NotificationCompat.CATEGORY_CALL) val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId) @@ -450,15 +458,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)) diff --git a/vector/src/main/res/drawable/ic_missed_video_call.xml b/vector/src/main/res/drawable/ic_missed_video_call.xml new file mode 100644 index 0000000000..555e15a371 --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_video_call.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_missed_video_call_small.xml b/vector/src/main/res/drawable/ic_missed_video_call_small.xml new file mode 100644 index 0000000000..c703f0cfca --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_video_call_small.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/drawable/ic_missed_voice_call.xml b/vector/src/main/res/drawable/ic_missed_voice_call.xml new file mode 100644 index 0000000000..dc869fa8bc --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_voice_call.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_missed_voice_call_small.xml b/vector/src/main/res/drawable/ic_missed_voice_call_small.xml new file mode 100644 index 0000000000..21d8b309c6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_missed_voice_call_small.xml @@ -0,0 +1,5 @@ + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index ef25329eed..e3385765a8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -74,19 +74,19 @@ You upgraded here. %s set the server ACLs for this room. You set the server ACLs for this room. - • Server matching %s are banned. - • Server matching %s are allowed. - • Server matching IP literals are allowed. - • Server matching IP literals are banned. + • Servers matching %s are banned. + • Servers matching %s are allowed. + • Servers matching IP literals are allowed. + • Servers matching IP literals are banned. %s changed the server ACLs for this room. You changed the server ACLs for this room. - • Server matching %s are now banned. - • Server matching %s were removed from the ban list. - • Server matching %s are now allowed. - • Server matching %s were removed from the allowed list. - • Server matching IP literals are now allowed. - • Server matching IP literals are now banned. + • Servers matching %s are now banned. + • Servers matching %s were removed from the ban list. + • Servers matching %s are now allowed. + • Servers matching %s were removed from the allowed list. + • Servers matching IP literals are now allowed. + • Servers matching IP literals are now banned. No change. 🎉 All servers are banned from participating! This room can no longer be used. @@ -727,6 +727,14 @@ Call connected Call connecting… Call ended + + Missed audio call + %d missed audio calls + + + Missed video call + %d missed video calls + Calling… Incoming Call Incoming Video Call