diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 16cc35cebe..4de90e9405 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -26,6 +26,7 @@ pkcs previewable previewables + pstn riotx signin signout diff --git a/CHANGES.md b/CHANGES.md index 091b49791d..f84d182cc8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Bugfix 🐛: - Fix crash after initial sync on Dendrite - Fix crash reported by PlayStore (#2707) - Ignore url override from credential if it is not valid (#2822) + - Fix crash when deactivating an account Translations 🗣: - @@ -31,6 +32,7 @@ Test: Other changes: - New Dev Tools panel for developers - Fix typos in CHANGES.md (#2811) + - Colors rework: first step: merge file `colors_riot.xml` to file `colors_riotx.xml` and rename the file to `colors.xml` Changes in Element 1.0.17 (2021-02-09) =================================================== diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 78b9cb20ed..01c4f8ccb3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -43,12 +43,13 @@ class DeactivateAccountTest : InstrumentedTest { @Test fun deactivateAccountTest() { - val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true)) // Deactivate the account commonTestHelper.runBlockingTest { session.deactivateAccount( - object : UserInteractiveAuthInterceptor { + eraseAllData = false, + userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { promise.resume( UserPasswordAuth( @@ -58,7 +59,8 @@ class DeactivateAccountTest : InstrumentedTest { ) ) } - }, false) + } + ) } // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index c06cdd9e23..e0ee9f36ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -53,22 +53,24 @@ fun Throwable.isInvalidUIAAuth(): Boolean { * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible */ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { - return if (this is Failure.OtherServerError && this.httpCode == 401) { + return if (this is Failure.OtherServerError && httpCode == 401) { tryOrNull { MoshiProvider.providesMoshi() .adapter(RegistrationFlowResponse::class.java) - .fromJson(this.errorBody) + .fromJson(errorBody) } - } else if (this is Failure.ServerError && this.httpCode == 401 && this.error.code == MatrixError.M_FORBIDDEN) { + } else if (this is Failure.ServerError && httpCode == 401 && error.code == MatrixError.M_FORBIDDEN) { // This happens when the submission for this stage was bad (like bad password) - if (this.error.session != null && this.error.flows != null) { + if (error.session != null && error.flows != null) { RegistrationFlowResponse( - flows = this.error.flows, - session = this.error.session, - completedStages = this.error.completedStages, - params = this.error.params + flows = error.flows, + session = error.session, + completedStages = error.completedStages, + params = error.params ) - } else null + } else { + null + } } else { null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt index eb327dfd56..1f28dbd8af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -27,7 +27,8 @@ interface AccountService { * @param password Current password. * @param newPassword New password */ - suspend fun changePassword(password: String, newPassword: String) + suspend fun changePassword(password: String, + newPassword: String) /** * Deactivate the account. @@ -41,9 +42,10 @@ interface AccountService { * be shared with any new or unregistered users, but registered users who already have access to these messages will still * have access to their copy. * - * @param password the account password * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * an incomplete view of conversations + * @param userInteractiveAuthInterceptor see [UserInteractiveAuthInterceptor] */ - suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) + suspend fun deactivateAccount(eraseAllData: Boolean, + userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index c6bdcd19c7..dc67aa536a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -16,12 +16,11 @@ package org.matrix.android.sdk.api.session.call -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.util.Cancelable - interface CallSignalingService { - fun getTurnServer(callback: MatrixCallback): Cancelable + suspend fun getTurnServer(): TurnServerResponse + + fun getPSTNProtocolChecker(): PSTNProtocolChecker /** * Create an outgoing call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt new file mode 100644 index 0000000000..6627f62e24 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/PSTNProtocolChecker.kt @@ -0,0 +1,98 @@ +/* + * 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.call + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.internal.session.SessionScope +import org.matrix.android.sdk.internal.session.thirdparty.GetThirdPartyProtocolsTask +import org.matrix.android.sdk.internal.task.TaskExecutor +import timber.log.Timber +import java.util.concurrent.atomic.AtomicBoolean +import javax.inject.Inject + +private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn" +private const val PSTN_MATRIX_KEY = "m.protocol.pstn" + +/** + * This class is responsible for checking if the HS support the PSTN protocol. + * As long as the request succeed, it'll check only once by session. + */ +@SessionScope +class PSTNProtocolChecker @Inject internal constructor(private val taskExecutor: TaskExecutor, + private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask) { + + interface Listener { + fun onPSTNSupportUpdated() + } + + private var alreadyChecked = AtomicBoolean(false) + + private val pstnSupportListeners = mutableListOf() + + fun addListener(listener: Listener) { + pstnSupportListeners.add(listener) + } + + fun removeListener(listener: Listener) { + pstnSupportListeners.remove(listener) + } + + var supportedPSTNProtocol: String? = null + private set + + fun checkForPSTNSupportIfNeeded() { + if (alreadyChecked.get()) return + taskExecutor.executorScope.checkForPSTNSupport() + } + + private fun CoroutineScope.checkForPSTNSupport() = launch { + try { + supportedPSTNProtocol = getSupportedPSTN(3) + alreadyChecked.set(true) + if (supportedPSTNProtocol != null) { + pstnSupportListeners.forEach { + tryOrNull { it.onPSTNSupportUpdated() } + } + } + } catch (failure: Throwable) { + Timber.v("Fail to get supported PSTN, will check again next time.") + } + } + + private suspend fun getSupportedPSTN(maxTries: Int): String? { + val thirdPartyProtocols: Map = try { + getThirdPartyProtocolsTask.execute(Unit) + } catch (failure: Throwable) { + if (maxTries == 1) { + throw failure + } else { + // Wait for 10s before trying again + delay(10_000L) + return getSupportedPSTN(maxTries - 1) + } + } + return when { + thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY + thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY + else -> null + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index fa5ea359e8..eead9b4ab7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -56,8 +56,6 @@ interface CryptoService { fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) - fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) - fun getCryptoVersion(context: Context, longFormat: Boolean): String fun isCryptoEnabled(): Boolean diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt index 1a0383cb22..da0866a5fd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt @@ -16,14 +16,25 @@ package org.matrix.android.sdk.internal.auth.registration +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse -import org.matrix.android.sdk.api.auth.UIABaseAuth import timber.log.Timber import kotlin.coroutines.suspendCoroutine -internal suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveAuthInterceptor, retryBlock: suspend (UIABaseAuth) -> Unit): Boolean { +/** + * Handle a UIA challenge + * + * @param failure the failure to handle + * @param interceptor see doc in [UserInteractiveAuthInterceptor] + * @param retryBlock called at the end of the process, in this block generally retry executing the task, with + * provided authUpdate + * @return true if UIA is handled without error + */ +internal suspend fun handleUIA(failure: Throwable, + interceptor: UserInteractiveAuthInterceptor, + retryBlock: suspend (UIABaseAuth) -> Unit): Boolean { Timber.d("## UIA: check error ${failure.message}") val flowResponse = failure.toRegistrationFlowResponse() ?: return false.also { @@ -38,16 +49,16 @@ internal suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveA suspendCoroutine { continuation -> interceptor.performStage(flowResponse, (failure as? Failure.ServerError)?.error?.code, continuation) } - } catch (failure: Throwable) { - Timber.w(failure, "## UIA: failed to participate") + } catch (failure2: Throwable) { + Timber.w(failure2, "## UIA: failed to participate") return false } - Timber.d("## UIA: updated auth $authUpdate") + Timber.d("## UIA: updated auth") return try { retryBlock(authUpdate) true - } catch (failure: Throwable) { - handleUIA(failure, interceptor, retryBlock) + } catch (failure3: Throwable) { + handleUIA(failure3, interceptor, retryBlock) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index a786ebd4b2..e114f86a99 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -61,7 +61,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultGetDeviceInfoTask @@ -75,7 +74,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadKeysTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultUploadSigningKeysTask import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.crypto.tasks.EncryptEventTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask @@ -240,9 +238,6 @@ internal abstract class CryptoModule { @Binds abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask - @Binds - abstract fun bindDeleteDeviceWithUserPasswordTask(task: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask - @Binds abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 678bc9819f..67229a5eae 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -75,7 +75,6 @@ import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask @@ -153,9 +152,8 @@ internal class DefaultCryptoService @Inject constructor( // Repository private val megolmEncryptionFactory: MXMegolmEncryptionFactory, private val olmEncryptionFactory: MXOlmEncryptionFactory, - private val deleteDeviceTask: DeleteDeviceTask, - private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, // Tasks + private val deleteDeviceTask: DeleteDeviceTask, private val getDevicesTask: GetDevicesTask, private val getDeviceInfoTask: GetDeviceInfoTask, private val setDeviceNameTask: SetDeviceNameTask, @@ -217,15 +215,6 @@ internal class DefaultCryptoService @Inject constructor( .executeBy(taskExecutor) } - override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) { - deleteDeviceWithUserPasswordTask - .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) { - this.executionThread = TaskThread.CRYPTO - this.callback = callback - } - .executeBy(taskExecutor) - } - override fun getCryptoVersion(context: Context, longFormat: Boolean): String { return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index ff25ac0f66..61596bb5b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -47,12 +47,16 @@ internal class DefaultDeleteDeviceTask @Inject constructor( } } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null - || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> - execute(params.copy(userAuthParam = auth)) - } + || !handleUIA( + failure = throwable, + interceptor = params.userInteractiveAuthInterceptor, + retryBlock = { authUpdate -> + execute(params.copy(userAuthParam = authUpdate)) + } + ) ) { Timber.d("## UIA: propagate failure") - throw throwable + throw throwable } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt deleted file mode 100644 index dc0077425e..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 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.crypto.tasks - -import org.matrix.android.sdk.api.auth.data.LoginFlowTypes -import org.matrix.android.sdk.internal.crypto.api.CryptoApi -import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams -import org.matrix.android.sdk.api.auth.UserPasswordAuth -import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.network.GlobalErrorReceiver -import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.task.Task -import javax.inject.Inject - -internal interface DeleteDeviceWithUserPasswordTask : Task { - data class Params( - val deviceId: String, - val authSession: String?, - val password: String - ) -} - -internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor( - private val cryptoApi: CryptoApi, - @UserId private val userId: String, - private val globalErrorReceiver: GlobalErrorReceiver -) : DeleteDeviceWithUserPasswordTask { - - override suspend fun execute(params: DeleteDeviceWithUserPasswordTask.Params) { - return executeRequest(globalErrorReceiver) { - apiCall = cryptoApi.deleteDevice(params.deviceId, - DeleteDeviceParams( - auth = UserPasswordAuth( - type = LoginFlowTypes.PASSWORD, - session = params.authSession, - user = userId, - password = params.password - ).asMap() - ) - ) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index ef31130f55..f8a8354e48 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -126,11 +126,16 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor( uploadSigningKeysTask.execute(uploadSigningKeysParams) } catch (failure: Throwable) { if (params.interactiveAuthInterceptor == null - || !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate -> - uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) - }) { + || !handleUIA( + failure = failure, + interceptor = params.interactiveAuthInterceptor, + retryBlock = { authUpdate -> + uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate)) + } + ) + ) { Timber.d("## UIA: propagate failure") - throw failure + throw failure } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index d67b21567e..ca6b0554a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -16,10 +16,9 @@ package org.matrix.android.sdk.internal.session.account +import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.internal.auth.registration.handleUIA -import org.matrix.android.sdk.api.auth.UIABaseAuth -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.cleanup.CleanupSession @@ -30,8 +29,8 @@ import javax.inject.Inject internal interface DeactivateAccountTask : Task { data class Params( - val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, val eraseAllData: Boolean, + val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, val userAuthParam: UIABaseAuth? = null ) } @@ -39,7 +38,6 @@ internal interface DeactivateAccountTask : Task(globalErrorReceiver) { apiCall = accountAPI.deactivate(deactivateAccountParams) } + true } catch (throwable: Throwable) { - if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> - execute(params.copy(userAuthParam = auth)) - } + if (!handleUIA( + failure = throwable, + interceptor = params.userInteractiveAuthInterceptor, + retryBlock = { authUpdate -> + execute(params.copy(userAuthParam = authUpdate)) + } + ) ) { Timber.d("## UIA: propagate failure") - throw throwable + throw throwable + } else { + false } } - // Logout from identity server if any, ignoring errors - runCatching { identityDisconnectTask.execute(Unit) } - .onFailure { Timber.w(it, "Unable to disconnect identity server") } - cleanupSession.handle() + if (canCleanup) { + // Logout from identity server if any, ignoring errors + runCatching { identityDisconnectTask.execute(Unit) } + .onFailure { Timber.w(it, "Unable to disconnect identity server") } + + cleanupSession.handle() + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt index 25b67159a9..dc77d7bffb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -27,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) } - override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) { - deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData)) + override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { + deactivateAccountTask.execute(DeactivateAccountTask.Params(eraseAllData, userInteractiveAuthInterceptor)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 10690c59de..7d046cb642 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -16,16 +16,12 @@ package org.matrix.android.sdk.internal.session.call -import kotlinx.coroutines.Dispatchers -import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.call.CallListener 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.PSTNProtocolChecker import org.matrix.android.sdk.api.session.call.TurnServerResponse -import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.launchToCallback import timber.log.Timber import javax.inject.Inject @@ -34,14 +30,16 @@ internal class DefaultCallSignalingService @Inject constructor( private val callSignalingHandler: CallSignalingHandler, private val mxCallFactory: MxCallFactory, private val activeCallHandler: ActiveCallHandler, - private val taskExecutor: TaskExecutor, - private val turnServerDataSource: TurnServerDataSource + private val turnServerDataSource: TurnServerDataSource, + private val pstnProtocolChecker: PSTNProtocolChecker ) : CallSignalingService { - override fun getTurnServer(callback: MatrixCallback): Cancelable { - return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) { - turnServerDataSource.getTurnServer() - } + override suspend fun getTurnServer(): TurnServerResponse { + return turnServerDataSource.getTurnServer() + } + + override fun getPSTNProtocolChecker(): PSTNProtocolChecker { + return pstnProtocolChecker } override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt index 916a602936..c2a38af093 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -26,7 +26,6 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task @@ -47,11 +46,12 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor( private val profileAPI: ProfileAPI, @SessionDatabase private val monarchy: Monarchy, private val pendingThreePidMapper: PendingThreePidMapper, - @UserId private val userId: String, private val globalErrorReceiver: GlobalErrorReceiver) : FinalizeAddingThreePidTask() { override suspend fun execute(params: Params) { - if (params.userWantsToCancel.not()) { + val canCleanup = if (params.userWantsToCancel) { + true + } else { // Get the required pending data val pendingThreePids = monarchy.fetchAllMappedSync( { it.where(PendingThreePidEntity::class.java) }, @@ -69,21 +69,30 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor( ) apiCall = profileAPI.finalizeAddThreePid(body) } + true } catch (throwable: Throwable) { if (params.userInteractiveAuthInterceptor == null - || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth -> - execute(params.copy(userAuthParam = auth)) - } + || !handleUIA( + failure = throwable, + interceptor = params.userInteractiveAuthInterceptor, + retryBlock = { authUpdate -> + execute(params.copy(userAuthParam = authUpdate)) + } + ) ) { Timber.d("## UIA: propagate failure") - throw throwable.toRegistrationFlowResponse() + throw throwable.toRegistrationFlowResponse() ?.let { Failure.RegistrationFlowError(it) } ?: throwable + } else { + false } } } - cleanupDatabase(params) + if (canCleanup) { + cleanupDatabase(params) + } } private suspend fun cleanupDatabase(params: Params) { diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 067a1a4dfe..cd1abecfa5 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -41,6 +41,9 @@ parser.add_argument('-b', type=int, required=True, help='the buildkite build number.') +parser.add_argument('-f', + '--filename', + help='the filename, to download only one artifact.') parser.add_argument('-e', '--expecting', type=int, @@ -148,6 +151,8 @@ for elt in data: print(" %s: %s" % (key, str(value))) url = elt.get("download_url") filename = elt.get("filename") + if args.filename is not None and args.filename != filename: + continue target = targetDir + "/" + filename print("Downloading %s to '%s'..." % (filename, targetDir)) if not args.simulate: diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt new file mode 100644 index 0000000000..b77670ba76 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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.core.epoxy + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents + +@EpoxyModelClass(layout = R.layout.item_timeline_empty) +abstract class TimelineEmptyItem : VectorEpoxyModel(), ItemWithEvents { + + @EpoxyAttribute lateinit var eventId: String + + override fun getEventIds(): List { + return listOf(eventId) + } + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt index 0385973386..ce23111a95 100644 --- a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -113,7 +113,7 @@ class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory { override fun onResume() { super.onResume() - // It's the only way we have to know if sso falback flow was successful + // It's the only way we have to know if sso fallback flow was successful withState(sharedViewModel) { if (it.ssoFallbackPageWasShown) { Timber.d("## UIA ssoFallbackPageWasShown tentative success") diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 25b2a80a85..8a2d56a5a2 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.call +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory @@ -29,17 +30,16 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager -import org.matrix.android.sdk.api.MatrixCallback +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session 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.MxPeerConnectionState -import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem -import java.util.Timer -import java.util.TimerTask class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, @@ -50,7 +50,7 @@ class VectorCallViewModel @AssistedInject constructor( private var call: WebRtcCall? = null - private var connectionTimeoutTimer: Timer? = null + private var connectionTimeoutJob: Job? = null private var hasBeenConnectedOnce = false private val callListener = object : WebRtcCall.Listener { @@ -92,26 +92,20 @@ class VectorCallViewModel @AssistedInject constructor( val callState = call.state if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { hasBeenConnectedOnce = true - connectionTimeoutTimer?.cancel() - connectionTimeoutTimer = null + connectionTimeoutJob?.cancel() + connectionTimeoutJob = null } else { // do we reset as long as it's moving? - connectionTimeoutTimer?.cancel() + connectionTimeoutJob?.cancel() if (hasBeenConnectedOnce) { - connectionTimeoutTimer = Timer().apply { - schedule(object : TimerTask() { - override fun run() { - session.callSignalingService().getTurnServer(object : MatrixCallback { - override fun onFailure(failure: Throwable) { - _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null)) - } - - override fun onSuccess(data: TurnServerResponse) { - _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data)) - } - }) - } - }, 30_000) + connectionTimeoutJob = viewModelScope.launch { + delay(30_000) + try { + val turn = session.callSignalingService().getTurnServer() + _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(turn)) + } catch (failure: Throwable) { + _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null)) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt index 66370763e1..36a11b5923 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -83,12 +83,12 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un fun setAudioDevice(device: Device) { runInAudioThread(Runnable { if (!_availableDevices.contains(device)) { - Timber.w(" Audio device not available: $device") + Timber.w("Audio device not available: $device") userSelectedDevice = null return@Runnable } if (mode != Mode.DEFAULT) { - Timber.i(" User selected device set to: $device") + Timber.i("User selected device set to: $device") userSelectedDevice = device updateAudioRoute(mode, false) } @@ -108,7 +108,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un success = updateAudioRoute(mode, false) } catch (e: Throwable) { success = false - Timber.e(e, " Failed to update audio route for mode: " + mode) + Timber.e(e, "Failed to update audio route for mode: $mode") } if (success) { this@CallAudioManager.mode = mode @@ -124,7 +124,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un * `false`, otherwise. */ private fun updateAudioRoute(mode: Mode, force: Boolean): Boolean { - Timber.i(" Update audio route for mode: " + mode) + Timber.i("Update audio route for mode: $mode") if (!audioDeviceRouter?.setMode(mode).orFalse()) { return false } @@ -158,7 +158,7 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un return true } selectedDevice = audioDevice - Timber.i(" Selected audio device: " + audioDevice) + Timber.i("Selected audio device: $audioDevice") audioDeviceRouter?.setAudioRoute(audioDevice) configChange?.invoke() return true diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 1c5caee2cd..6fccea6c8c 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -22,20 +22,22 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import javax.inject.Inject -class DialPadLookup @Inject constructor(val session: Session, - val directRoomHelper: DirectRoomHelper, - val callManager: WebRtcCallManager +class DialPadLookup @Inject constructor( + private val session: Session, + private val directRoomHelper: DirectRoomHelper, + private val callManager: WebRtcCallManager ) { - class Failure : Throwable() + data class Result(val userId: String, val roomId: String) suspend fun lookupPhoneNumber(phoneNumber: String): Result { val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure() val thirdPartyUser = tryOrNull { - session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf( - "m.id.phone" to phoneNumber - )).firstOrNull() + session.thirdPartyService().getThirdPartyUser( + protocol = supportedProtocolKey, + fields = mapOf("m.id.phone" to phoneNumber) + ).firstOrNull() } ?: throw Failure() val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt deleted file mode 100644 index 3e6d2df690..0000000000 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2021 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 kotlinx.coroutines.delay -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol - -private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn" -private const val PSTN_MATRIX_KEY = "m.protocol.pstn" - -suspend fun Session.getSupportedPSTN(maxTries: Int): String? { - val thirdPartyProtocols: Map = try { - thirdPartyService().getThirdPartyProtocols() - } catch (failure: Throwable) { - if (maxTries == 1) { - return null - } else { - // Wait for 10s before trying again - delay(10_000L) - return getSupportedPSTN(maxTries - 1) - } - } - return when { - thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY - thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY - else -> null - } -} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index c72d7c8a76..469fba4d5e 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -53,7 +53,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.SdpType -import org.matrix.android.sdk.internal.util.awaitCallback import org.threeten.bp.Duration import org.webrtc.AudioSource import org.webrtc.AudioTrack @@ -420,9 +419,7 @@ class WebRtcCall(val mxCall: MxCall, private suspend fun getTurnServer(): TurnServerResponse? { return tryOrNull { - awaitCallback { - sessionProvider.get()?.callSignalingService()?.getTurnServer(it) - } + sessionProvider.get()?.callSignalingService()?.getTurnServer() } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 95728e0a97..2f8f84051e 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -26,14 +26,13 @@ import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.utils.EglUtils import im.vector.app.push.fcm.FcmHelper -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent @@ -65,22 +64,26 @@ class WebRtcCallManager @Inject constructor( private val currentSession: Session? get() = activeSessionDataSource.currentValue?.orNull() + private val pstnProtocolChecker: PSTNProtocolChecker? + get() = currentSession?.callSignalingService()?.getPSTNProtocolChecker() + interface CurrentCallListener { fun onCurrentCallChange(call: WebRtcCall?) {} fun onAudioDevicesChange() {} } - interface PSTNSupportListener { - fun onPSTNSupportUpdated() + val supportedPSTNProtocol: String? + get() = pstnProtocolChecker?.supportedPSTNProtocol + + val supportsPSTNProtocol: Boolean + get() = supportedPSTNProtocol != null + + fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) { + pstnProtocolChecker?.addListener(listener) } - private val pstnSupportListeners = emptyList().toMutableList() - fun addPstnSupportListener(listener: PSTNSupportListener) { - pstnSupportListeners.add(listener) - } - - fun removePstnSupportListener(listener: PSTNSupportListener) { - pstnSupportListeners.remove(listener) + fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) { + pstnProtocolChecker?.removeListener(listener) } private val currentCallsListeners = CopyOnWriteArrayList() @@ -104,27 +107,11 @@ class WebRtcCallManager @Inject constructor( private var peerConnectionFactory: PeerConnectionFactory? = null private val executor = Executors.newSingleThreadExecutor() private val dispatcher = executor.asCoroutineDispatcher() - var supportedPSTNProtocol: String? = null - private set - - val supportsPSTNProtocol: Boolean - get() = supportedPSTNProtocol != null private val rootEglBase by lazy { EglUtils.rootEglBase } private var isInBackground: Boolean = true - init { - GlobalScope.launch { - supportedPSTNProtocol = currentSession?.getSupportedPSTN(3) - if (supportedPSTNProtocol != null) { - pstnSupportListeners.forEach { - tryOrNull { it.onPSTNSupportUpdated() } - } - } - } - } - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { isInBackground = false @@ -167,6 +154,10 @@ class WebRtcCallManager @Inject constructor( return callsByCallId.values.toList() } + fun checkForPSTNSupportIfNeeded() { + pstnProtocolChecker?.checkForPSTNSupportIfNeeded() + } + /** * @return a set of all advertised call during the lifetime of the app. */ @@ -176,7 +167,6 @@ class WebRtcCallManager @Inject constructor( Timber.v("## VOIP headSetButtonTapped") val call = getCurrentCall() ?: return if (call.mxCall.state is CallState.LocalRinging) { - // accept call call.acceptIncomingCall() } if (call.mxCall.state is CallState.Connected) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 42278cd948..fe55d81cc4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.util.awaitCallback import java.io.OutputStream import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, @@ -421,7 +422,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( _viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode)) } else -> { - promise.resumeWith(Result.failure(UnsupportedOperationException())) + promise.resumeWithException(UnsupportedOperationException()) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 44e02fea0b..62bdc61b63 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -52,6 +52,7 @@ import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class HomeActivityViewModel @AssistedInject constructor( @Assisted initialState: HomeActivityViewState, @@ -228,7 +229,7 @@ class HomeActivityViewModel @AssistedInject constructor( ) ) } else { - promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing"))) + promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) } } }, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 331be9d825..c7a5873a65 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -26,8 +26,8 @@ import com.airbnb.mvrx.ViewModelContext import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.PublishRelay import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel @@ -64,6 +64,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.LocalEcho @@ -120,7 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val directRoomHelper: DirectRoomHelper, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), - Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener { + Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener { private val room = session.getRoom(initialState.roomId)!! private val eventId = initialState.eventId @@ -176,6 +177,7 @@ class RoomDetailViewModel @AssistedInject constructor( // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) callManager.addPstnSupportListener(this) + callManager.checkForPSTNSupportIfNeeded() chatEffectManager.delegate = this } @@ -231,65 +233,65 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.ResendAll -> handleResendAll() - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.AcceptCall -> handleAcceptCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) - is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) - is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) - RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() - RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() - is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) - RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) - is RoomDetailAction.ShowRoomAvatarFullScreen -> { + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action) + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.AcceptCall -> handleAcceptCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) + RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople() + RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar() + is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action) + RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings) + is RoomDetailAction.ShowRoomAvatarFullScreen -> { _viewEvents.post( RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView) ) } - is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) + is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) }.exhaustive } @@ -618,10 +620,10 @@ class RoomDetailViewModel @AssistedInject constructor( return@withState false } when (itemId) { - R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.timeline_setting -> true - R.id.invite -> state.canInvite - R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true + R.id.invite -> state.canInvite + R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.open_matrix_apps -> true R.id.voice_call, R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() @@ -741,7 +743,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } - is ParsedCommand.SendChatEffect -> { + is ParsedCommand.SendChatEffect -> { sendChatEffect(slashCommandResult) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() @@ -774,7 +776,7 @@ class RoomDetailViewModel @AssistedInject constructor( } }.exhaustive } - is SendMode.EDIT -> { + is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.getRelationContent()?.inReplyTo?.eventId if (inReplyTo != null) { @@ -799,7 +801,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel() @@ -822,7 +824,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _viewEvents.post(RoomDetailViewEvents.MessageSent) @@ -1441,7 +1443,7 @@ class RoomDetailViewModel @AssistedInject constructor( } override fun onPSTNSupportUpdated() { - updateShowDialerOptionState() + updateShowDialerOptionState() } private fun updateShowDialerOptionState() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt index af56e2eb02..fbf9ebe32f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail import androidx.recyclerview.widget.LinearLayoutManager import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem +import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList @@ -47,8 +47,8 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, if (layoutManager.findFirstVisibleItemPosition() != position) { return } - val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return - val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() + val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return + val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) if (indexOfFirstNewItem != -1) { Timber.v("Should scroll to position: $position") diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 29871cf307..9acd34c827 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -38,18 +38,15 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder -import im.vector.app.features.home.room.detail.timeline.helper.ReadMarkerVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem -import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData -import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer @@ -194,75 +191,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } + private val interceptorHelper = TimelineControllerInterceptorHelper( + ::positionOfReadMarker, + adapterPositionMapping, + vectorPreferences, + callManager + ) + init { addInterceptor(this) requestModelBuild() } - // Update position when we are building new items override fun intercept(models: MutableList>) = synchronized(modelCache) { - positionOfReadMarker = null - adapterPositionMapping.clear() - val callIds = mutableSetOf() - val modelsIterator = models.listIterator() - val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() - modelsIterator.withIndex().forEach { - val index = it.index - val epoxyModel = it.value - if (epoxyModel is CallTileTimelineItem) { - val callId = epoxyModel.attributes.callId - // We should remove the call tile if we already have one for this call or - // if this is an active call tile without an actual call (which can happen with permalink) - val shouldRemoveCallItem = callIds.contains(callId) - || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) - if (shouldRemoveCallItem && !showHiddenEvents) { - modelsIterator.remove() - return@forEach - } - callIds.add(callId) - } - if (epoxyModel is BaseEventItem) { - epoxyModel.getEventIds().forEach { eventId -> - adapterPositionMapping[eventId] = index - } - } - } - val currentUnreadState = this.unreadState - if (currentUnreadState is UnreadState.HasUnread) { - val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1) - positionOfReadMarker = position - if (position != null) { - val readMarker = TimelineReadMarkerItem_() - .also { - it.id("read_marker") - it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) - } - models.add(position, readMarker) - } - } - val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false - if (shouldAddBackwardPrefetch) { - val indexOfPrefetchBackward = (previousModelsSize - 1) - .coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD) - .coerceAtLeast(0) - - val loadingItem = LoadingItem_() - .id("prefetch_backward_loading${System.currentTimeMillis()}") - .showLoader(false) - .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS) - - models.add(indexOfPrefetchBackward, loadingItem) - } - val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false - if (shouldAddForwardPrefetch) { - val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1) - val loadingItem = LoadingItem_() - .id("prefetch_forward_loading${System.currentTimeMillis()}") - .showLoader(false) - .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) - models.add(indexOfPrefetchForward, loadingItem) - } - previousModelsSize = models.size + interceptorHelper.intercept(models, unreadState, timeline, callback) } fun update(viewState: RoomDetailViewState) { @@ -431,6 +373,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } } + private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { + return onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onLoadMore(direction) + } + } + } + private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) { if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) { return @@ -461,14 +411,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return shouldAdd } - private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { - return onVisibilityStateChanged { _, _, visibilityState -> - if (visibilityState == VisibilityState.VISIBLE) { - callback?.onLoadMore(direction) - } - } - } - fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) { return adapterPositionMapping[eventId] } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 982ceb906c..7fd50147d4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -16,7 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.core.epoxy.EmptyItem_ +import im.vector.app.core.epoxy.TimelineEmptyItem +import im.vector.app.core.epoxy.TimelineEmptyItem_ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -114,6 +115,12 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me Timber.e(throwable, "failed to create message item") defaultItemFactory.create(event, highlight, callback, throwable) } - return (computedModel ?: EmptyItem_()) + return computedModel ?: buildEmptyItem(event) + } + + private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem { + return TimelineEmptyItem_() + .id(timelineEvent.localId) + .eventId(timelineEvent.eventId) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt new file mode 100644 index 0000000000..971a3a35d8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2021 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.home.room.detail.timeline.helper + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.VisibilityState +import im.vector.app.core.epoxy.LoadingItem_ +import im.vector.app.core.epoxy.TimelineEmptyItem_ +import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.home.room.detail.UnreadState +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem +import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents +import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import kotlin.reflect.KMutableProperty0 + +private const val DEFAULT_PREFETCH_THRESHOLD = 30 + +class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0, + private val adapterPositionMapping: MutableMap, + private val vectorPreferences: VectorPreferences, + private val callManager: WebRtcCallManager +) { + + private var previousModelsSize = 0 + + // Update position when we are building new items + fun intercept( + models: MutableList>, + unreadState: UnreadState, + timeline: Timeline?, + callback: TimelineEventController.Callback? + ) { + positionOfReadMarker.set(null) + adapterPositionMapping.clear() + val callIds = mutableSetOf() + + // Add some prefetch loader if needed + models.addBackwardPrefetchIfNeeded(timeline, callback) + models.addForwardPrefetchIfNeeded(timeline, callback) + + val modelsIterator = models.listIterator() + val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() + var index = 0 + val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId + // Then iterate on models so we have the exact positions in the adapter + modelsIterator.forEach { epoxyModel -> + if (epoxyModel is ItemWithEvents) { + epoxyModel.getEventIds().forEach { eventId -> + adapterPositionMapping[eventId] = index + if (eventId == firstUnreadEventId) { + modelsIterator.addReadMarkerItem(callback) + index++ + positionOfReadMarker.set(index) + } + } + } + if (epoxyModel is CallTileTimelineItem) { + modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) + } + index++ + } + previousModelsSize = models.size + } + + private fun MutableListIterator>.addReadMarkerItem(callback: TimelineEventController.Callback?) { + val readMarker = TimelineReadMarkerItem_() + .also { + it.id("read_marker") + it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback)) + } + add(readMarker) + // Use next as we still have some process to do before the next iterator loop + next() + } + + private fun MutableListIterator>.removeCallItemIfNeeded( + epoxyModel: CallTileTimelineItem, + callIds: MutableSet, + showHiddenEvents: Boolean + ) { + val callId = epoxyModel.attributes.callId + // We should remove the call tile if we already have one for this call or + // if this is an active call tile without an actual call (which can happen with permalink) + val shouldRemoveCallItem = callIds.contains(callId) + || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) + if (shouldRemoveCallItem && !showHiddenEvents) { + remove() + val emptyItem = TimelineEmptyItem_() + .id(epoxyModel.id()) + .eventId(epoxyModel.attributes.informationData.eventId) + add(emptyItem) + } + callIds.add(callId) + } + + private fun MutableList>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { + val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false + if (shouldAddBackwardPrefetch) { + val indexOfPrefetchBackward = (previousModelsSize - 1) + .coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD) + .coerceAtLeast(0) + + val loadingItem = LoadingItem_() + .id("prefetch_backward_loading${System.currentTimeMillis()}") + .showLoader(false) + .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback) + + add(indexOfPrefetchBackward, loadingItem) + } + } + + private fun MutableList>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { + val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false + if (shouldAddForwardPrefetch) { + val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1) + val loadingItem = LoadingItem_() + .id("prefetch_forward_loading${System.currentTimeMillis()}") + .showLoader(false) + .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback) + add(indexOfPrefetchForward, loadingItem) + } + } + + private fun LoadingItem_.setVisibilityStateChangedListener( + direction: Timeline.Direction, + callback: TimelineEventController.Callback? + ): LoadingItem_ { + return onVisibilityStateChanged { _, _, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onLoadMore(direction) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index e617489902..13bb6db6ef 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter /** * Children must override getViewType() */ -abstract class BaseEventItem : VectorEpoxyModel() { +abstract class BaseEventItem : VectorEpoxyModel(), ItemWithEvents { // To use for instance when opening a permalink with an eventId @EpoxyAttribute @@ -53,12 +53,6 @@ abstract class BaseEventItem : VectorEpoxyModel holder.checkableBackground.isChecked = highlighted } - /** - * Returns the eventIds associated with the EventItem. - * Will generally get only one, but it handles the merging items. - */ - abstract fun getEventIds(): List - abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt similarity index 65% rename from vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt index aaf870667b..cf4211bb2c 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/EmptyItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 New Vector Ltd + * Copyright (c) 2021 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. @@ -14,12 +14,12 @@ * limitations under the License. */ -package im.vector.app.core.epoxy +package im.vector.app.features.home.room.detail.timeline.item -import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R - -@EpoxyModelClass(layout = R.layout.item_empty) -abstract class EmptyItem : VectorEpoxyModel() { - class Holder : VectorEpoxyHolder() +interface ItemWithEvents { + /** + * Returns the eventIds associated with the EventItem. + * Will generally get only one, but it handles the merged items. + */ + fun getEventIds(): List } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 49cb75c9d6..80af64d9f3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException data class DeactivateAccountViewState( val passwordShown: Boolean = false @@ -64,7 +65,7 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { - uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + uiaContinuation?.resumeWithException(IllegalArgumentException()) } } is DeactivateAccountAction.PasswordAuthDone -> { @@ -79,7 +80,7 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v } DeactivateAccountAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") - uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation?.resumeWithException(Exception()) uiaContinuation = null pendingAuth = null } @@ -98,13 +99,15 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v viewModelScope.launch { val event = try { session.deactivateAccount( + action.eraseAllData, object : UserInteractiveAuthInterceptor { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { _viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode)) pendingAuth = DefaultBaseAuth(session = flowResponse.session) uiaContinuation = promise } - }, action.eraseAllData) + } + ) DeactivateAccountViewEvents.Done } catch (failure: Exception) { if (failure.isInvalidUIAAuth()) { diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index 566dea2cd4..8bdf97b6ec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -49,6 +49,7 @@ import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class CrossSigningSettingsViewModel @AssistedInject constructor( @Assisted private val initialState: CrossSigningSettingsViewState, @@ -130,7 +131,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { - uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + uiaContinuation?.resumeWithException(IllegalArgumentException()) } } is CrossSigningSettingsAction.PasswordAuthDone -> { @@ -146,7 +147,7 @@ class CrossSigningSettingsViewModel @AssistedInject constructor( CrossSigningSettingsAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") _viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView) - uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation?.resumeWithException(Exception()) uiaContinuation = null pendingAuth = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index a56a7b8d48..c48b08e806 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -64,6 +64,7 @@ import java.util.concurrent.TimeUnit import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException data class DevicesViewState( val myDeviceId: String = "", @@ -217,7 +218,7 @@ class DevicesViewModel @AssistedInject constructor( if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { - uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + uiaContinuation?.resumeWithException(IllegalArgumentException()) } Unit } @@ -235,7 +236,7 @@ class DevicesViewModel @AssistedInject constructor( DevicesAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") // _viewEvents.post(DevicesViewEvents.Loading) - uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation?.resumeWithException(Exception()) uiaContinuation = null pendingAuth = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index a1d4d6227b..89d632b813 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -46,6 +46,7 @@ import org.matrix.android.sdk.rx.rx import timber.log.Timber import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class ThreePidsSettingsViewModel @AssistedInject constructor( @Assisted initialState: ThreePidsSettingsViewState, @@ -140,7 +141,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( if (pendingAuth != null) { uiaContinuation?.resume(pendingAuth!!) } else { - uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException()))) + uiaContinuation?.resumeWithException(IllegalArgumentException()) } } is ThreePidsSettingsAction.PasswordAuthDone -> { @@ -155,7 +156,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor( } ThreePidsSettingsAction.ReAuthCancelled -> { Timber.d("## UIA - Reauth cancelled") - uiaContinuation?.resumeWith(Result.failure((Exception()))) + uiaContinuation?.resumeWithException(Exception()) uiaContinuation = null pendingAuth = null } diff --git a/vector/src/main/res/layout/item_empty.xml b/vector/src/main/res/layout/item_timeline_empty.xml similarity index 100% rename from vector/src/main/res/layout/item_empty.xml rename to vector/src/main/res/layout/item_timeline_empty.xml diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 41b8080fc0..0b9852634d 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -1,55 +1,52 @@ - + + + + - - - - + + - - + + + + + + + + + - - - - - - - - - + + - - + + + + - - - - + - + + - - + + - - + + + - - - + + + + + + - - - - - - - - - + @@ -70,16 +67,16 @@ - + - - - + + + diff --git a/vector/src/main/res/values/colors_riotx.xml b/vector/src/main/res/values/colors.xml similarity index 72% rename from vector/src/main/res/values/colors_riotx.xml rename to vector/src/main/res/values/colors.xml index 35d0c797b5..e4cfa18461 100644 --- a/vector/src/main/res/values/colors_riotx.xml +++ b/vector/src/main/res/values/colors.xml @@ -1,7 +1,115 @@ - + + #70BF56 + #ff4b55 + #ff4b55 + #2f9edb + + + #ff4b55 + #FFC7C7C7 + #FF999999 + + + + + #FFFFFFFF + + #FF181B21 + + #F000 + + + #FF1A2027 + + #03b381 + + #03b381 + + + #FF0D0E10 + + #FF15171B + + #03b381 + + + #000 + + #FF060708 + + + + #EEEFEF + + #FF61708B + + #FF22262E + + + @color/riotx_android_secondary_light + @color/riotx_android_secondary_dark + + + @color/riotx_background_light + @color/riotx_background_dark + + + #FFFFFF + #FFFFFF + + #903C3C3C + #CCDDDDDD + + + + + + + #FF2E2F32 + #FF9E9E9E + + #FF9E9E9E + @color/riot_primary_text_color_light + + + #FFEDF3FF + #FFA1B2D1 + + #FFA1B2D1 + @color/riot_primary_text_color_dark + + + #2f9edb + @color/vector_fuchsia_color + + + #FFF56679 + #FFFFC666 + #FFF8E71C + #FF7AC9A1 + #FF9E9E9E + + + #FFFFFFFF + #FFFFFFFF + #FF4B55 + #FF4B55 + #FF368BD6 + #61708B + + + + #368BD6 + #368BD6 + + + #368BD6 + #ff812d + + #FF0DBD8B @@ -38,7 +146,7 @@ #5c56f5 #74d12c - + #FF000000 #FFFFFFFF #55000000 @@ -256,6 +364,4 @@ #FFF8E3 #22262E - - - \ No newline at end of file + diff --git a/vector/src/main/res/values/colors_riot.xml b/vector/src/main/res/values/colors_riot.xml deleted file mode 100644 index 2f9d521351..0000000000 --- a/vector/src/main/res/values/colors_riot.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - - - #70BF56 - #ff4b55 - #ff4b55 - #2f9edb - - - #ff4b55 - #FFC7C7C7 - #FF999999 - - - - - #FFFFFFFF - - #FF181B21 - - #F000 - - - #FF1A2027 - - #03b381 - - #03b381 - - - #FF0D0E10 - - #FF15171B - - #03b381 - - - #000 - - #FF060708 - - - - #EEEFEF - - #FF61708B - - #FF22262E - - - @color/riotx_android_secondary_light - @color/riotx_android_secondary_dark - - - @color/riotx_background_light - @color/riotx_background_dark - - - #FFFFFF - #FFFFFF - - #903C3C3C - #CCDDDDDD - - - - - - - #FF2E2F32 - #FF9E9E9E - - #FF9E9E9E - @color/riot_primary_text_color_light - - - #FFEDF3FF - #FFA1B2D1 - - #FFA1B2D1 - @color/riot_primary_text_color_dark - - - #2f9edb - @color/vector_fuchsia_color - - - #FFF56679 - #FFFFC666 - #FFF8E71C - #FF7AC9A1 - #FF9E9E9E - - - #FFFFFFFF - #FFFFFFFF - #FF4B55 - #FF4B55 - #FF368BD6 - #61708B - - - - #368BD6 - #368BD6 - - - #368BD6 - #ff812d - -