Merge branch 'release/1.1.14' into main
This commit is contained in:
commit
0bc007c2c3
25
CHANGES.md
25
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)
|
Changes in Element v1.1.13 (2021-07-19)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ dependencies {
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
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 "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
|
|
|
@ -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
|
|
@ -52,7 +52,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.4.0'
|
||||||
// Pref theme
|
// Pref theme
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
|
|
|
@ -35,7 +35,7 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(":matrix-sdk-android")
|
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:rxkotlin:2.4.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlin_coroutines_version"
|
||||||
|
|
|
@ -112,7 +112,7 @@ dependencies {
|
||||||
def lifecycle_version = '2.2.0'
|
def lifecycle_version = '2.2.0'
|
||||||
def arch_version = '2.1.0'
|
def arch_version = '2.1.0'
|
||||||
def markwon_version = '3.1.0'
|
def markwon_version = '3.1.0'
|
||||||
def daggerVersion = '2.37'
|
def daggerVersion = '2.38'
|
||||||
def work_version = '2.5.0'
|
def work_version = '2.5.0'
|
||||||
def retrofit_version = '2.9.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-core:$kotlin_coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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.core:core-ktx:1.6.0"
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||||
|
@ -169,7 +169,7 @@ dependencies {
|
||||||
implementation 'com.otaliastudios:transcoder:0.10.3'
|
implementation 'com.otaliastudios:transcoder:0.10.3'
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// 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 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.api.session.call
|
package org.matrix.android.sdk.api.session.call
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
|
|
||||||
sealed class CallState {
|
sealed class CallState {
|
||||||
|
|
||||||
/** Idle, setting up objects */
|
/** Idle, setting up objects */
|
||||||
|
@ -42,6 +44,6 @@ sealed class CallState {
|
||||||
* */
|
* */
|
||||||
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
|
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
|
||||||
|
|
||||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
/** Ended. Incoming/Outgoing call, the call is terminated */
|
||||||
object Terminated : CallState()
|
data class Ended(val reason: EndCallReason? = null) : CallState()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.call
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ interface MxCall : MxCallDetail {
|
||||||
/**
|
/**
|
||||||
* End the call
|
* End the call
|
||||||
*/
|
*/
|
||||||
fun hangUp(reason: CallHangupContent.Reason? = null)
|
fun hangUp(reason: EndCallReason? = null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a call
|
* Start a call
|
||||||
|
|
|
@ -39,12 +39,6 @@ fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) =
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RoomCategoryFilter {
|
|
||||||
ONLY_DM,
|
|
||||||
ONLY_ROOMS,
|
|
||||||
ALL
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class can be used to filter room summaries to use with:
|
* 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]
|
* [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
|
||||||
|
@ -59,11 +53,10 @@ data class RoomSummaryQueryParams(
|
||||||
val excludeType: List<String?>?,
|
val excludeType: List<String?>?,
|
||||||
val includeType: List<String?>?,
|
val includeType: List<String?>?,
|
||||||
val activeSpaceFilter: ActiveSpaceFilter?,
|
val activeSpaceFilter: ActiveSpaceFilter?,
|
||||||
var activeGroupId: String? = null
|
val activeGroupId: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
class Builder {
|
class Builder {
|
||||||
|
|
||||||
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||||
|
|
|
@ -43,29 +43,5 @@ data class CallHangupContent(
|
||||||
* or `invite_timeout` for when the other party did not answer in time.
|
* or `invite_timeout` for when the other party did not answer in time.
|
||||||
* One of: ["ice_failed", "invite_timeout"]
|
* One of: ["ice_failed", "invite_timeout"]
|
||||||
*/
|
*/
|
||||||
@Json(name = "reason") val reason: Reason? = null
|
@Json(name = "reason") val reason: EndCallReason? = null
|
||||||
) : CallSignalingContent {
|
) : CallSignalingContent
|
||||||
@JsonClass(generateAdapter = false)
|
|
||||||
enum class Reason {
|
|
||||||
@Json(name = "ice_failed")
|
|
||||||
ICE_FAILED,
|
|
||||||
|
|
||||||
@Json(name = "ice_timeout")
|
|
||||||
ICE_TIMEOUT,
|
|
||||||
|
|
||||||
@Json(name = "user_hangup")
|
|
||||||
USER_HANGUP,
|
|
||||||
|
|
||||||
@Json(name = "replaced")
|
|
||||||
REPLACED,
|
|
||||||
|
|
||||||
@Json(name = "user_media_failed")
|
|
||||||
USER_MEDIA_FAILED,
|
|
||||||
|
|
||||||
@Json(name = "invite_timeout")
|
|
||||||
INVITE_TIMEOUT,
|
|
||||||
|
|
||||||
@Json(name = "unknown_error")
|
|
||||||
UNKWOWN_ERROR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,5 +36,10 @@ data class CallRejectContent(
|
||||||
/**
|
/**
|
||||||
* Required. The version of the VoIP specification this message adheres to.
|
* Required. The version of the VoIP specification this message adheres to.
|
||||||
*/
|
*/
|
||||||
@Json(name = "version") override val version: String?
|
@Json(name = "version") override val version: String?,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional error reason for the reject.
|
||||||
|
*/
|
||||||
|
@Json(name = "reason") val reason: EndCallReason? = null
|
||||||
) : CallSignalingContent
|
) : CallSignalingContent
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.room.model.call
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = false)
|
||||||
|
enum class EndCallReason {
|
||||||
|
@Json(name = "ice_failed")
|
||||||
|
ICE_FAILED,
|
||||||
|
|
||||||
|
@Json(name = "ice_timeout")
|
||||||
|
ICE_TIMEOUT,
|
||||||
|
|
||||||
|
@Json(name = "user_hangup")
|
||||||
|
USER_HANGUP,
|
||||||
|
|
||||||
|
@Json(name = "replaced")
|
||||||
|
REPLACED,
|
||||||
|
|
||||||
|
@Json(name = "user_media_failed")
|
||||||
|
USER_MEDIA_FAILED,
|
||||||
|
|
||||||
|
@Json(name = "invite_timeout")
|
||||||
|
INVITE_TIMEOUT,
|
||||||
|
|
||||||
|
@Json(name = "unknown_error")
|
||||||
|
UNKWOWN_ERROR,
|
||||||
|
|
||||||
|
@Json(name = "user_busy")
|
||||||
|
USER_BUSY,
|
||||||
|
|
||||||
|
@Json(name = "answered_elsewhere")
|
||||||
|
ANSWERED_ELSEWHERE
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
|
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
|
||||||
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
|
||||||
// TODO Give a way to include other initial states
|
|
||||||
open class CreateRoomParams {
|
open class CreateRoomParams {
|
||||||
/**
|
/**
|
||||||
* A public visibility indicates that the room will be shown in the published room list.
|
* A public visibility indicates that the room will be shown in the published room list.
|
||||||
|
@ -103,6 +102,13 @@ open class CreateRoomParams {
|
||||||
*/
|
*/
|
||||||
val creationContent = mutableMapOf<String, Any>()
|
val creationContent = mutableMapOf<String, Any>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of state events to set in the new room. This allows the user to override the default state events
|
||||||
|
* set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
|
||||||
|
* Takes precedence over events set by preset, but gets overridden by name and topic keys.
|
||||||
|
*/
|
||||||
|
val initialStates = mutableListOf<CreateRoomStateEvent>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set to true to disable federation of this room.
|
* Set to true to disable federation of this room.
|
||||||
* Default: false
|
* Default: false
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.api.session.room.model.create
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Content
|
||||||
|
|
||||||
|
data class CreateRoomStateEvent(
|
||||||
|
/**
|
||||||
|
* Required. The type of event to send.
|
||||||
|
*/
|
||||||
|
val type: String,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required. The content of the event.
|
||||||
|
*/
|
||||||
|
val content: Content,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state_key of the state event. Defaults to an empty string.
|
||||||
|
*/
|
||||||
|
val stateKey: String = ""
|
||||||
|
)
|
|
@ -314,6 +314,12 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
|
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
|
||||||
// Open the store
|
// Open the store
|
||||||
cryptoStore.open()
|
cryptoStore.open()
|
||||||
|
|
||||||
|
if (!cryptoStore.areDeviceKeysUploaded()) {
|
||||||
|
// Schedule upload of OTK
|
||||||
|
oneTimeKeysUploader.updateOneTimeKeyCount(0)
|
||||||
|
}
|
||||||
|
|
||||||
// this can throw if no network
|
// this can throw if no network
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
uploadDeviceKeys()
|
uploadDeviceKeys()
|
||||||
|
@ -905,7 +911,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* Upload my user's device keys.
|
* Upload my user's device keys.
|
||||||
*/
|
*/
|
||||||
private suspend fun uploadDeviceKeys() {
|
private suspend fun uploadDeviceKeys() {
|
||||||
if (cryptoStore.getDeviceKeysUploaded()) {
|
if (cryptoStore.areDeviceKeysUploaded()) {
|
||||||
Timber.d("Keys already uploaded, nothing to do")
|
Timber.d("Keys already uploaded, nothing to do")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
package org.matrix.android.sdk.internal.crypto
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.matrix.android.sdk.api.MatrixPatterns
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.auth.data.Credentials
|
import org.matrix.android.sdk.api.auth.data.Credentials
|
||||||
|
@ -336,7 +337,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||||
downloadKeysForUsersTask.execute(params)
|
downloadKeysForUsersTask.execute(params)
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
|
||||||
|
if (throwable is CancellationException) {
|
||||||
|
// the crypto module is getting closed, so we cannot access the DB anymore
|
||||||
|
Timber.w("The crypto module is closed, ignoring this error")
|
||||||
|
} else {
|
||||||
onKeysDownloadFailed(filteredUsers)
|
onKeysDownloadFailed(filteredUsers)
|
||||||
|
}
|
||||||
throw throwable
|
throw throwable
|
||||||
}
|
}
|
||||||
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.crypto
|
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.MXKey
|
||||||
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
|
||||||
import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
|
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
|
// discard the oldest private keys first. This will eventually clean
|
||||||
// out stale private keys that won't receive a message.
|
// out stale private keys that won't receive a message.
|
||||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
||||||
|
if (oneTimeKeyCount == null) {
|
||||||
|
// Ask the server how many otk he has
|
||||||
|
oneTimeKeyCount = fetchOtkCount()
|
||||||
|
}
|
||||||
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
||||||
if (oneTimeKeyCountFromSync != null) {
|
if (oneTimeKeyCountFromSync != null) {
|
||||||
// We need to keep a pool of one time public keys on the server so that
|
// 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.
|
// private keys clogging up our local storage.
|
||||||
// So we need some kind of engineering compromise to balance all of
|
// So we need some kind of engineering compromise to balance all of
|
||||||
// these factors.
|
// these factors.
|
||||||
try {
|
tryOrNull("Unable to upload OTK") {
|
||||||
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
||||||
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
||||||
} finally {
|
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
|
Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
|
||||||
oneTimeKeyCheckInProgress = false
|
|
||||||
lastOneTimeKeyCheck = 0
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -475,7 +475,7 @@ internal interface IMXCryptoStore {
|
||||||
fun getGossipingEvents(): List<Event>
|
fun getGossipingEvents(): List<Event>
|
||||||
|
|
||||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||||
fun getDeviceKeysUploaded(): Boolean
|
fun areDeviceKeysUploaded(): Boolean
|
||||||
fun tidyUpDataBase()
|
fun tidyUpDataBase()
|
||||||
fun logDbUsageInfo()
|
fun logDbUsageInfo()
|
||||||
}
|
}
|
||||||
|
|
|
@ -937,7 +937,7 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDeviceKeysUploaded(): Boolean {
|
override fun areDeviceKeysUploaded(): Boolean {
|
||||||
return doWithRealm(realmConfiguration) {
|
return doWithRealm(realmConfiguration) {
|
||||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
|
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
|
@ -68,7 +68,7 @@ internal class VerificationTransportToDevice(
|
||||||
contentMap.setObject(otherUserId, it, keyReq)
|
contentMap.setObject(otherUserId, it, keyReq)
|
||||||
}
|
}
|
||||||
sendToDeviceTask
|
sendToDeviceTask
|
||||||
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) {
|
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.v("## verification [$tx.transactionId] send toDevice request success")
|
Timber.v("## verification [$tx.transactionId] send toDevice request success")
|
||||||
|
@ -124,7 +124,7 @@ internal class VerificationTransportToDevice(
|
||||||
contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject)
|
contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject)
|
||||||
|
|
||||||
sendToDeviceTask
|
sendToDeviceTask
|
||||||
.configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) {
|
.configureWith(SendToDeviceTask.Params(type, contentMap)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.")
|
Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.")
|
||||||
|
@ -155,7 +155,7 @@ internal class VerificationTransportToDevice(
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
|
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
|
||||||
sendToDeviceTask
|
sendToDeviceTask
|
||||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
|
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
onDone?.invoke()
|
onDone?.invoke()
|
||||||
|
@ -176,7 +176,7 @@ internal class VerificationTransportToDevice(
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
|
contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
|
||||||
sendToDeviceTask
|
sendToDeviceTask
|
||||||
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
|
.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
|
||||||
this.callback = object : MatrixCallback<Unit> {
|
this.callback = object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
|
Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
|
||||||
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
|
import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
|
||||||
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
|
||||||
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
|
||||||
import org.matrix.android.sdk.internal.util.md5
|
import org.matrix.android.sdk.internal.util.md5
|
||||||
import org.matrix.android.sdk.internal.util.writeToFile
|
import org.matrix.android.sdk.internal.util.writeToFile
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -96,6 +97,9 @@ internal class DefaultFileService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var atomicFileDownload: AtomicFileCreator? = null
|
||||||
|
var atomicFileDecrypt: AtomicFileCreator? = null
|
||||||
|
|
||||||
if (existingDownload != null) {
|
if (existingDownload != null) {
|
||||||
// FIXME If the first downloader cancels then we'll unfortunately be cancelled too.
|
// FIXME If the first downloader cancels then we'll unfortunately be cancelled too.
|
||||||
return existingDownload.await()
|
return existingDownload.await()
|
||||||
|
@ -131,8 +135,11 @@ internal class DefaultFileService @Inject constructor(
|
||||||
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
|
||||||
|
|
||||||
// Write the file to cache (encrypted version if the file is encrypted)
|
// Write the file to cache (encrypted version if the file is encrypted)
|
||||||
writeToFile(source.inputStream(), cachedFiles.file)
|
// Write to a part file first, so if we abort before done, we don't have a broken cached file
|
||||||
|
val atomicFileCreator = AtomicFileCreator(cachedFiles.file).also { atomicFileDownload = it }
|
||||||
|
writeToFile(source.inputStream(), atomicFileCreator.partFile)
|
||||||
response.close()
|
response.close()
|
||||||
|
atomicFileCreator.commit()
|
||||||
} else {
|
} else {
|
||||||
Timber.v("## FileService: cache hit for $url")
|
Timber.v("## FileService: cache hit for $url")
|
||||||
}
|
}
|
||||||
|
@ -145,8 +152,10 @@ internal class DefaultFileService @Inject constructor(
|
||||||
Timber.v("## FileService: decrypt file")
|
Timber.v("## FileService: decrypt file")
|
||||||
// Ensure the parent folder exists
|
// Ensure the parent folder exists
|
||||||
cachedFiles.decryptedFile.parentFile?.mkdirs()
|
cachedFiles.decryptedFile.parentFile?.mkdirs()
|
||||||
|
// Write to a part file first, so if we abort before done, we don't have a broken cached file
|
||||||
|
val atomicFileCreator = AtomicFileCreator(cachedFiles.decryptedFile).also { atomicFileDecrypt = it }
|
||||||
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
|
val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
|
||||||
cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
|
atomicFileCreator.partFile.outputStream().buffered().use { outputStream ->
|
||||||
MXEncryptedAttachments.decryptAttachment(
|
MXEncryptedAttachments.decryptAttachment(
|
||||||
inputStream,
|
inputStream,
|
||||||
elementToDecrypt,
|
elementToDecrypt,
|
||||||
|
@ -154,6 +163,7 @@ internal class DefaultFileService @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
atomicFileCreator.commit()
|
||||||
if (!decryptSuccess) {
|
if (!decryptSuccess) {
|
||||||
throw IllegalStateException("Decryption error")
|
throw IllegalStateException("Decryption error")
|
||||||
}
|
}
|
||||||
|
@ -174,6 +184,11 @@ internal class DefaultFileService @Inject constructor(
|
||||||
}
|
}
|
||||||
toNotify?.completeWith(result)
|
toNotify?.completeWith(result)
|
||||||
|
|
||||||
|
result.onFailure {
|
||||||
|
atomicFileDownload?.cancel()
|
||||||
|
atomicFileDecrypt?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
return result.getOrThrow()
|
return result.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.Event
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
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.EventInsertLiveProcessor
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("CallEventProcessor", LoggerTag.VOIP)
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
|
internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
|
||||||
: EventInsertLiveProcessor {
|
: EventInsertLiveProcessor {
|
||||||
|
@ -71,14 +74,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
|
private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
event.roomId ?: return Unit.also {
|
event.roomId ?: return Unit.also {
|
||||||
Timber.w("Event with no room id ${event.eventId}")
|
Timber.tag(loggerTag.value).w("Event with no room id ${event.eventId}")
|
||||||
}
|
|
||||||
val age = now - (event.ageLocalTs ?: now)
|
|
||||||
if (age > 40_000) {
|
|
||||||
// Too old to ring?
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
callSignalingHandler.onCallEvent(event)
|
callSignalingHandler.onCallEvent(event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.matrix.android.sdk.internal.session.call
|
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.CallListener
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
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 timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("CallSignalingHandler", LoggerTag.VOIP)
|
||||||
|
private const val MAX_AGE_TO_RING = 40_000
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
|
internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
|
||||||
private val mxCallFactory: MxCallFactory,
|
private val mxCallFactory: MxCallFactory,
|
||||||
|
@ -111,12 +115,12 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (call.isOutgoing) {
|
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
|
return
|
||||||
}
|
}
|
||||||
val selectedPartyId = content.selectedPartyId
|
val selectedPartyId = content.selectedPartyId
|
||||||
if (selectedPartyId == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
callListenersDispatcher.onCallSelectAnswerReceived(content)
|
callListenersDispatcher.onCallSelectAnswerReceived(content)
|
||||||
|
@ -130,7 +134,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
callListenersDispatcher.onCallIceCandidateReceived(call, content)
|
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
|
// 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)
|
// a partner yet but we're treating the hangup as a reject as per VoIP v0)
|
||||||
if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (call.state != CallState.Terminated) {
|
if (call.state !is CallState.Ended) {
|
||||||
activeCallHandler.removeCall(content.callId)
|
activeCallHandler.removeCall(content.callId)
|
||||||
callListenersDispatcher.onCallHangupReceived(content)
|
callListenersDispatcher.onCallHangupReceived(content)
|
||||||
}
|
}
|
||||||
|
@ -180,12 +184,18 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
if (event.roomId == null || event.senderId == null) {
|
if (event.roomId == null || event.senderId == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val age = now - (event.ageLocalTs ?: now)
|
||||||
|
if (age > MAX_AGE_TO_RING) {
|
||||||
|
Timber.tag(loggerTag.value).w("Call invite is too old to ring.")
|
||||||
|
return
|
||||||
|
}
|
||||||
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
|
val content = event.getClearContent().toModel<CallInviteContent>() ?: return
|
||||||
|
|
||||||
content.callId ?: return
|
content.callId ?: return
|
||||||
if (invitedCallIds.contains(content.callId)) {
|
if (invitedCallIds.contains(content.callId)) {
|
||||||
// Call is already known, maybe due to fast lane. Ignore
|
// 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
|
return
|
||||||
}
|
}
|
||||||
val incomingCall = mxCallFactory.createIncomingCall(
|
val incomingCall = mxCallFactory.createIncomingCall(
|
||||||
|
@ -214,7 +224,8 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
callListenersDispatcher.onCallManagedByOtherSession(content.callId)
|
||||||
} else {
|
} else {
|
||||||
if (call.opponentPartyId != null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
|
mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
|
||||||
|
@ -231,7 +242,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
activeCallHandler.getCallWithId(it)
|
activeCallHandler.getCallWithId(it)
|
||||||
}
|
}
|
||||||
if (currentCall == null) {
|
if (currentCall == null) {
|
||||||
Timber.v("Call with id $callId is null")
|
Timber.tag(loggerTag.value).v("Call with id $callId is null")
|
||||||
}
|
}
|
||||||
return currentCall
|
return currentCall
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.MxCall
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -51,7 +50,6 @@ internal class DefaultCallSignalingService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCallWithId(callId: String): MxCall? {
|
override fun getCallWithId(callId: String): MxCall? {
|
||||||
Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}")
|
|
||||||
return activeCallHandler.getCallWithId(callId)
|
return activeCallHandler.getCallWithId(callId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package org.matrix.android.sdk.internal.session.call.model
|
package org.matrix.android.sdk.internal.session.call.model
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
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.CallIdGenerator
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
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.CallReplacesContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
||||||
|
@ -47,6 +49,8 @@ import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProces
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("MxCallImpl", LoggerTag.VOIP)
|
||||||
|
|
||||||
internal class MxCallImpl(
|
internal class MxCallImpl(
|
||||||
override val callId: String,
|
override val callId: String,
|
||||||
override val isOutgoing: Boolean,
|
override val isOutgoing: Boolean,
|
||||||
|
@ -93,7 +97,7 @@ internal class MxCallImpl(
|
||||||
try {
|
try {
|
||||||
it.onStateUpdate(this)
|
it.onStateUpdate(this)
|
||||||
} catch (failure: Throwable) {
|
} 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) {
|
override fun offerSdp(sdpString: String) {
|
||||||
if (!isOutgoing) return
|
if (!isOutgoing) return
|
||||||
Timber.v("## VOIP offerSdp $callId")
|
Timber.tag(loggerTag.value).v("offerSdp $callId")
|
||||||
state = CallState.Dialing
|
state = CallState.Dialing
|
||||||
CallInviteContent(
|
CallInviteContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
|
@ -124,7 +128,7 @@ internal class MxCallImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendLocalCallCandidates(candidates: List<CallCandidate>) {
|
override fun sendLocalCallCandidates(candidates: List<CallCandidate>) {
|
||||||
Timber.v("Send local call canditates $callId: $candidates")
|
Timber.tag(loggerTag.value).v("Send local call canditates $callId: $candidates")
|
||||||
CallCandidatesContent(
|
CallCandidatesContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
|
@ -141,11 +145,11 @@ internal class MxCallImpl(
|
||||||
|
|
||||||
override fun reject() {
|
override fun reject() {
|
||||||
if (opponentVersion < 1) {
|
if (opponentVersion < 1) {
|
||||||
Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
|
Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
|
||||||
hangUp()
|
hangUp(EndCallReason.USER_HANGUP)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Timber.v("## VOIP reject $callId")
|
Timber.tag(loggerTag.value).v("reject $callId")
|
||||||
CallRejectContent(
|
CallRejectContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
|
@ -153,24 +157,24 @@ internal class MxCallImpl(
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
state = CallState.Terminated
|
state = CallState.Ended(reason = EndCallReason.USER_HANGUP)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hangUp(reason: CallHangupContent.Reason?) {
|
override fun hangUp(reason: EndCallReason?) {
|
||||||
Timber.v("## VOIP hangup $callId")
|
Timber.tag(loggerTag.value).v("hangup $callId")
|
||||||
CallHangupContent(
|
CallHangupContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
|
reason = reason,
|
||||||
version = MxCall.VOIP_PROTO_VERSION.toString()
|
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
state = CallState.Terminated
|
state = CallState.Ended(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun accept(sdpString: String) {
|
override fun accept(sdpString: String) {
|
||||||
Timber.v("## VOIP accept $callId")
|
Timber.tag(loggerTag.value).v("accept $callId")
|
||||||
if (isOutgoing) return
|
if (isOutgoing) return
|
||||||
state = CallState.Answering
|
state = CallState.Answering
|
||||||
CallAnswerContent(
|
CallAnswerContent(
|
||||||
|
@ -185,7 +189,7 @@ internal class MxCallImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun negotiate(sdpString: String, type: SdpType) {
|
override fun negotiate(sdpString: String, type: SdpType) {
|
||||||
Timber.v("## VOIP negotiate $callId")
|
Timber.tag(loggerTag.value).v("negotiate $callId")
|
||||||
CallNegotiateContent(
|
CallNegotiateContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
|
@ -198,7 +202,7 @@ internal class MxCallImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun selectAnswer() {
|
override fun selectAnswer() {
|
||||||
Timber.v("## VOIP select answer $callId")
|
Timber.tag(loggerTag.value).v("select answer $callId")
|
||||||
if (isOutgoing) return
|
if (isOutgoing) return
|
||||||
state = CallState.Answering
|
state = CallState.Answering
|
||||||
CallSelectAnswerContent(
|
CallSelectAnswerContent(
|
||||||
|
@ -219,7 +223,7 @@ internal class MxCallImpl(
|
||||||
val profileInfo = try {
|
val profileInfo = try {
|
||||||
getProfileInfoTask.execute(profileInfoParams)
|
getProfileInfoTask.execute(profileInfoParams)
|
||||||
} catch (failure: Throwable) {
|
} 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
|
null
|
||||||
}
|
}
|
||||||
CallReplacesContent(
|
CallReplacesContent(
|
||||||
|
|
|
@ -81,13 +81,14 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
|
params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
|
||||||
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
|
params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
|
||||||
}
|
}
|
||||||
val initialStates = listOfNotNull(
|
val initialStates = (listOfNotNull(
|
||||||
buildEncryptionWithAlgorithmEvent(params),
|
buildEncryptionWithAlgorithmEvent(params),
|
||||||
buildHistoryVisibilityEvent(params),
|
buildHistoryVisibilityEvent(params),
|
||||||
buildAvatarEvent(params),
|
buildAvatarEvent(params),
|
||||||
buildGuestAccess(params),
|
buildGuestAccess(params),
|
||||||
buildJoinRulesRestricted(params)
|
buildJoinRulesRestricted(params)
|
||||||
)
|
)
|
||||||
|
+ buildCustomInitialStates(params))
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
return CreateRoomBody(
|
return CreateRoomBody(
|
||||||
|
@ -95,7 +96,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
roomAliasName = params.roomAliasName,
|
roomAliasName = params.roomAliasName,
|
||||||
name = params.name,
|
name = params.name,
|
||||||
topic = params.topic,
|
topic = params.topic,
|
||||||
invitedUserIds = params.invitedUserIds.filter { it != userId },
|
invitedUserIds = params.invitedUserIds.filter { it != userId }.takeIf { it.isNotEmpty() },
|
||||||
invite3pids = invite3pids,
|
invite3pids = invite3pids,
|
||||||
creationContent = params.creationContent.takeIf { it.isNotEmpty() },
|
creationContent = params.creationContent.takeIf { it.isNotEmpty() },
|
||||||
initialStates = initialStates,
|
initialStates = initialStates,
|
||||||
|
@ -103,10 +104,19 @@ internal class CreateRoomBodyBuilder @Inject constructor(
|
||||||
isDirect = params.isDirect,
|
isDirect = params.isDirect,
|
||||||
powerLevelContentOverride = params.powerLevelContentOverride,
|
powerLevelContentOverride = params.powerLevelContentOverride,
|
||||||
roomVersion = params.roomVersion
|
roomVersion = params.roomVersion
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildCustomInitialStates(params: CreateRoomParams): List<Event> {
|
||||||
|
return params.initialStates.map {
|
||||||
|
Event(
|
||||||
|
type = it.type,
|
||||||
|
stateKey = it.stateKey,
|
||||||
|
content = it.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
|
||||||
return params.avatarUri?.let { avatarUri ->
|
return params.avatarUri?.let { avatarUri ->
|
||||||
// First upload the image, ignoring any error
|
// First upload the image, ignoring any error
|
||||||
|
|
|
@ -300,8 +300,8 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.activeGroupId != null) {
|
queryParams.activeGroupId?.let { activeGroupId ->
|
||||||
query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!)
|
query.contains(RoomSummaryEntityFields.GROUP_IDS, activeGroupId)
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.internal.util.file
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal class AtomicFileCreator(private val file: File) {
|
||||||
|
val partFile = File(file.parentFile, "${file.name}.part")
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (file.exists()) {
|
||||||
|
Timber.w("## AtomicFileCreator: target file ${file.path} exists, it should not happen.")
|
||||||
|
}
|
||||||
|
if (partFile.exists()) {
|
||||||
|
Timber.d("## AtomicFileCreator: discard aborted part file ${partFile.path}")
|
||||||
|
// No need to delete the file, we will overwrite it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
partFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commit() {
|
||||||
|
partFile.renameTo(file)
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,8 +42,8 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
implementation "androidx.fragment:fragment-ktx:1.3.6"
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
implementation 'androidx.exifinterface:exifinterface:1.3.2'
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
|
|
|
@ -14,7 +14,7 @@ kapt {
|
||||||
// Note: 2 digits max for each value
|
// Note: 2 digits max for each value
|
||||||
ext.versionMajor = 1
|
ext.versionMajor = 1
|
||||||
ext.versionMinor = 1
|
ext.versionMinor = 1
|
||||||
ext.versionPatch = 13
|
ext.versionPatch = 14
|
||||||
|
|
||||||
static def getGitTimestamp() {
|
static def getGitTimestamp() {
|
||||||
def cmd = 'git show -s --format=%ct'
|
def cmd = 'git show -s --format=%ct'
|
||||||
|
@ -305,13 +305,13 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
def epoxy_version = '4.6.2'
|
def epoxy_version = '4.6.2'
|
||||||
def fragment_version = '1.3.5'
|
def fragment_version = '1.3.6'
|
||||||
def arrow_version = "0.8.2"
|
def arrow_version = "0.8.2"
|
||||||
def markwon_version = '4.1.2'
|
def markwon_version = '4.1.2'
|
||||||
def big_image_viewer_version = '1.8.0'
|
def big_image_viewer_version = '1.8.0'
|
||||||
def glide_version = '4.12.0'
|
def glide_version = '4.12.0'
|
||||||
def moshi_version = '1.12.0'
|
def moshi_version = '1.12.0'
|
||||||
def daggerVersion = '2.37'
|
def daggerVersion = '2.38'
|
||||||
def autofill_version = "1.1.0"
|
def autofill_version = "1.1.0"
|
||||||
def work_version = '2.5.0'
|
def work_version = '2.5.0'
|
||||||
def arch_version = '2.1.0'
|
def arch_version = '2.1.0'
|
||||||
|
@ -337,12 +337,12 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||||
|
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
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.fragment:fragment-ktx:$fragment_version"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||||
implementation 'androidx.core:core-ktx:1.6.0'
|
implementation 'androidx.core:core-ktx:1.6.0'
|
||||||
implementation "androidx.media:media:1.3.1"
|
implementation "androidx.media:media:1.4.0"
|
||||||
implementation "androidx.transition:transition:1.4.1"
|
implementation "androidx.transition:transition:1.4.1"
|
||||||
|
|
||||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||||
|
@ -360,7 +360,7 @@ dependencies {
|
||||||
implementation 'com.facebook.stetho:stetho:1.6.0'
|
implementation 'com.facebook.stetho:stetho:1.6.0'
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28'
|
||||||
|
|
||||||
// rx
|
// rx
|
||||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||||
|
|
|
@ -37,16 +37,21 @@ import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
import im.vector.app.features.popup.IncomingCallAlert
|
import im.vector.app.features.popup.IncomingCallAlert
|
||||||
import im.vector.app.features.popup.PopupAlertManager
|
import im.vector.app.features.popup.PopupAlertManager
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("CallService", LoggerTag.VOIP)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Foreground service to manage calls
|
* Foreground service to manage calls
|
||||||
*/
|
*/
|
||||||
class CallService : VectorService() {
|
class CallService : VectorService() {
|
||||||
|
|
||||||
private val connections = mutableMapOf<String, CallConnection>()
|
private val connections = mutableMapOf<String, CallConnection>()
|
||||||
private val knownCalls = mutableSetOf<String>()
|
private val knownCalls = mutableSetOf<CallInformation>()
|
||||||
|
private val connectedCallIds = mutableSetOf<String>()
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManagerCompat
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
private lateinit var notificationUtils: NotificationUtils
|
private lateinit var notificationUtils: NotificationUtils
|
||||||
|
@ -91,7 +96,7 @@ class CallService : VectorService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
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) {
|
if (mediaSession == null) {
|
||||||
mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply {
|
mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply {
|
||||||
setCallback(mediaSessionButtonCallback)
|
setCallback(mediaSessionButtonCallback)
|
||||||
|
@ -148,15 +153,15 @@ class CallService : VectorService() {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
private fun displayIncomingCallNotification(intent: Intent) {
|
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 callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
|
val callInformation = call.toCallInformation()
|
||||||
val isVideoCall = call.mxCall.isVideoCall
|
val isVideoCall = call.mxCall.isVideoCall
|
||||||
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
Timber.tag(loggerTag.value).v("displayIncomingCallNotification : display the dedicated notification")
|
||||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
|
||||||
val incomingCallAlert = IncomingCallAlert(callId,
|
val incomingCallAlert = IncomingCallAlert(callId,
|
||||||
shouldBeDisplayedIn = { activity ->
|
shouldBeDisplayedIn = { activity ->
|
||||||
if (activity is VectorCallActivity) {
|
if (activity is VectorCallActivity) {
|
||||||
|
@ -165,7 +170,7 @@ class CallService : VectorService() {
|
||||||
}
|
}
|
||||||
).apply {
|
).apply {
|
||||||
viewBinder = IncomingCallAlert.ViewBinder(
|
viewBinder = IncomingCallAlert.ViewBinder(
|
||||||
matrixItem = opponentMatrixItem,
|
matrixItem = callInformation.opponentMatrixItem,
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
isVideoCall = isVideoCall,
|
isVideoCall = isVideoCall,
|
||||||
onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) },
|
onAccept = { showCallScreen(call, VectorCallActivity.INCOMING_ACCEPT) },
|
||||||
|
@ -177,7 +182,7 @@ class CallService : VectorService() {
|
||||||
alertManager.postVectorAlert(incomingCallAlert)
|
alertManager.postVectorAlert(incomingCallAlert)
|
||||||
val notification = notificationUtils.buildIncomingCallNotification(
|
val notification = notificationUtils.buildIncomingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId,
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId,
|
||||||
fromBg = fromBg
|
fromBg = fromBg
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
|
@ -185,23 +190,32 @@ class CallService : VectorService() {
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCallTerminated(intent: Intent) {
|
private fun handleCallTerminated(intent: Intent) {
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason
|
||||||
|
val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false)
|
||||||
alertManager.cancelAlert(callId)
|
alertManager.cancelAlert(callId)
|
||||||
if (!knownCalls.remove(callId)) {
|
val terminatedCall = knownCalls.firstOrNull { it.callId == callId }
|
||||||
Timber.v("Call terminated for unknown call $callId$")
|
if (terminatedCall == null) {
|
||||||
|
Timber.tag(loggerTag.value).v("Call terminated for unknown call $callId$")
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
knownCalls.remove(terminatedCall)
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
mediaSession?.isActive = false
|
mediaSession?.isActive = false
|
||||||
myStopSelf()
|
myStopSelf()
|
||||||
}
|
}
|
||||||
|
val wasConnected = connectedCallIds.remove(callId)
|
||||||
|
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) {
|
private fun showCallScreen(call: WebRtcCall, mode: String) {
|
||||||
|
@ -218,51 +232,52 @@ class CallService : VectorService() {
|
||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
val callInformation = call.toCallInformation()
|
||||||
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
Timber.tag(loggerTag.value).v("displayOutgoingCallNotification : display the dedicated notification")
|
||||||
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
startForeground(callId.hashCode(), notification)
|
startForeground(callId.hashCode(), notification)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a call in progress notification.
|
* Display a call in progress notification.
|
||||||
*/
|
*/
|
||||||
private fun displayCallInProgressNotification(intent: Intent) {
|
private fun displayCallInProgressNotification(intent: Intent) {
|
||||||
Timber.v("## VOIP displayCallInProgressNotification")
|
Timber.tag(loggerTag.value).v("displayCallInProgressNotification")
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
connectedCallIds.add(callId)
|
||||||
val call = callManager.getCallById(callId) ?: return Unit.also {
|
val call = callManager.getCallById(callId) ?: return Unit.also {
|
||||||
handleUnexpectedState(callId)
|
handleUnexpectedState(callId)
|
||||||
}
|
}
|
||||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
|
||||||
alertManager.cancelAlert(callId)
|
alertManager.cancelAlert(callId)
|
||||||
|
val callInformation = call.toCallInformation()
|
||||||
val notification = notificationUtils.buildPendingCallNotification(
|
val notification = notificationUtils.buildPendingCallNotification(
|
||||||
call = call,
|
call = call,
|
||||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
title = callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId
|
||||||
)
|
)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
startForeground(callId.hashCode(), notification)
|
startForeground(callId.hashCode(), notification)
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(callId.hashCode(), notification)
|
notificationManager.notify(callId.hashCode(), notification)
|
||||||
}
|
}
|
||||||
knownCalls.add(callId)
|
knownCalls.add(callInformation)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnexpectedState(callId: String?) {
|
private fun handleUnexpectedState(callId: String?) {
|
||||||
Timber.v("Fallback to clear everything")
|
Timber.tag(loggerTag.value).v("Fallback to clear everything")
|
||||||
callRingPlayerIncoming?.stop()
|
callRingPlayerIncoming?.stop()
|
||||||
callRingPlayerOutgoing?.stop()
|
callRingPlayerOutgoing?.stop()
|
||||||
if (callId != null) {
|
if (callId != null) {
|
||||||
notificationManager.cancel(callId.hashCode())
|
notificationManager.cancel(callId.hashCode())
|
||||||
}
|
}
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
val notification = notificationUtils.buildCallEndedNotification(false)
|
||||||
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
||||||
if (knownCalls.isEmpty()) {
|
if (knownCalls.isEmpty()) {
|
||||||
mediaSession?.isActive = false
|
mediaSession?.isActive = false
|
||||||
|
@ -274,14 +289,31 @@ class CallService : VectorService() {
|
||||||
connections[callConnection.callId] = callConnection
|
connections[callConnection.callId] = callConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getOpponentMatrixItem(call: WebRtcCall): MatrixItem? {
|
private fun WebRtcCall.toCallInformation(): CallInformation {
|
||||||
return vectorComponent().activeSessionHolder().getSafeActiveSession()?.let {
|
return CallInformation(
|
||||||
call.getOpponentAsMatrixItem(it)
|
callId = this.callId,
|
||||||
}
|
nativeRoomId = this.nativeRoomId,
|
||||||
|
opponentUserId = this.mxCall.opponentUserId,
|
||||||
|
opponentMatrixItem = vectorComponent().activeSessionHolder().getSafeActiveSession()?.let {
|
||||||
|
this.getOpponentAsMatrixItem(it)
|
||||||
|
},
|
||||||
|
isVideoCall = this.mxCall.isVideoCall,
|
||||||
|
isOutgoing = this.mxCall.isOutgoing
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class CallInformation(
|
||||||
|
val callId: String,
|
||||||
|
val nativeRoomId: String,
|
||||||
|
val opponentUserId: String,
|
||||||
|
val opponentMatrixItem: MatrixItem?,
|
||||||
|
val isVideoCall: Boolean,
|
||||||
|
val isOutgoing: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_NOTIFICATION_ID = 6480
|
private const val DEFAULT_NOTIFICATION_ID = 6480
|
||||||
|
private const val MISSED_CALL_TAG = "MISSED_CALL_TAG"
|
||||||
|
|
||||||
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
||||||
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
||||||
|
@ -294,6 +326,8 @@ class CallService : VectorService() {
|
||||||
|
|
||||||
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
||||||
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
|
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
|
||||||
|
private const val EXTRA_END_CALL_REJECTED = "EXTRA_END_CALL_REJECTED"
|
||||||
|
private const val EXTRA_END_CALL_REASON = "EXTRA_END_CALL_REASON"
|
||||||
|
|
||||||
fun onIncomingCallRinging(context: Context,
|
fun onIncomingCallRinging(context: Context,
|
||||||
callId: String,
|
callId: String,
|
||||||
|
@ -329,11 +363,13 @@ class CallService : VectorService() {
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCallTerminated(context: Context, callId: String) {
|
fun onCallTerminated(context: Context, callId: String, endCallReason: EndCallReason, rejected: Boolean) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_CALL_TERMINATED
|
action = ACTION_CALL_TERMINATED
|
||||||
putExtra(EXTRA_CALL_ID, callId)
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
putExtra(EXTRA_END_CALL_REASON, endCallReason)
|
||||||
|
putExtra(EXTRA_END_CALL_REJECTED, rejected)
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ class CallControlsView @JvmOverloads constructor(
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallState.Terminated,
|
is CallState.Ended,
|
||||||
null -> {
|
null -> {
|
||||||
views.ringingControls.isVisible = false
|
views.ringingControls.isVisible = false
|
||||||
views.connectedControls.isVisible = false
|
views.connectedControls.isVisible = false
|
||||||
|
|
|
@ -54,6 +54,7 @@ import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
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.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||||
|
@ -71,6 +72,8 @@ data class CallArgs(
|
||||||
val isVideoCall: Boolean
|
val isVideoCall: Boolean
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("VectorCallActivity", LoggerTag.VOIP)
|
||||||
|
|
||||||
class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallControlsView.InteractionListener {
|
class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallControlsView.InteractionListener {
|
||||||
|
|
||||||
override fun getBinding() = ActivityCallBinding.inflate(layoutInflater)
|
override fun getBinding() = ActivityCallBinding.inflate(layoutInflater)
|
||||||
|
@ -113,11 +116,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||||
} else {
|
} else {
|
||||||
Timber.e("## VOIP missing callArgs for VectorCall Activity")
|
Timber.tag(loggerTag.value).e("missing callArgs for VectorCall Activity")
|
||||||
finish()
|
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) {
|
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
|
||||||
turnScreenOnAndKeyguardOff()
|
turnScreenOnAndKeyguardOff()
|
||||||
}
|
}
|
||||||
|
@ -160,7 +163,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(state: VectorCallViewState) {
|
private fun renderState(state: VectorCallViewState) {
|
||||||
Timber.v("## VOIP renderState call $state")
|
Timber.tag(loggerTag.value).v("renderState call $state")
|
||||||
if (state.callState is Fail) {
|
if (state.callState is Fail) {
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
|
@ -246,7 +249,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
views.callConnectingProgress.isVisible = true
|
views.callConnectingProgress.isVisible = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is CallState.Terminated -> {
|
is CallState.Ended -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
|
@ -309,7 +312,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
|
|
||||||
private fun start() {
|
private fun start() {
|
||||||
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
||||||
Timber.v("## VOIP rootEglBase is null")
|
Timber.tag(loggerTag.value).v("rootEglBase is null")
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,7 +338,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||||
Timber.v("## VOIP handleViewEvents $event")
|
Timber.tag(loggerTag.value).v("handleViewEvents $event")
|
||||||
when (event) {
|
when (event) {
|
||||||
VectorCallViewEvents.DismissNoCall -> {
|
VectorCallViewEvents.DismissNoCall -> {
|
||||||
finish()
|
finish()
|
||||||
|
@ -357,7 +360,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onErrorTimoutConnect(turn: TurnServerResponse?) {
|
private fun onErrorTimoutConnect(turn: TurnServerResponse?) {
|
||||||
Timber.d("## VOIP onErrorTimoutConnect $turn")
|
Timber.tag(loggerTag.value).d("onErrorTimoutConnect $turn")
|
||||||
// TODO ask to use default stun, etc...
|
// TODO ask to use default stun, etc...
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.call_failed_no_connection)
|
.setTitle(R.string.call_failed_no_connection)
|
||||||
|
@ -437,7 +440,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
|
|
||||||
// Needed to let you answer call when phone is locked
|
// Needed to let you answer call when phone is locked
|
||||||
private fun turnScreenOnAndKeyguardOff() {
|
private fun turnScreenOnAndKeyguardOff() {
|
||||||
Timber.v("## VOIP turnScreenOnAndKeyguardOff")
|
Timber.tag(loggerTag.value).v("turnScreenOnAndKeyguardOff")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
setShowWhenLocked(true)
|
setShowWhenLocked(true)
|
||||||
setTurnScreenOn(true)
|
setTurnScreenOn(true)
|
||||||
|
@ -458,7 +461,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun turnScreenOffAndKeyguardOn() {
|
private fun turnScreenOffAndKeyguardOn() {
|
||||||
Timber.v("## VOIP turnScreenOnAndKeyguardOn")
|
Timber.tag(loggerTag.value).v("turnScreenOnAndKeyguardOn")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
setShowWhenLocked(false)
|
setShowWhenLocked(false)
|
||||||
setTurnScreenOn(false)
|
setTurnScreenOn(false)
|
||||||
|
|
|
@ -25,9 +25,12 @@ import android.media.AudioManager
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
import im.vector.app.core.services.BluetoothHeadsetReceiver
|
||||||
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
import im.vector.app.core.services.WiredHeadsetStateReceiver
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.HashSet
|
import java.util.HashSet
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("API21AudioDeviceDetector", LoggerTag.VOIP)
|
||||||
|
|
||||||
internal class API21AudioDeviceDetector(private val context: Context,
|
internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
private val audioManager: AudioManager,
|
private val audioManager: AudioManager,
|
||||||
private val callAudioManager: CallAudioManager
|
private val callAudioManager: CallAudioManager
|
||||||
|
@ -62,17 +65,17 @@ internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isBluetoothHeadsetOn(): Boolean {
|
private fun isBluetoothHeadsetOn(): Boolean {
|
||||||
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
Timber.tag(loggerTag.value).v("AudioManager isBluetoothHeadsetOn")
|
||||||
try {
|
try {
|
||||||
if (connectedBlueToothHeadset == null) return false.also {
|
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 {
|
if (!audioManager.isBluetoothScoAvailableOffCall) return false.also {
|
||||||
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
Timber.tag(loggerTag.value).v("AudioManager isBluetoothScoAvailableOffCall false")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
Timber.e("AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,11 +94,11 @@ internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
|
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(context, this)
|
||||||
val bm: BluetoothManager? = context.getSystemService()
|
val bm: BluetoothManager? = context.getSystemService()
|
||||||
val adapter = bm?.adapter
|
val adapter = bm?.adapter
|
||||||
Timber.d("## VOIP Bluetooth adapter $adapter")
|
Timber.tag(loggerTag.value).d("Bluetooth adapter $adapter")
|
||||||
bluetoothAdapter = adapter
|
bluetoothAdapter = adapter
|
||||||
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
adapter?.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
|
||||||
override fun onServiceDisconnected(profile: Int) {
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
Timber.d("## VOIP onServiceDisconnected $profile")
|
Timber.tag(loggerTag.value).d("onServiceDisconnected $profile")
|
||||||
if (profile == BluetoothProfile.HEADSET) {
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
connectedBlueToothHeadset = null
|
connectedBlueToothHeadset = null
|
||||||
onAudioDeviceChange()
|
onAudioDeviceChange()
|
||||||
|
@ -103,7 +106,7 @@ internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
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) {
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
connectedBlueToothHeadset = proxy
|
connectedBlueToothHeadset = proxy
|
||||||
onAudioDeviceChange()
|
onAudioDeviceChange()
|
||||||
|
@ -122,12 +125,12 @@ internal class API21AudioDeviceDetector(private val context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||||
Timber.v("onHeadsetEvent $event")
|
Timber.tag(loggerTag.value).v("onHeadsetEvent $event")
|
||||||
onAudioDeviceChange()
|
onAudioDeviceChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||||
Timber.v("onBTHeadsetEvent $event")
|
Timber.tag(loggerTag.value).v("onBTHeadsetEvent $event")
|
||||||
onAudioDeviceChange()
|
onAudioDeviceChange()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||||
private val call = callManager.getCallById(initialState.callId)
|
private val call = callManager.getCallById(initialState.callId)
|
||||||
private val callListener = object : WebRtcCall.Listener {
|
private val callListener = object : WebRtcCall.Listener {
|
||||||
override fun onStateUpdate(call: MxCall) {
|
override fun onStateUpdate(call: MxCall) {
|
||||||
if (call.state == CallState.Terminated) {
|
if (call.state is CallState.Ended) {
|
||||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.call.webrtc
|
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.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
import org.webrtc.DataChannel
|
import org.webrtc.DataChannel
|
||||||
|
@ -25,10 +26,12 @@ import org.webrtc.PeerConnection
|
||||||
import org.webrtc.RtpReceiver
|
import org.webrtc.RtpReceiver
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("PeerConnectionObserver", LoggerTag.VOIP)
|
||||||
|
|
||||||
class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer {
|
class PeerConnectionObserver(private val webRtcCall: WebRtcCall) : PeerConnection.Observer {
|
||||||
|
|
||||||
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
|
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
|
||||||
Timber.v("## VOIP StreamObserver onConnectionChange: $newState")
|
Timber.tag(loggerTag.value).v("StreamObserver onConnectionChange: $newState")
|
||||||
when (newState) {
|
when (newState) {
|
||||||
/**
|
/**
|
||||||
* Every ICE transport used by the connection is either in use (state "connected" or "completed")
|
* 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) {
|
override fun onIceCandidate(iceCandidate: IceCandidate) {
|
||||||
Timber.v("## VOIP StreamObserver onIceCandidate: $iceCandidate")
|
Timber.tag(loggerTag.value).v("StreamObserver onIceCandidate: $iceCandidate")
|
||||||
webRtcCall.onIceCandidate(iceCandidate)
|
webRtcCall.onIceCandidate(iceCandidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDataChannel(dc: DataChannel) {
|
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) {
|
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) {
|
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) {
|
||||||
Timber.v("## VOIP StreamObserver onIceConnectionChange IceConnectionState:$newState")
|
Timber.tag(loggerTag.value).v("StreamObserver onIceConnectionChange IceConnectionState:$newState")
|
||||||
when (newState) {
|
when (newState) {
|
||||||
/**
|
/**
|
||||||
* the ICE agent is gathering addresses or is waiting to be given remote candidates through
|
* 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) {
|
override fun onAddStream(stream: MediaStream) {
|
||||||
Timber.v("## VOIP StreamObserver onAddStream: $stream")
|
Timber.tag(loggerTag.value).v("StreamObserver onAddStream: $stream")
|
||||||
webRtcCall.onAddStream(stream)
|
webRtcCall.onAddStream(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRemoveStream(stream: MediaStream) {
|
override fun onRemoveStream(stream: MediaStream) {
|
||||||
Timber.v("## VOIP StreamObserver onRemoveStream")
|
Timber.tag(loggerTag.value).v("StreamObserver onRemoveStream")
|
||||||
webRtcCall.onRemoveStream()
|
webRtcCall.onRemoveStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState) {
|
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) {
|
override fun onSignalingChange(newState: PeerConnection.SignalingState) {
|
||||||
Timber.v("## VOIP StreamObserver onSignalingChange: $newState")
|
Timber.tag(loggerTag.value).v("StreamObserver onSignalingChange: $newState")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
|
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>) {
|
||||||
Timber.v("## VOIP StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}")
|
Timber.tag(loggerTag.value).v("StreamObserver onIceCandidatesRemoved: ${candidates.contentToString()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRenegotiationNeeded() {
|
override fun onRenegotiationNeeded() {
|
||||||
Timber.v("## VOIP StreamObserver onRenegotiationNeeded")
|
Timber.tag(loggerTag.value).v("StreamObserver onRenegotiationNeeded")
|
||||||
webRtcCall.onRenegotiationNeeded(restartIce = false)
|
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.
|
* gets a new set of tracks because the media element being captured loaded a new source.
|
||||||
*/
|
*/
|
||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||||
Timber.v("## VOIP StreamObserver onAddTrack")
|
Timber.tag(loggerTag.value).v("StreamObserver onAddTrack")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.app.features.call.webrtc
|
|
||||||
|
|
||||||
import org.webrtc.DataChannel
|
|
||||||
import org.webrtc.IceCandidate
|
|
||||||
import org.webrtc.MediaStream
|
|
||||||
import org.webrtc.PeerConnection
|
|
||||||
import org.webrtc.RtpReceiver
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
abstract class PeerConnectionObserverAdapter : PeerConnection.Observer {
|
|
||||||
override fun onIceCandidate(p0: IceCandidate?) {
|
|
||||||
Timber.v("## VOIP onIceCandidate $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDataChannel(p0: DataChannel?) {
|
|
||||||
Timber.v("## VOIP onDataChannel $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceConnectionReceivingChange(p0: Boolean) {
|
|
||||||
Timber.v("## VOIP onIceConnectionReceivingChange $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
|
||||||
Timber.v("## VOIP onIceConnectionChange $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
|
||||||
Timber.v("## VOIP onIceConnectionChange $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAddStream(mediaStream: MediaStream?) {
|
|
||||||
Timber.v("## VOIP onAddStream $mediaStream")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
|
||||||
Timber.v("## VOIP onSignalingChange $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
|
||||||
Timber.v("## VOIP onIceCandidatesRemoved $p0")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoveStream(mediaStream: MediaStream?) {
|
|
||||||
Timber.v("## VOIP onRemoveStream $mediaStream")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRenegotiationNeeded() {
|
|
||||||
Timber.v("## VOIP onRenegotiationNeeded")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
|
||||||
Timber.v("## VOIP onAddTrack $p0 / out: $p1")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -45,6 +45,7 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.CallIdGenerator
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
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.CallHangupContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||||
import org.threeten.bp.Duration
|
import org.threeten.bp.Duration
|
||||||
import org.webrtc.AudioSource
|
import org.webrtc.AudioSource
|
||||||
|
@ -88,6 +92,8 @@ private const val AUDIO_TRACK_ID = "${STREAM_ID}a0"
|
||||||
private const val VIDEO_TRACK_ID = "${STREAM_ID}v0"
|
private const val VIDEO_TRACK_ID = "${STREAM_ID}v0"
|
||||||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
|
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
|
||||||
|
|
||||||
|
private val loggerTag = LoggerTag("WebRtcCall", LoggerTag.VOIP)
|
||||||
|
|
||||||
class WebRtcCall(
|
class WebRtcCall(
|
||||||
val mxCall: MxCall,
|
val mxCall: MxCall,
|
||||||
// This is where the call is placed from an ui perspective.
|
// This is where the call is placed from an ui perspective.
|
||||||
|
@ -99,7 +105,7 @@ class WebRtcCall(
|
||||||
private val sessionProvider: Provider<Session?>,
|
private val sessionProvider: Provider<Session?>,
|
||||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||||
private val onCallEnded: (String) -> Unit
|
private val onCallEnded: (String, EndCallReason, Boolean) -> Unit
|
||||||
) : MxCall.StateListener {
|
) : MxCall.StateListener {
|
||||||
|
|
||||||
interface Listener : MxCall.StateListener {
|
interface Listener : MxCall.StateListener {
|
||||||
|
@ -192,7 +198,7 @@ class WebRtcCall(
|
||||||
.subscribe {
|
.subscribe {
|
||||||
// omit empty :/
|
// omit empty :/
|
||||||
if (it.isNotEmpty()) {
|
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) }
|
// it.forEach { peerConnection?.addIceCandidate(it) }
|
||||||
mxCall.sendLocalCallCandidates(it.mapToCallCandidate())
|
mxCall.sendLocalCallCandidates(it.mapToCallCandidate())
|
||||||
}
|
}
|
||||||
|
@ -210,7 +216,7 @@ class WebRtcCall(
|
||||||
fun onRenegotiationNeeded(restartIce: Boolean) {
|
fun onRenegotiationNeeded(restartIce: Boolean) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
if (mxCall.state != CallState.CreateOffer && mxCall.opponentVersion == 0) {
|
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
|
return@launch
|
||||||
}
|
}
|
||||||
val constraints = MediaConstraints()
|
val constraints = MediaConstraints()
|
||||||
|
@ -218,7 +224,7 @@ class WebRtcCall(
|
||||||
constraints.mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
constraints.mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true"))
|
||||||
}
|
}
|
||||||
val peerConnection = peerConnection ?: return@launch
|
val peerConnection = peerConnection ?: return@launch
|
||||||
Timber.v("## VOIP creating offer...")
|
Timber.tag(loggerTag.value).v("creating offer...")
|
||||||
makingOffer = true
|
makingOffer = true
|
||||||
try {
|
try {
|
||||||
val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch
|
val sessionDescription = peerConnection.awaitCreateOffer(constraints) ?: return@launch
|
||||||
|
@ -227,7 +233,7 @@ class WebRtcCall(
|
||||||
// Allow a short time for initial candidates to be gathered
|
// Allow a short time for initial candidates to be gathered
|
||||||
delay(200)
|
delay(200)
|
||||||
}
|
}
|
||||||
if (mxCall.state == CallState.Terminated) {
|
if (mxCall.state is CallState.Ended) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (mxCall.state == CallState.CreateOffer) {
|
if (mxCall.state == CallState.CreateOffer) {
|
||||||
|
@ -238,7 +244,7 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// Need to handle error properly.
|
// Need to handle error properly.
|
||||||
Timber.v("Failure while creating offer")
|
Timber.tag(loggerTag.value).v("Failure while creating offer")
|
||||||
} finally {
|
} finally {
|
||||||
makingOffer = false
|
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 {
|
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||||
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
|
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
|
||||||
}
|
}
|
||||||
|
@ -285,7 +291,7 @@ class WebRtcCall(
|
||||||
createCallId = CallIdGenerator.generate(),
|
createCallId = CallIdGenerator.generate(),
|
||||||
awaitCallId = null
|
awaitCallId = null
|
||||||
)
|
)
|
||||||
endCall(sendEndSignaling = false)
|
terminate(EndCallReason.REPLACED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,14 +313,14 @@ class WebRtcCall(
|
||||||
createCallId = newCallId,
|
createCallId = newCallId,
|
||||||
awaitCallId = null
|
awaitCallId = null
|
||||||
)
|
)
|
||||||
endCall(sendEndSignaling = false)
|
terminate(EndCallReason.REPLACED)
|
||||||
transferTargetCall.endCall(sendEndSignaling = false)
|
transferTargetCall.terminate(EndCallReason.REPLACED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun acceptIncomingCall() {
|
fun acceptIncomingCall() {
|
||||||
sessionScope?.launch {
|
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) {
|
if (mxCall.state == CallState.LocalRinging) {
|
||||||
internalAcceptIncomingCall()
|
internalAcceptIncomingCall()
|
||||||
}
|
}
|
||||||
|
@ -333,7 +339,7 @@ class WebRtcCall(
|
||||||
sender.dtmf()?.insertDtmf(digit, 100, 70)
|
sender.dtmf()?.insertDtmf(digit, 100, 70)
|
||||||
return@launch
|
return@launch
|
||||||
} catch (failure: Throwable) {
|
} 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?) {
|
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
||||||
localSurfaceRenderers.addIfNeeded(localViewRenderer)
|
localSurfaceRenderers.addIfNeeded(localViewRenderer)
|
||||||
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
|
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
|
||||||
when (mode) {
|
when (mode) {
|
||||||
|
@ -389,7 +395,7 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun detachRenderersInternal(renderers: List<SurfaceViewRenderer>?) = withContext(dispatcher) {
|
private suspend fun detachRenderersInternal(renderers: List<SurfaceViewRenderer>?) = withContext(dispatcher) {
|
||||||
Timber.v("## VOIP detachRenderers")
|
Timber.tag(loggerTag.value).v("detachRenderers")
|
||||||
if (renderers.isNullOrEmpty()) {
|
if (renderers.isNullOrEmpty()) {
|
||||||
// remove all sinks
|
// remove all sinks
|
||||||
localSurfaceRenderers.forEach {
|
localSurfaceRenderers.forEach {
|
||||||
|
@ -422,12 +428,12 @@ class WebRtcCall(
|
||||||
// 2. Access camera (if video call) + microphone, create local stream
|
// 2. Access camera (if video call) + microphone, create local stream
|
||||||
createLocalStream()
|
createLocalStream()
|
||||||
attachViewRenderersInternal()
|
attachViewRenderersInternal()
|
||||||
Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
|
Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource")
|
||||||
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
||||||
Timber.v("## VOIP adding remote ice candidate $it")
|
Timber.tag(loggerTag.value).v("adding remote ice candidate $it")
|
||||||
peerConnection?.addIceCandidate(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
|
// Now we wait for negotiation callback
|
||||||
}
|
}
|
||||||
|
@ -453,15 +459,15 @@ class WebRtcCall(
|
||||||
SessionDescription(SessionDescription.Type.OFFER, it)
|
SessionDescription(SessionDescription.Type.OFFER, it)
|
||||||
}
|
}
|
||||||
if (offerSdp == null) {
|
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
|
return@withContext
|
||||||
}
|
}
|
||||||
Timber.v("Offer sdp for invite: ${offerSdp.description}")
|
Timber.tag(loggerTag.value).v("Offer sdp for invite: ${offerSdp.description}")
|
||||||
try {
|
try {
|
||||||
peerConnection?.awaitSetRemoteDescription(offerSdp)
|
peerConnection?.awaitSetRemoteDescription(offerSdp)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.v("Failure putting remote description")
|
Timber.tag(loggerTag.value).v("Failure putting remote description")
|
||||||
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
|
endCall(reason = EndCallReason.UNKWOWN_ERROR)
|
||||||
return@withContext
|
return@withContext
|
||||||
}
|
}
|
||||||
// 2) Access camera + microphone, create local stream
|
// 2) Access camera + microphone, create local stream
|
||||||
|
@ -472,12 +478,12 @@ class WebRtcCall(
|
||||||
createAnswer()?.also {
|
createAnswer()?.also {
|
||||||
mxCall.accept(it.description)
|
mxCall.accept(it.description)
|
||||||
}
|
}
|
||||||
Timber.v("## VOIP remoteCandidateSource $remoteCandidateSource")
|
Timber.tag(loggerTag.value).v("remoteCandidateSource $remoteCandidateSource")
|
||||||
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
remoteIceCandidateDisposable = remoteCandidateSource.subscribe({
|
||||||
Timber.v("## VOIP adding remote ice candidate $it")
|
Timber.tag(loggerTag.value).v("adding remote ice candidate $it")
|
||||||
peerConnection?.addIceCandidate(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() {
|
private fun createLocalStream() {
|
||||||
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
|
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)
|
configureAudioTrack(peerConnectionFactory)
|
||||||
// add video track if needed
|
// add video track if needed
|
||||||
if (mxCall.isVideoCall) {
|
if (mxCall.isVideoCall) {
|
||||||
|
@ -502,7 +508,7 @@ class WebRtcCall(
|
||||||
val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
val audioSource = peerConnectionFactory.createAudioSource(DEFAULT_AUDIO_CONSTRAINTS)
|
||||||
val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
val audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource)
|
||||||
audioTrack.setEnabled(true)
|
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))
|
peerConnection?.addTrack(audioTrack, listOf(STREAM_ID))
|
||||||
localAudioSource = audioSource
|
localAudioSource = audioSource
|
||||||
localAudioTrack = audioTrack
|
localAudioTrack = audioTrack
|
||||||
|
@ -544,7 +550,7 @@ class WebRtcCall(
|
||||||
|
|
||||||
override fun onCameraClosed() {
|
override fun onCameraClosed() {
|
||||||
super.onCameraClosed()
|
super.onCameraClosed()
|
||||||
Timber.v("onCameraClosed")
|
Timber.tag(loggerTag.value).v("onCameraClosed")
|
||||||
// This could happen if you open the camera app in chat
|
// 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
|
// We then register in order to restart capture as soon as the camera is available again
|
||||||
videoCapturerIsInError = true
|
videoCapturerIsInError = true
|
||||||
|
@ -552,16 +558,16 @@ class WebRtcCall(
|
||||||
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
|
cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() {
|
||||||
override fun onCameraUnavailable(cameraId: String) {
|
override fun onCameraUnavailable(cameraId: String) {
|
||||||
super.onCameraUnavailable(cameraId)
|
super.onCameraUnavailable(cameraId)
|
||||||
Timber.v("On camera unavailable: $cameraId")
|
Timber.tag(loggerTag.value).v("On camera unavailable: $cameraId")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraAccessPrioritiesChanged() {
|
override fun onCameraAccessPrioritiesChanged() {
|
||||||
super.onCameraAccessPrioritiesChanged()
|
super.onCameraAccessPrioritiesChanged()
|
||||||
Timber.v("onCameraAccessPrioritiesChanged")
|
Timber.tag(loggerTag.value).v("onCameraAccessPrioritiesChanged")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraAvailable(cameraId: String) {
|
override fun onCameraAvailable(cameraId: String) {
|
||||||
Timber.v("On camera available: $cameraId")
|
Timber.tag(loggerTag.value).v("On camera available: $cameraId")
|
||||||
if (cameraId == camera.name) {
|
if (cameraId == camera.name) {
|
||||||
videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
|
videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps)
|
||||||
cameraManager?.unregisterAvailabilityCallback(this)
|
cameraManager?.unregisterAvailabilityCallback(this)
|
||||||
|
@ -574,7 +580,7 @@ class WebRtcCall(
|
||||||
|
|
||||||
val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast)
|
val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast)
|
||||||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
|
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)
|
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||||||
// HD
|
// HD
|
||||||
|
@ -582,7 +588,7 @@ class WebRtcCall(
|
||||||
this.videoCapturer = videoCapturer
|
this.videoCapturer = videoCapturer
|
||||||
|
|
||||||
val videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource)
|
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)
|
videoTrack.setEnabled(true)
|
||||||
peerConnection?.addTrack(videoTrack, listOf(STREAM_ID))
|
peerConnection?.addTrack(videoTrack, listOf(STREAM_ID))
|
||||||
localVideoSource = videoSource
|
localVideoSource = videoSource
|
||||||
|
@ -592,7 +598,7 @@ class WebRtcCall(
|
||||||
|
|
||||||
fun setCaptureFormat(format: CaptureFormat) {
|
fun setCaptureFormat(format: CaptureFormat) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
Timber.v("## VOIP setCaptureFormat $format")
|
Timber.tag(loggerTag.value).v("setCaptureFormat $format")
|
||||||
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
|
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
|
||||||
currentCaptureFormat = format
|
currentCaptureFormat = format
|
||||||
}
|
}
|
||||||
|
@ -686,14 +692,14 @@ class WebRtcCall(
|
||||||
|
|
||||||
fun switchCamera() {
|
fun switchCamera() {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
Timber.v("## VOIP switchCamera")
|
Timber.tag(loggerTag.value).v("switchCamera")
|
||||||
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
|
if (mxCall.state is CallState.Connected && mxCall.isVideoCall) {
|
||||||
val oppositeCamera = getOppositeCameraIfAny() ?: return@launch
|
val oppositeCamera = getOppositeCameraIfAny() ?: return@launch
|
||||||
videoCapturer?.switchCamera(
|
videoCapturer?.switchCamera(
|
||||||
object : CameraVideoCapturer.CameraSwitchHandler {
|
object : CameraVideoCapturer.CameraSwitchHandler {
|
||||||
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
|
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
|
||||||
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
|
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
|
||||||
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
|
Timber.tag(loggerTag.value).v("onCameraSwitchDone isFront $isFrontCamera")
|
||||||
cameraInUse = oppositeCamera
|
cameraInUse = oppositeCamera
|
||||||
localSurfaceRenderers.forEach {
|
localSurfaceRenderers.forEach {
|
||||||
it.get()?.setMirror(isFrontCamera)
|
it.get()?.setMirror(isFrontCamera)
|
||||||
|
@ -704,7 +710,7 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCameraSwitchError(errorDescription: String?) {
|
override fun onCameraSwitchError(errorDescription: String?) {
|
||||||
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
|
Timber.tag(loggerTag.value).v("onCameraSwitchError isFront $errorDescription")
|
||||||
}
|
}
|
||||||
}, oppositeCamera.name
|
}, oppositeCamera.name
|
||||||
)
|
)
|
||||||
|
@ -713,7 +719,7 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createAnswer(): SessionDescription? {
|
private suspend fun createAnswer(): SessionDescription? {
|
||||||
Timber.w("## VOIP createAnswer")
|
Timber.tag(loggerTag.value).w("createAnswer")
|
||||||
val peerConnection = peerConnection ?: return null
|
val peerConnection = peerConnection ?: return null
|
||||||
val constraints = MediaConstraints().apply {
|
val constraints = MediaConstraints().apply {
|
||||||
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
|
||||||
|
@ -724,7 +730,7 @@ class WebRtcCall(
|
||||||
peerConnection.awaitSetLocalDescription(localDescription)
|
peerConnection.awaitSetLocalDescription(localDescription)
|
||||||
localDescription
|
localDescription
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.v("Fail to create answer")
|
Timber.tag(loggerTag.value).v("Fail to create answer")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -765,9 +771,9 @@ class WebRtcCall(
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
// reportError("Weird-looking stream: " + stream);
|
// reportError("Weird-looking stream: " + stream);
|
||||||
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
|
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??
|
// TODO maybe do something more??
|
||||||
endCall(true)
|
endCall(EndCallReason.UNKWOWN_ERROR)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (stream.audioTracks.size == 1) {
|
if (stream.audioTracks.size == 1) {
|
||||||
|
@ -795,11 +801,22 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
|
fun endCall(reason: EndCallReason = EndCallReason.USER_HANGUP) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
if (mxCall.state == CallState.Terminated) {
|
if (mxCall.state is CallState.Ended) {
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
val reject = mxCall.state is CallState.LocalRinging
|
||||||
|
terminate(EndCallReason.USER_HANGUP, reject)
|
||||||
|
if (reject) {
|
||||||
|
mxCall.reject()
|
||||||
|
} else {
|
||||||
|
mxCall.hangUp(reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun terminate(reason: EndCallReason? = null, rejected: Boolean = false) = withContext(dispatcher) {
|
||||||
// Close tracks ASAP
|
// Close tracks ASAP
|
||||||
localVideoTrack?.setEnabled(false)
|
localVideoTrack?.setEnabled(false)
|
||||||
localVideoTrack?.setEnabled(false)
|
localVideoTrack?.setEnabled(false)
|
||||||
|
@ -807,18 +824,9 @@ class WebRtcCall(
|
||||||
val cameraManager = context.getSystemService<CameraManager>()!!
|
val cameraManager = context.getSystemService<CameraManager>()!!
|
||||||
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
||||||
}
|
}
|
||||||
val wasRinging = mxCall.state is CallState.LocalRinging
|
mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP)
|
||||||
mxCall.state = CallState.Terminated
|
|
||||||
release()
|
release()
|
||||||
onCallEnded(callId)
|
onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected)
|
||||||
if (sendEndSignaling) {
|
|
||||||
if (wasRinging) {
|
|
||||||
mxCall.reject()
|
|
||||||
} else {
|
|
||||||
mxCall.hangUp(reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call listener
|
// Call listener
|
||||||
|
@ -829,7 +837,7 @@ class WebRtcCall(
|
||||||
if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) {
|
if (it.sdpMid.isNullOrEmpty() || it.candidate.isNullOrEmpty()) {
|
||||||
return@forEach
|
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)
|
val iceCandidate = IceCandidate(it.sdpMid, it.sdpMLineIndex, it.candidate)
|
||||||
remoteCandidateSource.onNext(iceCandidate)
|
remoteCandidateSource.onNext(iceCandidate)
|
||||||
}
|
}
|
||||||
|
@ -838,12 +846,12 @@ class WebRtcCall(
|
||||||
|
|
||||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||||
sessionScope?.launch(dispatcher) {
|
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)
|
val sdp = SessionDescription(SessionDescription.Type.ANSWER, callAnswerContent.answer.sdp)
|
||||||
try {
|
try {
|
||||||
peerConnection?.awaitSetRemoteDescription(sdp)
|
peerConnection?.awaitSetRemoteDescription(sdp)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
|
endCall(EndCallReason.UNKWOWN_ERROR)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
|
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
|
||||||
|
@ -858,7 +866,7 @@ class WebRtcCall(
|
||||||
val type = description?.type
|
val type = description?.type
|
||||||
val sdpText = description?.sdp
|
val sdpText = description?.sdp
|
||||||
if (type == null || sdpText == null) {
|
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
|
return@launch
|
||||||
}
|
}
|
||||||
val peerConnection = peerConnection ?: return@launch
|
val peerConnection = peerConnection ?: return@launch
|
||||||
|
@ -873,7 +881,7 @@ class WebRtcCall(
|
||||||
|
|
||||||
ignoreOffer = !polite && offerCollision
|
ignoreOffer = !polite && offerCollision
|
||||||
if (ignoreOffer) {
|
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
|
return@launch
|
||||||
}
|
}
|
||||||
val prevOnHold = computeIsLocalOnHold()
|
val prevOnHold = computeIsLocalOnHold()
|
||||||
|
@ -886,7 +894,7 @@ class WebRtcCall(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Failed to complete negotiation")
|
Timber.tag(loggerTag.value).e(failure, "Failed to complete negotiation")
|
||||||
}
|
}
|
||||||
val nowOnHold = computeIsLocalOnHold()
|
val nowOnHold = computeIsLocalOnHold()
|
||||||
wasLocalOnHold = nowOnHold
|
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) {
|
fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
val session = sessionProvider.get() ?: return@launch
|
val session = sessionProvider.get() ?: return@launch
|
||||||
val newAssertedIdentity = callAssertedIdentityContent.assertedIdentity ?: return@launch
|
val newAssertedIdentity = callAssertedIdentityContent.assertedIdentity ?: return@launch
|
||||||
if (newAssertedIdentity.id == null && newAssertedIdentity.displayName == null) {
|
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
|
return@launch
|
||||||
}
|
}
|
||||||
remoteAssertedIdentity = newAssertedIdentity
|
remoteAssertedIdentity = newAssertedIdentity
|
||||||
|
|
|
@ -21,7 +21,12 @@ import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
|
||||||
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
|
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
|
||||||
return session.getRoomSummary(nativeRoomId)?.otherMemberIds?.firstOrNull()?.let {
|
return session.getRoomSummary(nativeRoomId)?.let { roomSummary ->
|
||||||
session.getUser(it)?.toMatrixItem()
|
// Fallback to RoomSummary if there is no other member.
|
||||||
|
if (roomSummary.otherMemberIds.isEmpty()) {
|
||||||
|
roomSummary.toMatrixItem()
|
||||||
|
} else {
|
||||||
|
roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,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.lookup.CallUserMapper
|
||||||
import im.vector.app.features.call.utils.EglUtils
|
import im.vector.app.features.call.utils.EglUtils
|
||||||
import im.vector.app.features.call.vectorCallService
|
import im.vector.app.features.call.vectorCallService
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
import im.vector.app.push.fcm.FcmHelper
|
import im.vector.app.push.fcm.FcmHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
|
import org.matrix.android.sdk.api.logger.LoggerTag
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.CallListener
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
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.CallNegotiateContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||||
|
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
|
||||||
import org.webrtc.DefaultVideoDecoderFactory
|
import org.webrtc.DefaultVideoDecoderFactory
|
||||||
import org.webrtc.DefaultVideoEncoderFactory
|
import org.webrtc.DefaultVideoEncoderFactory
|
||||||
import org.webrtc.PeerConnectionFactory
|
import org.webrtc.PeerConnectionFactory
|
||||||
|
@ -60,6 +64,8 @@ import javax.inject.Singleton
|
||||||
* Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes
|
* Manage peerConnectionFactory & Peer connections outside of activity lifecycle to resist configuration changes
|
||||||
* Use app context
|
* Use app context
|
||||||
*/
|
*/
|
||||||
|
private val loggerTag = LoggerTag("WebRtcCallManager", LoggerTag.VOIP)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class WebRtcCallManager @Inject constructor(
|
class WebRtcCallManager @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -75,6 +81,9 @@ class WebRtcCallManager @Inject constructor(
|
||||||
private val callUserMapper: CallUserMapper?
|
private val callUserMapper: CallUserMapper?
|
||||||
get() = currentSession?.vectorCallService?.userMapper
|
get() = currentSession?.vectorCallService?.userMapper
|
||||||
|
|
||||||
|
private val sessionScope: CoroutineScope?
|
||||||
|
get() = currentSession?.coroutineScope
|
||||||
|
|
||||||
interface CurrentCallListener {
|
interface CurrentCallListener {
|
||||||
fun onCurrentCallChange(call: WebRtcCall?) {}
|
fun onCurrentCallChange(call: WebRtcCall?) {}
|
||||||
fun onAudioDevicesChange() {}
|
fun onAudioDevicesChange() {}
|
||||||
|
@ -184,7 +193,7 @@ class WebRtcCallManager @Inject constructor(
|
||||||
fun getAdvertisedCalls() = advertisedCalls
|
fun getAdvertisedCalls() = advertisedCalls
|
||||||
|
|
||||||
fun headSetButtonTapped() {
|
fun headSetButtonTapped() {
|
||||||
Timber.v("## VOIP headSetButtonTapped")
|
Timber.tag(loggerTag.value).v("headSetButtonTapped")
|
||||||
val call = getCurrentCall() ?: return
|
val call = getCurrentCall() ?: return
|
||||||
if (call.mxCall.state is CallState.LocalRinging) {
|
if (call.mxCall.state is CallState.LocalRinging) {
|
||||||
call.acceptIncomingCall()
|
call.acceptIncomingCall()
|
||||||
|
@ -197,12 +206,12 @@ class WebRtcCallManager @Inject constructor(
|
||||||
|
|
||||||
private fun createPeerConnectionFactoryIfNeeded() {
|
private fun createPeerConnectionFactoryIfNeeded() {
|
||||||
if (peerConnectionFactory != null) return
|
if (peerConnectionFactory != null) return
|
||||||
Timber.v("## VOIP createPeerConnectionFactory")
|
Timber.tag(loggerTag.value).v("createPeerConnectionFactory")
|
||||||
val eglBaseContext = rootEglBase?.eglBaseContext ?: return Unit.also {
|
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
|
PeerConnectionFactory.initialize(PeerConnectionFactory
|
||||||
.InitializationOptions.builder(context.applicationContext)
|
.InitializationOptions.builder(context.applicationContext)
|
||||||
.createInitializationOptions()
|
.createInitializationOptions()
|
||||||
|
@ -216,7 +225,7 @@ class WebRtcCallManager @Inject constructor(
|
||||||
/* enableH264HighProfile */
|
/* enableH264HighProfile */
|
||||||
true)
|
true)
|
||||||
val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
|
val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(eglBaseContext)
|
||||||
Timber.v("## VOIP PeerConnectionFactory.createPeerConnectionFactory ...")
|
Timber.tag(loggerTag.value).v("PeerConnectionFactory.createPeerConnectionFactory ...")
|
||||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||||
.setOptions(options)
|
.setOptions(options)
|
||||||
.setVideoEncoderFactory(defaultVideoEncoderFactory)
|
.setVideoEncoderFactory(defaultVideoEncoderFactory)
|
||||||
|
@ -225,19 +234,19 @@ class WebRtcCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallActive(call: WebRtcCall) {
|
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 }
|
val currentCall = getCurrentCall().takeIf { it != call }
|
||||||
currentCall?.updateRemoteOnHold(onHold = true)
|
currentCall?.updateRemoteOnHold(onHold = true)
|
||||||
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
|
audioManager.setMode(if (call.mxCall.isVideoCall) CallAudioManager.Mode.VIDEO_CALL else CallAudioManager.Mode.AUDIO_CALL)
|
||||||
this.currentCall.setAndNotify(call)
|
this.currentCall.setAndNotify(call)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCallEnded(callId: String) {
|
private fun onCallEnded(callId: String, endCallReason: EndCallReason, rejected: Boolean) {
|
||||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: $callId")
|
Timber.tag(loggerTag.value).v("onCall ended: $callId")
|
||||||
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
|
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
|
||||||
Timber.v("On call ended for unknown call $callId")
|
Timber.tag(loggerTag.value).v("On call ended for unknown call $callId")
|
||||||
}
|
}
|
||||||
CallService.onCallTerminated(context, callId)
|
CallService.onCallTerminated(context, callId, endCallReason, rejected)
|
||||||
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
|
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
|
||||||
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
||||||
transferees.remove(callId)
|
transferees.remove(callId)
|
||||||
|
@ -247,7 +256,7 @@ class WebRtcCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
// There is no active calls
|
// There is no active calls
|
||||||
if (getCurrentCall() == null) {
|
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?.dispose()
|
||||||
peerConnectionFactory = null
|
peerConnectionFactory = null
|
||||||
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
|
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) {
|
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
|
||||||
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
|
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()) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if (getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) {
|
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?
|
// Just ignore, maybe we could answer from other session?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -294,10 +303,10 @@ class WebRtcCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
|
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]
|
val call = callsByCallId[iceCandidatesContent.callId]
|
||||||
?: return Unit.also {
|
?: 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)
|
call.onCallIceCandidateReceived(iceCandidatesContent)
|
||||||
}
|
}
|
||||||
|
@ -329,19 +338,19 @@ class WebRtcCallManager @Inject constructor(
|
||||||
return webRtcCall
|
return webRtcCall
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) {
|
fun endCallForRoom(roomId: String) {
|
||||||
callsByRoomId[roomId]?.firstOrNull()?.endCall(originatedByMe)
|
callsByRoomId[roomId]?.firstOrNull()?.endCall()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||||
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
|
Timber.tag(loggerTag.value).v("onCallInviteReceived callId ${mxCall.callId}")
|
||||||
val nativeRoomId = callUserMapper?.nativeRoomForVirtualRoom(mxCall.roomId) ?: mxCall.roomId
|
val nativeRoomId = callUserMapper?.nativeRoomForVirtualRoom(mxCall.roomId) ?: mxCall.roomId
|
||||||
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if ((getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) {
|
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?
|
// Just ignore, maybe we could answer from other session?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -370,7 +379,7 @@ class WebRtcCallManager @Inject constructor(
|
||||||
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
|
||||||
val call = callsByCallId[callAnswerContent.callId]
|
val call = callsByCallId[callAnswerContent.callId]
|
||||||
?: return Unit.also {
|
?: 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
|
val mxCall = call.mxCall
|
||||||
// Update service state
|
// Update service state
|
||||||
|
@ -384,43 +393,38 @@ class WebRtcCallManager @Inject constructor(
|
||||||
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
override fun onCallHangupReceived(callHangupContent: CallHangupContent) {
|
||||||
val call = callsByCallId[callHangupContent.callId]
|
val call = callsByCallId[callHangupContent.callId]
|
||||||
?: return Unit.also {
|
?: 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) {
|
override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
|
||||||
val call = callsByCallId[callRejectContent.callId]
|
val call = callsByCallId[callRejectContent.callId]
|
||||||
?: return Unit.also {
|
?: 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) {
|
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
|
||||||
val call = callsByCallId[callSelectAnswerContent.callId]
|
val call = callsByCallId[callSelectAnswerContent.callId]
|
||||||
?: return Unit.also {
|
?: 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) {
|
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
|
||||||
val call = callsByCallId[callNegotiateContent.callId]
|
val call = callsByCallId[callNegotiateContent.callId]
|
||||||
?: return Unit.also {
|
?: 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)
|
call.onCallNegotiateReceived(callNegotiateContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallManagedByOtherSession(callId: String) {
|
override fun onCallManagedByOtherSession(callId: String) {
|
||||||
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
|
Timber.tag(loggerTag.value).v("onCallManagedByOtherSession: $callId")
|
||||||
onCallEnded(callId)
|
onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
|
||||||
|
@ -429,7 +433,7 @@ class WebRtcCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
val call = callsByCallId[callAssertedIdentityContent.callId]
|
val call = callsByCallId[callAssertedIdentityContent.callId]
|
||||||
?: return Unit.also {
|
?: 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,6 @@ import im.vector.app.features.home.RoomListDisplayMode
|
||||||
|
|
||||||
interface RoomListSectionBuilder {
|
interface RoomListSectionBuilder {
|
||||||
fun buildSections(mode: RoomListDisplayMode) : List<RoomsSection>
|
fun buildSections(mode: RoomListDisplayMode) : List<RoomsSection>
|
||||||
|
|
||||||
|
fun dispose()
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,8 @@ import im.vector.app.core.resources.StringProvider
|
||||||
import im.vector.app.features.home.RoomListDisplayMode
|
import im.vector.app.features.home.RoomListDisplayMode
|
||||||
import im.vector.app.features.invite.AutoAcceptInvites
|
import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
import im.vector.app.features.invite.showInvites
|
import im.vector.app.features.invite.showInvites
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
import org.matrix.android.sdk.api.query.RoomCategoryFilter
|
||||||
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
|
import org.matrix.android.sdk.api.query.RoomTagQueryFilter
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.rx.asObservable
|
import org.matrix.android.sdk.rx.asObservable
|
||||||
|
|
||||||
class GroupRoomListSectionBuilder(
|
class RoomListSectionBuilderGroup(
|
||||||
val session: Session,
|
private val session: Session,
|
||||||
val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
val viewModelScope: CoroutineScope,
|
private val appStateHandler: AppStateHandler,
|
||||||
val appStateHandler: AppStateHandler,
|
|
||||||
private val autoAcceptInvites: AutoAcceptInvites,
|
private val autoAcceptInvites: AutoAcceptInvites,
|
||||||
val onDisposable: (Disposable) -> Unit,
|
private val onUpdatable: (UpdatableLivePageResult) -> Unit
|
||||||
val onUdpatable: (UpdatableLivePageResult) -> Unit
|
|
||||||
) : RoomListSectionBuilder {
|
) : RoomListSectionBuilder {
|
||||||
|
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
override fun buildSections(mode: RoomListDisplayMode): List<RoomsSection> {
|
override fun buildSections(mode: RoomListDisplayMode): List<RoomsSection> {
|
||||||
val activeGroupAwareQueries = mutableListOf<UpdatableLivePageResult>()
|
val activeGroupAwareQueries = mutableListOf<UpdatableLivePageResult>()
|
||||||
val sections = mutableListOf<RoomsSection>()
|
val sections = mutableListOf<RoomsSection>()
|
||||||
|
@ -52,7 +51,7 @@ class GroupRoomListSectionBuilder(
|
||||||
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
RoomListDisplayMode.PEOPLE -> {
|
RoomListDisplayMode.PEOPLE -> {
|
||||||
// 3 sections Invites / Fav / Dms
|
// 4 sections Invites / Fav / Dms / Low Priority
|
||||||
buildPeopleSections(sections, activeGroupAwareQueries, actualGroupId)
|
buildPeopleSections(sections, activeGroupAwareQueries, actualGroupId)
|
||||||
}
|
}
|
||||||
RoomListDisplayMode.ROOMS -> {
|
RoomListDisplayMode.ROOMS -> {
|
||||||
|
@ -69,7 +68,7 @@ class GroupRoomListSectionBuilder(
|
||||||
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
||||||
session.getFilteredPagedRoomSummariesLive(qpm)
|
session.getFilteredPagedRoomSummariesLive(qpm)
|
||||||
.let { updatableFilterLivePageResult ->
|
.let { updatableFilterLivePageResult ->
|
||||||
onUdpatable(updatableFilterLivePageResult)
|
onUpdatable(updatableFilterLivePageResult)
|
||||||
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +87,7 @@ class GroupRoomListSectionBuilder(
|
||||||
it.activeGroupId = actualGroupId
|
it.activeGroupId = actualGroupId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addSection(
|
addSection(
|
||||||
sections,
|
sections,
|
||||||
activeGroupAwareQueries,
|
activeGroupAwareQueries,
|
||||||
|
@ -111,8 +111,9 @@ class GroupRoomListSectionBuilder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
onDisposable.invoke(it)
|
disposables.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +219,19 @@ class GroupRoomListSectionBuilder(
|
||||||
) {
|
) {
|
||||||
it.memberships = listOf(Membership.JOIN)
|
it.memberships = listOf(Membership.JOIN)
|
||||||
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
|
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
|
it.activeGroupId = actualGroupId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -231,7 +244,6 @@ class GroupRoomListSectionBuilder(
|
||||||
withQueryParams(
|
withQueryParams(
|
||||||
{ query.invoke(it) },
|
{ query.invoke(it) },
|
||||||
{ roomQueryParams ->
|
{ roomQueryParams ->
|
||||||
|
|
||||||
val name = stringProvider.getString(nameRes)
|
val name = stringProvider.getString(nameRes)
|
||||||
session.getFilteredPagedRoomSummariesLive(roomQueryParams)
|
session.getFilteredPagedRoomSummariesLive(roomQueryParams)
|
||||||
.also {
|
.also {
|
||||||
|
@ -246,8 +258,9 @@ class GroupRoomListSectionBuilder(
|
||||||
?.notificationCount
|
?.notificationCount
|
||||||
?.postValue(session.getNotificationCountForRooms(roomQueryParams))
|
?.postValue(session.getNotificationCountForRooms(roomQueryParams))
|
||||||
}.also {
|
}.also {
|
||||||
onDisposable.invoke(it)
|
disposables.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.add(
|
sections.add(
|
||||||
RoomsSection(
|
RoomsSection(
|
||||||
sectionName = name,
|
sectionName = name,
|
||||||
|
@ -267,4 +280,8 @@ class GroupRoomListSectionBuilder(
|
||||||
.build()
|
.build()
|
||||||
.let { block(it) }
|
.let { block(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
disposables.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -30,7 +30,7 @@ import im.vector.app.features.invite.AutoAcceptInvites
|
||||||
import im.vector.app.features.invite.showInvites
|
import im.vector.app.features.invite.showInvites
|
||||||
import im.vector.app.space
|
import im.vector.app.space
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxkotlin.Observables
|
import io.reactivex.rxkotlin.Observables
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.CoroutineScope
|
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.api.session.room.summary.RoomAggregateNotificationCount
|
||||||
import org.matrix.android.sdk.rx.asObservable
|
import org.matrix.android.sdk.rx.asObservable
|
||||||
|
|
||||||
class SpaceRoomListSectionBuilder(
|
class RoomListSectionBuilderSpace(
|
||||||
val session: Session,
|
private val session: Session,
|
||||||
val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
val appStateHandler: AppStateHandler,
|
private val appStateHandler: AppStateHandler,
|
||||||
val viewModelScope: CoroutineScope,
|
private val viewModelScope: CoroutineScope,
|
||||||
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
|
|
||||||
private val autoAcceptInvites: AutoAcceptInvites,
|
private val autoAcceptInvites: AutoAcceptInvites,
|
||||||
val onDisposable: (Disposable) -> Unit,
|
private val onUpdatable: (UpdatableLivePageResult) -> Unit,
|
||||||
val onUdpatable: (UpdatableLivePageResult) -> Unit,
|
private val suggestedRoomJoiningState: LiveData<Map<String, Async<Unit>>>,
|
||||||
val onlyOrphansInHome: Boolean = false
|
private val onlyOrphansInHome: Boolean = false
|
||||||
) : RoomListSectionBuilder {
|
) : RoomListSectionBuilder {
|
||||||
|
|
||||||
val pagedListConfig = PagedList.Config.Builder()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
private val pagedListConfig = PagedList.Config.Builder()
|
||||||
.setPageSize(10)
|
.setPageSize(10)
|
||||||
.setInitialLoadSizeHint(20)
|
.setInitialLoadSizeHint(20)
|
||||||
.setEnablePlaceholders(true)
|
.setEnablePlaceholders(true)
|
||||||
|
@ -70,12 +71,15 @@ class SpaceRoomListSectionBuilder(
|
||||||
val activeSpaceAwareQueries = mutableListOf<RoomListViewModel.ActiveSpaceQueryUpdater>()
|
val activeSpaceAwareQueries = mutableListOf<RoomListViewModel.ActiveSpaceQueryUpdater>()
|
||||||
when (mode) {
|
when (mode) {
|
||||||
RoomListDisplayMode.PEOPLE -> {
|
RoomListDisplayMode.PEOPLE -> {
|
||||||
|
// 4 sections Invites / Fav / Dms / Low Priority
|
||||||
buildDmSections(sections, activeSpaceAwareQueries)
|
buildDmSections(sections, activeSpaceAwareQueries)
|
||||||
}
|
}
|
||||||
RoomListDisplayMode.ROOMS -> {
|
RoomListDisplayMode.ROOMS -> {
|
||||||
|
// 6 sections invites / Fav / Rooms / Low Priority / Server notice / Suggested rooms
|
||||||
buildRoomsSections(sections, activeSpaceAwareQueries)
|
buildRoomsSections(sections, activeSpaceAwareQueries)
|
||||||
}
|
}
|
||||||
RoomListDisplayMode.FILTERED -> {
|
RoomListDisplayMode.FILTERED -> {
|
||||||
|
// Used when searching for rooms
|
||||||
withQueryParams(
|
withQueryParams(
|
||||||
{
|
{
|
||||||
it.memberships = Membership.activeMemberships()
|
it.memberships = Membership.activeMemberships()
|
||||||
|
@ -84,7 +88,7 @@ class SpaceRoomListSectionBuilder(
|
||||||
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
val name = stringProvider.getString(R.string.bottom_action_rooms)
|
||||||
session.getFilteredPagedRoomSummariesLive(qpm)
|
session.getFilteredPagedRoomSummariesLive(qpm)
|
||||||
.let { updatableFilterLivePageResult ->
|
.let { updatableFilterLivePageResult ->
|
||||||
onUdpatable(updatableFilterLivePageResult)
|
onUpdatable(updatableFilterLivePageResult)
|
||||||
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
sections.add(RoomsSection(name, updatableFilterLivePageResult.livePagedList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,13 +138,14 @@ class SpaceRoomListSectionBuilder(
|
||||||
updater.updateForSpaceId(selectedSpace?.roomId)
|
updater.updateForSpaceId(selectedSpace?.roomId)
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
onDisposable.invoke(it)
|
disposables.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections
|
return sections
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildRoomsSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
|
private fun buildRoomsSections(sections: MutableList<RoomsSection>,
|
||||||
|
activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
|
||||||
if (autoAcceptInvites.showInvites()) {
|
if (autoAcceptInvites.showInvites()) {
|
||||||
addSection(
|
addSection(
|
||||||
sections = sections,
|
sections = sections,
|
||||||
|
@ -248,7 +253,7 @@ class SpaceRoomListSectionBuilder(
|
||||||
}.subscribe {
|
}.subscribe {
|
||||||
liveSuggestedRooms.postValue(it)
|
liveSuggestedRooms.postValue(it)
|
||||||
}.also {
|
}.also {
|
||||||
onDisposable.invoke(it)
|
disposables.add(it)
|
||||||
}
|
}
|
||||||
sections.add(
|
sections.add(
|
||||||
RoomsSection(
|
RoomsSection(
|
||||||
|
@ -259,9 +264,11 @@ class SpaceRoomListSectionBuilder(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDmSections(sections: MutableList<RoomsSection>, activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
|
private fun buildDmSections(sections: MutableList<RoomsSection>,
|
||||||
|
activeSpaceAwareQueries: MutableList<RoomListViewModel.ActiveSpaceQueryUpdater>) {
|
||||||
if (autoAcceptInvites.showInvites()) {
|
if (autoAcceptInvites.showInvites()) {
|
||||||
addSection(sections = sections,
|
addSection(
|
||||||
|
sections = sections,
|
||||||
activeSpaceUpdaters = activeSpaceAwareQueries,
|
activeSpaceUpdaters = activeSpaceAwareQueries,
|
||||||
nameRes = R.string.invitations_header,
|
nameRes = R.string.invitations_header,
|
||||||
notifyOfLocalEcho = true,
|
notifyOfLocalEcho = true,
|
||||||
|
@ -273,7 +280,8 @@ class SpaceRoomListSectionBuilder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addSection(sections,
|
addSection(
|
||||||
|
sections,
|
||||||
activeSpaceAwareQueries,
|
activeSpaceAwareQueries,
|
||||||
R.string.bottom_action_favourites,
|
R.string.bottom_action_favourites,
|
||||||
false,
|
false,
|
||||||
|
@ -284,7 +292,8 @@ class SpaceRoomListSectionBuilder(
|
||||||
it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
|
it.roomTagQueryFilter = RoomTagQueryFilter(true, null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
addSection(sections,
|
addSection(
|
||||||
|
sections,
|
||||||
activeSpaceAwareQueries,
|
activeSpaceAwareQueries,
|
||||||
R.string.bottom_action_people_x,
|
R.string.bottom_action_people_x,
|
||||||
false,
|
false,
|
||||||
|
@ -292,7 +301,19 @@ class SpaceRoomListSectionBuilder(
|
||||||
) {
|
) {
|
||||||
it.memberships = listOf(Membership.JOIN)
|
it.memberships = listOf(Membership.JOIN)
|
||||||
it.roomCategoryFilter = RoomCategoryFilter.ONLY_DM
|
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(
|
withQueryParams(
|
||||||
{ query.invoke(it) },
|
{ query.invoke(it) },
|
||||||
{ roomQueryParams ->
|
{ roomQueryParams ->
|
||||||
|
|
||||||
val name = stringProvider.getString(nameRes)
|
val name = stringProvider.getString(nameRes)
|
||||||
session.getFilteredPagedRoomSummariesLive(
|
session.getFilteredPagedRoomSummariesLive(
|
||||||
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
|
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
|
||||||
|
@ -349,7 +369,6 @@ class SpaceRoomListSectionBuilder(
|
||||||
}
|
}
|
||||||
}.livePagedList
|
}.livePagedList
|
||||||
.let { livePagedList ->
|
.let { livePagedList ->
|
||||||
|
|
||||||
// use it also as a source to update count
|
// use it also as a source to update count
|
||||||
livePagedList.asObservable()
|
livePagedList.asObservable()
|
||||||
.observeOn(Schedulers.computation())
|
.observeOn(Schedulers.computation())
|
||||||
|
@ -366,7 +385,7 @@ class SpaceRoomListSectionBuilder(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}.also {
|
}.also {
|
||||||
onDisposable.invoke(it)
|
disposables.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
sections.add(
|
sections.add(
|
||||||
|
@ -410,4 +429,8 @@ class SpaceRoomListSectionBuilder(
|
||||||
RoomListViewModel.SpaceFilterStrategy.NONE -> this
|
RoomListViewModel.SpaceFilterStrategy.NONE -> this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
disposables.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -120,38 +120,32 @@ class RoomListViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sections: List<RoomsSection> by lazy {
|
private val roomListSectionBuilder = if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
|
||||||
if (appStateHandler.getCurrentRoomGroupingMethod() is RoomGroupingMethod.BySpace) {
|
RoomListSectionBuilderSpace(
|
||||||
SpaceRoomListSectionBuilder(
|
|
||||||
session,
|
session,
|
||||||
stringProvider,
|
stringProvider,
|
||||||
appStateHandler,
|
appStateHandler,
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
|
autoAcceptInvites,
|
||||||
|
{
|
||||||
|
updatableQuery = it
|
||||||
|
},
|
||||||
suggestedRoomJoiningState,
|
suggestedRoomJoiningState,
|
||||||
autoAcceptInvites,
|
|
||||||
{
|
|
||||||
it.disposeOnClear()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
updatableQuery = it
|
|
||||||
},
|
|
||||||
vectorPreferences.labsSpacesOnlyOrphansInHome()
|
vectorPreferences.labsSpacesOnlyOrphansInHome()
|
||||||
).buildSections(initialState.displayMode)
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupRoomListSectionBuilder(
|
RoomListSectionBuilderGroup(
|
||||||
session,
|
session,
|
||||||
stringProvider,
|
stringProvider,
|
||||||
viewModelScope,
|
|
||||||
appStateHandler,
|
appStateHandler,
|
||||||
autoAcceptInvites,
|
autoAcceptInvites
|
||||||
{
|
) {
|
||||||
it.disposeOnClear()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
updatableQuery = it
|
updatableQuery = it
|
||||||
}
|
}
|
||||||
).buildSections(initialState.displayMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sections: List<RoomsSection> by lazy {
|
||||||
|
roomListSectionBuilder.buildSections(initialState.displayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: RoomListAction) {
|
override fun handle(action: RoomListAction) {
|
||||||
|
@ -341,4 +335,9 @@ class RoomListViewModel @Inject constructor(
|
||||||
_viewEvents.post(value)
|
_viewEvents.post(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
roomListSectionBuilder.dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ import androidx.fragment.app.Fragment
|
||||||
import im.vector.app.BuildConfig
|
import im.vector.app.BuildConfig
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.StringProvider
|
import im.vector.app.core.resources.StringProvider
|
||||||
|
import im.vector.app.core.services.CallService
|
||||||
import im.vector.app.core.utils.startNotificationChannelSettingsIntent
|
import im.vector.app.core.utils.startNotificationChannelSettingsIntent
|
||||||
import im.vector.app.features.call.VectorCallActivity
|
import im.vector.app.features.call.VectorCallActivity
|
||||||
import im.vector.app.features.call.service.CallHeadsUpActionReceiver
|
import im.vector.app.features.call.service.CallHeadsUpActionReceiver
|
||||||
|
@ -298,12 +299,14 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
.apply {
|
.apply {
|
||||||
if (call.mxCall.isVideoCall) {
|
if (call.mxCall.isVideoCall) {
|
||||||
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||||
} else {
|
} else {
|
||||||
setContentText(stringProvider.getString(R.string.incoming_voice_call))
|
setContentText(stringProvider.getString(R.string.incoming_voice_call))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setLights(accentColor, 500, 500)
|
.setLights(accentColor, 500, 500)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
|
@ -339,8 +342,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
builder.addAction(
|
builder.addAction(
|
||||||
NotificationCompat.Action(
|
NotificationCompat.Action(
|
||||||
R.drawable.ic_call_answer,
|
R.drawable.ic_call_answer,
|
||||||
// IconCompat.createWithResource(applicationContext, R.drawable.ic_call)
|
|
||||||
// .setTint(ContextCompat.getColor(applicationContext, R.color.vctr_positive_accent)),
|
|
||||||
getActionText(R.string.call_notification_answer, R.attr.colorPrimary),
|
getActionText(R.string.call_notification_answer, R.attr.colorPrimary),
|
||||||
answerCallPendingIntent
|
answerCallPendingIntent
|
||||||
)
|
)
|
||||||
|
@ -360,10 +361,15 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
.setContentTitle(ensureTitleNotEmpty(title))
|
.setContentTitle(ensureTitleNotEmpty(title))
|
||||||
.apply {
|
.apply {
|
||||||
setContentText(stringProvider.getString(R.string.call_ring))
|
setContentText(stringProvider.getString(R.string.call_ring))
|
||||||
|
if (call.mxCall.isVideoCall) {
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||||
|
} else {
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
.setLights(accentColor, 500, 500)
|
.setLights(accentColor, 500, 500)
|
||||||
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
|
|
||||||
val contentIntent = VectorCallActivity.newIntent(
|
val contentIntent = VectorCallActivity.newIntent(
|
||||||
|
@ -407,11 +413,13 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
||||||
.apply {
|
.apply {
|
||||||
if (call.mxCall.isVideoCall) {
|
if (call.mxCall.isVideoCall) {
|
||||||
setContentText(stringProvider.getString(R.string.video_call_in_progress))
|
setContentText(stringProvider.getString(R.string.video_call_in_progress))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer_video)
|
||||||
} else {
|
} else {
|
||||||
setContentText(stringProvider.getString(R.string.call_in_progress))
|
setContentText(stringProvider.getString(R.string.call_in_progress))
|
||||||
|
setSmallIcon(R.drawable.ic_call_answer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
|
||||||
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
val rejectCallPendingIntent = buildRejectCallPendingIntent(call.callId)
|
||||||
|
@ -450,15 +458,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
|
* Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended
|
||||||
*/
|
*/
|
||||||
fun buildCallEndedNotification(): Notification {
|
fun buildCallEndedNotification(isVideoCall: Boolean): Notification {
|
||||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle(stringProvider.getString(R.string.call_ended))
|
.setContentTitle(stringProvider.getString(R.string.call_ended))
|
||||||
.setTimeoutAfter(2000)
|
.apply {
|
||||||
.setSmallIcon(R.drawable.ic_material_call_end_grey)
|
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)
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build notification for the CallService, when a call is missed
|
||||||
|
*/
|
||||||
|
fun buildCallMissedNotification(callInformation: CallService.CallInformation): Notification {
|
||||||
|
val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setContentTitle(callInformation.opponentMatrixItem?.getBestName() ?: callInformation.opponentUserId)
|
||||||
|
.apply {
|
||||||
|
if (callInformation.isVideoCall) {
|
||||||
|
setContentText(stringProvider.getQuantityString(R.plurals.missed_video_call, 1, 1))
|
||||||
|
setSmallIcon(R.drawable.ic_missed_video_call)
|
||||||
|
} else {
|
||||||
|
setContentText(stringProvider.getQuantityString(R.plurals.missed_audio_call, 1, 1))
|
||||||
|
setSmallIcon(R.drawable.ic_missed_voice_call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||||
|
|
||||||
|
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||||
|
.addNextIntentWithParentStack(HomeActivity.newIntent(context))
|
||||||
|
.addNextIntent(RoomDetailActivity.newIntent(context, RoomDetailArgs(callInformation.nativeRoomId)))
|
||||||
|
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
|
builder.setContentIntent(contentPendingIntent)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
|
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
|
||||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||||
.setGroup(stringProvider.getString(R.string.app_name))
|
.setGroup(stringProvider.getString(R.string.app_name))
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M0,3.2273C0,1.6457 1.3432,0.3636 3,0.3636H14C15.6569,0.3636 17,1.6457 17,3.2273V12.7727C17,14.3543 15.6569,15.6364 14,15.6364H3C1.3431,15.6364 0,14.3543 0,12.7727V3.2273ZM19,5.1364L22.3753,2.5589C23.0301,2.0589 24,2.5038 24,3.3042V12.6958C24,13.4962 23.0301,13.9412 22.3753,13.4412L19,10.8637V5.1364ZM5.5288,8.8219C5.5288,9.2423 5.1848,9.5863 4.7644,9.5863C4.344,9.5863 4,9.2423 4,8.8219V5.7644C4,5.344 4.344,5 4.7644,5H7.8219C8.2423,5 8.5863,5.344 8.5863,5.7644C8.5863,6.1848 8.2423,6.5288 7.8219,6.5288H6.5989L9.3125,9.2423L13.0961,5.4586C13.3942,5.1605 13.8758,5.1605 14.1739,5.4586C14.472,5.7567 14.472,6.2383 14.1739,6.5364L9.8475,10.8628C9.5494,11.1609 9.0679,11.1609 8.7697,10.8628L5.5288,7.6218V8.8219Z"
|
||||||
|
android:fillColor="#737D8C"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<vector android:height="10.666667dp" android:viewportHeight="16"
|
||||||
|
android:viewportWidth="24" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#737D8C" android:fillType="evenOdd" android:pathData="M0,3.2273C0,1.6457 1.3432,0.3636 3,0.3636H14C15.6569,0.3636 17,1.6457 17,3.2273V12.7727C17,14.3543 15.6569,15.6364 14,15.6364H3C1.3431,15.6364 0,14.3543 0,12.7727V3.2273ZM19,5.1364L22.3753,2.5589C23.0301,2.0589 24,2.5038 24,3.3042V12.6958C24,13.4962 23.0301,13.9412 22.3753,13.4412L19,10.8637V5.1364ZM5.5288,8.8219C5.5288,9.2423 5.1848,9.5863 4.7644,9.5863C4.344,9.5863 4,9.2423 4,8.8219V5.7644C4,5.344 4.344,5 4.7644,5H7.8219C8.2423,5 8.5863,5.344 8.5863,5.7644C8.5863,6.1848 8.2423,6.5288 7.8219,6.5288H6.5989L9.3125,9.2423L13.0961,5.4586C13.3942,5.1605 13.8758,5.1605 14.1739,5.4586C14.472,5.7567 14.472,6.2383 14.1739,6.5364L9.8475,10.8628C9.5494,11.1609 9.0679,11.1609 8.7697,10.8628L5.5288,7.6218V8.8219Z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M6,9C6.55,9 7,8.55 7,8V6.43L11.24,10.67C11.63,11.06 12.26,11.06 12.65,10.67L18.31,5.01C18.7,4.62 18.7,3.99 18.31,3.6C17.92,3.21 17.29,3.21 16.9,3.6L11.95,8.55L8.4,5H10C10.55,5 11,4.55 11,4C11,3.45 10.55,3 10,3H6C5.45,3 5,3.45 5,4V8C5,8.55 5.45,9 6,9Z"
|
||||||
|
android:fillColor="#818A98"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.0084,13.0065C10.3211,12.9416 6.8514,13.3795 6.0078,13.6013C5.9579,13.6144 5.9004,13.6291 5.8362,13.6455C4.541,13.9761 0.4827,15.0118 0.0442,18.2936C-0.2955,20.8362 1.4058,21.6058 2.2562,21.4886C2.8448,21.4148 4.5301,21.1483 6.0872,20.8689C7.6163,20.5946 7.6155,19.5859 7.615,18.9038C7.615,18.8913 7.615,18.8788 7.615,18.8665L7.615,17.4953C7.615,17.1461 7.9432,16.9442 8.3958,16.8896C9.9982,16.672 11.3359,16.6713 12.0055,16.6713L12.0112,16.6713C12.6807,16.6713 14.0018,16.672 15.6042,16.8896C16.0569,16.9442 16.385,17.1461 16.385,17.4953L16.385,18.8665C16.385,18.8789 16.385,18.8913 16.385,18.9038C16.3845,19.5859 16.3837,20.5946 17.9128,20.869C19.4699,21.1483 21.1552,21.4148 21.7438,21.4886C22.5942,21.6058 24.2955,20.8362 23.9558,18.2936C23.5173,15.0118 19.459,13.9761 18.1638,13.6455C18.0996,13.6291 18.0421,13.6145 17.9922,13.6013C17.1487,13.3795 13.6956,12.9416 12.0084,13.0065Z"
|
||||||
|
android:fillColor="#818A98"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="16dp" android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#818A98" android:pathData="M6,9C6.55,9 7,8.55 7,8V6.43L11.24,10.67C11.63,11.06 12.26,11.06 12.65,10.67L18.31,5.01C18.7,4.62 18.7,3.99 18.31,3.6C17.92,3.21 17.29,3.21 16.9,3.6L11.95,8.55L8.4,5H10C10.55,5 11,4.55 11,4C11,3.45 10.55,3 10,3H6C5.45,3 5,3.45 5,4V8C5,8.55 5.45,9 6,9Z"/>
|
||||||
|
<path android:fillColor="#818A98" android:pathData="M12.0084,13.0065C10.3211,12.9416 6.8514,13.3795 6.0078,13.6013C5.9579,13.6144 5.9004,13.6291 5.8362,13.6455C4.541,13.9761 0.4827,15.0118 0.0442,18.2936C-0.2955,20.8362 1.4058,21.6058 2.2562,21.4886C2.8448,21.4148 4.5301,21.1483 6.0872,20.8689C7.6163,20.5946 7.6155,19.5859 7.615,18.9038C7.615,18.8913 7.615,18.8788 7.615,18.8665L7.615,17.4953C7.615,17.1461 7.9432,16.9442 8.3958,16.8896C9.9982,16.672 11.3359,16.6713 12.0055,16.6713L12.0112,16.6713C12.6807,16.6713 14.0018,16.672 15.6042,16.8896C16.0569,16.9442 16.385,17.1461 16.385,17.4953L16.385,18.8665C16.385,18.8789 16.385,18.8913 16.385,18.9038C16.3845,19.5859 16.3837,20.5946 17.9128,20.869C19.4699,21.1483 21.1552,21.4148 21.7438,21.4886C22.5942,21.6058 24.2955,20.8362 23.9558,18.2936C23.5173,15.0118 19.459,13.9761 18.1638,13.6455C18.0996,13.6291 18.0421,13.6145 17.9922,13.6013C17.1487,13.3795 13.6956,12.9416 12.0084,13.0065Z"/>
|
||||||
|
</vector>
|
|
@ -74,19 +74,19 @@
|
||||||
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
|
<string name="notice_direct_room_update_by_you">You upgraded here.</string>
|
||||||
<string name="notice_room_server_acl_set_title">%s set the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_set_title">%s set the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_set_title_by_you">You set the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_set_title_by_you">You set the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_set_banned">• Server matching %s are banned.</string>
|
<string name="notice_room_server_acl_set_banned">• Servers matching %s are banned.</string>
|
||||||
<string name="notice_room_server_acl_set_allowed">• Server matching %s are allowed.</string>
|
<string name="notice_room_server_acl_set_allowed">• Servers matching %s are allowed.</string>
|
||||||
<string name="notice_room_server_acl_set_ip_literals_allowed">• Server matching IP literals are allowed.</string>
|
<string name="notice_room_server_acl_set_ip_literals_allowed">• Servers matching IP literals are allowed.</string>
|
||||||
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Server matching IP literals are banned.</string>
|
<string name="notice_room_server_acl_set_ip_literals_not_allowed">• Servers matching IP literals are banned.</string>
|
||||||
|
|
||||||
<string name="notice_room_server_acl_updated_title">%s changed the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_updated_title">%s changed the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_updated_title_by_you">You changed the server ACLs for this room.</string>
|
<string name="notice_room_server_acl_updated_title_by_you">You changed the server ACLs for this room.</string>
|
||||||
<string name="notice_room_server_acl_updated_banned">• Server matching %s are now banned.</string>
|
<string name="notice_room_server_acl_updated_banned">• Servers matching %s are now banned.</string>
|
||||||
<string name="notice_room_server_acl_updated_was_banned">• Server matching %s were removed from the ban list.</string>
|
<string name="notice_room_server_acl_updated_was_banned">• Servers matching %s were removed from the ban list.</string>
|
||||||
<string name="notice_room_server_acl_updated_allowed">• Server matching %s are now allowed.</string>
|
<string name="notice_room_server_acl_updated_allowed">• Servers matching %s are now allowed.</string>
|
||||||
<string name="notice_room_server_acl_updated_was_allowed">• Server matching %s were removed from the allowed list.</string>
|
<string name="notice_room_server_acl_updated_was_allowed">• Servers matching %s were removed from the allowed list.</string>
|
||||||
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Server matching IP literals are now allowed.</string>
|
<string name="notice_room_server_acl_updated_ip_literals_allowed">• Servers matching IP literals are now allowed.</string>
|
||||||
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Server matching IP literals are now banned.</string>
|
<string name="notice_room_server_acl_updated_ip_literals_not_allowed">• Servers matching IP literals are now banned.</string>
|
||||||
<string name="notice_room_server_acl_updated_no_change">No change.</string>
|
<string name="notice_room_server_acl_updated_no_change">No change.</string>
|
||||||
<string name="notice_room_server_acl_allow_is_empty">🎉 All servers are banned from participating! This room can no longer be used.</string>
|
<string name="notice_room_server_acl_allow_is_empty">🎉 All servers are banned from participating! This room can no longer be used.</string>
|
||||||
|
|
||||||
|
@ -543,7 +543,7 @@
|
||||||
<string name="logout">Sign out</string>
|
<string name="logout">Sign out</string>
|
||||||
<string name="hs_url">Homeserver URL</string>
|
<string name="hs_url">Homeserver URL</string>
|
||||||
<string name="hs_client_url">Homeserver API URL</string>
|
<string name="hs_client_url">Homeserver API URL</string>
|
||||||
<string name="identity_url">Identity Server URL</string>
|
<string name="identity_url">Identity server URL</string>
|
||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
|
|
||||||
<string name="start_new_chat">Start New Chat</string>
|
<string name="start_new_chat">Start New Chat</string>
|
||||||
|
@ -633,7 +633,7 @@
|
||||||
<string name="auth_recaptcha_message">This homeserver would like to make sure you are not a robot</string>
|
<string name="auth_recaptcha_message">This homeserver would like to make sure you are not a robot</string>
|
||||||
<string name="auth_username_in_use">Username in use</string>
|
<string name="auth_username_in_use">Username in use</string>
|
||||||
<string name="auth_home_server">Homeserver:</string>
|
<string name="auth_home_server">Homeserver:</string>
|
||||||
<string name="auth_identity_server">Identity Server:</string>
|
<string name="auth_identity_server">Identity server:</string>
|
||||||
<string name="auth_reset_password_next_step_button">I have verified my email address</string>
|
<string name="auth_reset_password_next_step_button">I have verified my email address</string>
|
||||||
<string name="auth_reset_password_message">To reset your password, enter the email address linked to your account:</string>
|
<string name="auth_reset_password_message">To reset your password, enter the email address linked to your account:</string>
|
||||||
<string name="auth_reset_password_missing_email">The email address linked to your account must be entered.</string>
|
<string name="auth_reset_password_missing_email">The email address linked to your account must be entered.</string>
|
||||||
|
@ -727,6 +727,14 @@
|
||||||
<string name="call_connected">Call connected</string>
|
<string name="call_connected">Call connected</string>
|
||||||
<string name="call_connecting">Call connecting…</string>
|
<string name="call_connecting">Call connecting…</string>
|
||||||
<string name="call_ended">Call ended</string>
|
<string name="call_ended">Call ended</string>
|
||||||
|
<plurals name="missed_audio_call">
|
||||||
|
<item quantity="one">Missed audio call</item>
|
||||||
|
<item quantity="other">%d missed audio calls</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="missed_video_call">
|
||||||
|
<item quantity="one">Missed video call</item>
|
||||||
|
<item quantity="other">%d missed video calls</item>
|
||||||
|
</plurals>
|
||||||
<string name="call_ring">Calling…</string>
|
<string name="call_ring">Calling…</string>
|
||||||
<string name="incoming_call">Incoming Call</string>
|
<string name="incoming_call">Incoming Call</string>
|
||||||
<string name="incoming_video_call">Incoming Video Call</string>
|
<string name="incoming_video_call">Incoming Video Call</string>
|
||||||
|
@ -1128,8 +1136,8 @@
|
||||||
<string name="settings_troubleshoot_test_fcm_failed_account_missing_quick_fix">Add Account</string>
|
<string name="settings_troubleshoot_test_fcm_failed_account_missing_quick_fix">Add Account</string>
|
||||||
|
|
||||||
<string name="settings_troubleshoot_test_token_registration_title">Token Registration</string>
|
<string name="settings_troubleshoot_test_token_registration_title">Token Registration</string>
|
||||||
<string name="settings_troubleshoot_test_token_registration_success">FCM token successfully registered to HomeServer.</string>
|
<string name="settings_troubleshoot_test_token_registration_success">FCM token successfully registered to homeserver.</string>
|
||||||
<string name="settings_troubleshoot_test_token_registration_failed">Failed to register FCM token to HomeServer:\n%1$s</string>
|
<string name="settings_troubleshoot_test_token_registration_failed">Failed to register FCM token to homeserver:\n%1$s</string>
|
||||||
|
|
||||||
<string name="settings_troubleshoot_test_push_loop_title">Test Push</string>
|
<string name="settings_troubleshoot_test_push_loop_title">Test Push</string>
|
||||||
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">The application is waiting for the PUSH</string>
|
<string name="settings_troubleshoot_test_push_loop_waiting_for_push">The application is waiting for the PUSH</string>
|
||||||
|
@ -1171,7 +1179,7 @@
|
||||||
<string name="settings_notification_privacy_normal">Normal</string>
|
<string name="settings_notification_privacy_normal">Normal</string>
|
||||||
<string name="settings_notification_privacy_reduced">Reduced privacy</string>
|
<string name="settings_notification_privacy_reduced">Reduced privacy</string>
|
||||||
<string name="settings_notification_privacy_need_permission">The app needs permission to run in the background</string>
|
<string name="settings_notification_privacy_need_permission">The app needs permission to run in the background</string>
|
||||||
<string name="settings_notification_privacy_no_background_sync">The apps does <b>not</b> need to connect to the HomeServer in the background, it should reduce battery usage</string>
|
<string name="settings_notification_privacy_no_background_sync">The apps does <b>not</b> need to connect to the homeserver in the background, it should reduce battery usage</string>
|
||||||
<string name="settings_notification_privacy_fcm">• Notifications are sent via Firebase Cloud Messaging</string>
|
<string name="settings_notification_privacy_fcm">• Notifications are sent via Firebase Cloud Messaging</string>
|
||||||
<string name="settings_notification_privacy_metadata">• Notifications only contain meta data</string>
|
<string name="settings_notification_privacy_metadata">• Notifications only contain meta data</string>
|
||||||
<string name="settings_notification_privacy_secure_message_content">• Message content of the notification is <b>located securely direct from the Matrix homeserver</b></string>
|
<string name="settings_notification_privacy_secure_message_content">• Message content of the notification is <b>located securely direct from the Matrix homeserver</b></string>
|
||||||
|
@ -1236,7 +1244,7 @@
|
||||||
<string name="settings_other">Other</string>
|
<string name="settings_other">Other</string>
|
||||||
<string name="settings_advanced">Advanced</string>
|
<string name="settings_advanced">Advanced</string>
|
||||||
<string name="settings_integrations">Integrations</string>
|
<string name="settings_integrations">Integrations</string>
|
||||||
<string name="settings_integrations_summary">Use an Integration Manager to manage bots, bridges, widgets and sticker packs.\nIntegration Managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf.</string>
|
<string name="settings_integrations_summary">Use an integration manager to manage bots, bridges, widgets and sticker packs.\nIntegration managers receive configuration data, and can modify widgets, send room invites and set power levels on your behalf.</string>
|
||||||
<string name="settings_cryptography">Cryptography</string>
|
<string name="settings_cryptography">Cryptography</string>
|
||||||
<string name="settings_cryptography_manage_keys">Cryptography Keys Management</string>
|
<string name="settings_cryptography_manage_keys">Cryptography Keys Management</string>
|
||||||
<string name="settings_notifications_targets">Notification Targets</string>
|
<string name="settings_notifications_targets">Notification Targets</string>
|
||||||
|
@ -1321,9 +1329,9 @@
|
||||||
|
|
||||||
<string name="settings_logged_in">Logged in as</string>
|
<string name="settings_logged_in">Logged in as</string>
|
||||||
<string name="settings_home_server">Homeserver</string>
|
<string name="settings_home_server">Homeserver</string>
|
||||||
<string name="settings_identity_server">Identity Server</string>
|
<string name="settings_identity_server">Identity server</string>
|
||||||
<string name="settings_integration_allow">Allow integrations</string>
|
<string name="settings_integration_allow">Allow integrations</string>
|
||||||
<string name="settings_integration_manager">Integration Manager</string>
|
<string name="settings_integration_manager">Integration manager</string>
|
||||||
|
|
||||||
<string name="disabled_integration_dialog_title">Integrations are disabled</string>
|
<string name="disabled_integration_dialog_title">Integrations are disabled</string>
|
||||||
<string name="disabled_integration_dialog_content">"Enable 'Allow integrations' in Settings to do this."</string>
|
<string name="disabled_integration_dialog_content">"Enable 'Allow integrations' in Settings to do this."</string>
|
||||||
|
@ -1698,7 +1706,7 @@
|
||||||
<string name="room_widget_webview_access_microphone">Use the microphone</string>
|
<string name="room_widget_webview_access_microphone">Use the microphone</string>
|
||||||
<string name="room_widget_webview_read_protected_media">Read DRM protected Media</string>
|
<string name="room_widget_webview_read_protected_media">Read DRM protected Media</string>
|
||||||
|
|
||||||
<!-- Widget Integration Manager -->
|
<!-- Widget integration manager -->
|
||||||
|
|
||||||
<string name="widget_integration_unable_to_create">Unable to create widget.</string>
|
<string name="widget_integration_unable_to_create">Unable to create widget.</string>
|
||||||
<string name="widget_integration_failed_to_send_request">Failed to send request.</string>
|
<string name="widget_integration_failed_to_send_request">Failed to send request.</string>
|
||||||
|
@ -1909,7 +1917,7 @@
|
||||||
<string name="recovery_key_export_saved_as_warning">The recovery key has been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
|
<string name="recovery_key_export_saved_as_warning">The recovery key has been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
|
||||||
<string name="recovery_key_export_saved">The recovery key has been saved.</string>
|
<string name="recovery_key_export_saved">The recovery key has been saved.</string>
|
||||||
|
|
||||||
<string name="keys_backup_setup_override_backup_prompt_tile">A backup already exist on your HomeServer</string>
|
<string name="keys_backup_setup_override_backup_prompt_tile">A backup already exist on your homeserver</string>
|
||||||
<string name="keys_backup_setup_override_backup_prompt_description">It looks like you already have setup key backup from another session. Do you want to replace it with the one you’re creating?</string>
|
<string name="keys_backup_setup_override_backup_prompt_description">It looks like you already have setup key backup from another session. Do you want to replace it with the one you’re creating?</string>
|
||||||
<string name="keys_backup_setup_override_replace">Replace</string>
|
<string name="keys_backup_setup_override_replace">Replace</string>
|
||||||
<string name="keys_backup_setup_override_stop">Stop</string>
|
<string name="keys_backup_setup_override_stop">Stop</string>
|
||||||
|
@ -2075,7 +2083,7 @@
|
||||||
<string name="sas_error_unknown">Unknown Error</string>
|
<string name="sas_error_unknown">Unknown Error</string>
|
||||||
|
|
||||||
<!-- Identity server -->
|
<!-- Identity server -->
|
||||||
<string name="identity_server_not_defined">You are not using any Identity Server</string>
|
<string name="identity_server_not_defined">You are not using any identity server</string>
|
||||||
<string name="identity_server_not_defined_for_password_reset">No identity server is configured, it is required to reset your password.</string>
|
<string name="identity_server_not_defined_for_password_reset">No identity server is configured, it is required to reset your password.</string>
|
||||||
|
|
||||||
<string name="error_user_already_logged_in">It looks like you’re trying to connect to another homeserver. Do you want to sign out?</string>
|
<string name="error_user_already_logged_in">It looks like you’re trying to connect to another homeserver. Do you want to sign out?</string>
|
||||||
|
@ -2273,7 +2281,7 @@
|
||||||
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
|
<string name="settings_discovery_consent_action_give_consent">Give consent</string>
|
||||||
|
|
||||||
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
|
<string name="identity_server_consent_dialog_title">Send emails and phone numbers</string>
|
||||||
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured Identity Server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
|
<string name="identity_server_consent_dialog_content">In order to discover existing contacts you know, do you accept to send your contact data (phone numbers and/or emails) to the configured identity server (%1$s)?\n\nFor more privacy, the sent data will be hashed before being sent.</string>
|
||||||
|
|
||||||
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
|
<string name="settings_discovery_enter_identity_server">Enter an identity server URL</string>
|
||||||
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
|
<string name="settings_discovery_bad_identity_server">Could not connect to identity server</string>
|
||||||
|
|
Loading…
Reference in New Issue