diff --git a/.github/ISSUE_TEMPLATE/matrix-sdk.md b/.github/ISSUE_TEMPLATE/matrix-sdk.md new file mode 100644 index 0000000000..30f705a575 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/matrix-sdk.md @@ -0,0 +1,10 @@ +--- +name: Matrix SDK +about: Report issue or ask for a feature regarding the Android Matrix SDK +title: "[SDK] " +labels: matrix-sdk +assignees: '' + +--- + + diff --git a/CHANGES.md b/CHANGES.md index 638e7f586c..6837fa8313 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,31 @@ +Changes in Element 1.0.8 (2020-09-25) +=================================================== + +Improvements 🙌: + - Add "show password" in import Megolm keys dialog + - Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112) + - Better management of requested permissions (#2048) + - Add a setting to show timestamp for all messages (#2123) + - Use cache for user color + - Allow using an outdated homeserver, at user's risk (#1972) + - Restore small logo on login screens and fix scrolling issue on those screens + - PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985) + +Bugfix 🐛: + - Long message cannot be sent/takes infinite time & blocks other messages (#1397) + - Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes) + - User Verification in DM not working + - Manual import of Megolm keys does back up the imported keys + - Auto scrolling to the latest message when sending (#2094) + - Fix incorrect permission check when creating widgets (#2137) + - Pin code: user has to enter pin code twice (#2005) + +SDK API changes ⚠️: + - Rename `tryThis` to `tryOrNull` + +Other changes: + - Add an advanced action to reset an account data entry + Changes in Element 1.0.7 (2020-09-17) =================================================== diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index e5e71f3944..e9a1775ac3 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto import androidx.test.ext.junit.runners.AndroidJUnit4 import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull 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.toModel @@ -212,7 +212,7 @@ class UnwedgingTest : InstrumentedTest { mTestHelper.waitWithLatch { mTestHelper.retryPeriodicallyWithLatch(it) { // we should get back the key and be able to decrypt - val result = tryThis { + val result = tryOrNull { bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") } Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt index 7154487219..03f9de2894 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt @@ -20,7 +20,7 @@ import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.api.NoOpMatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull 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.toModel @@ -227,7 +227,7 @@ class WithHeldTests : InstrumentedTest { mTestHelper.retryPeriodicallyWithLatch(latch) { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { // try to decrypt and force key request - tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } + tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") } } sessionId = timeLineEvent?.root?.content?.toModel()?.sessionId timeLineEvent != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt index c3686da7dd..13693e5bf1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt @@ -17,13 +17,11 @@ package org.matrix.android.sdk.api.auth.data -// Either a list of supported login types, or an error if the homeserver is outdated sealed class LoginFlowResult { data class Success( val supportedLoginTypes: List, val isLoginAndRegistrationSupported: Boolean, - val homeServerUrl: String + val homeServerUrl: String, + val isOutdatedHomeserver: Boolean ) : LoginFlowResult() - - object OutdatedHomeserver : LoginFlowResult() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt index baae9b70f5..555909aef9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/Try.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.extensions import timber.log.Timber -inline fun tryThis(message: String? = null, operation: () -> A): A? { +inline fun tryOrNull(message: String? = null, operation: () -> A): A? { return try { operation() } catch (any: Throwable) { 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 8caed519b2..e27eaa2921 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 @@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.failure -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.di.MoshiProvider import java.io.IOException @@ -49,7 +49,7 @@ fun Throwable.isInvalidPassword(): Boolean { */ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { return if (this is Failure.OtherServerError && this.httpCode == 401) { - tryThis { + tryOrNull { MoshiProvider.providesMoshi() .adapter(RegistrationFlowResponse::class.java) .fromJson(this.errorBody) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 02c48dab07..3313a3e86f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -273,16 +273,16 @@ internal class DefaultAuthenticationService @Inject constructor( } private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { - return if (versions.isSupportedBySdk()) { - // Get the login flow - val loginFlowResponse = executeRequest(null) { - apiCall = authAPI.getLoginFlows() - } - LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl) - } else { - // Not supported - LoginFlowResult.OutdatedHomeserver + // Get the login flow + val loginFlowResponse = executeRequest(null) { + apiCall = authAPI.getLoginFlows() } + return LoginFlowResult.Success( + loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, + versions.isLoginAndRegistrationSupportedBySdk(), + homeServerUrl, + !versions.isSupportedBySdk() + ) } override fun getRegistrationWizard(): RegistrationWizard { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt index c4f55d14bf..3b5f91a201 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt @@ -18,10 +18,9 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.session.events.model.Event @@ -32,28 +31,29 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus -import timber.log.Timber +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams import javax.inject.Inject internal class CancelGossipRequestWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( - val sessionId: String, + override val sessionId: String, val requestId: String, - val recipients: Map> - ) { + val recipients: Map>, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams { companion object { fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { return Params( sessionId = sessionId, requestId = request.requestId, - recipients = request.recipients + recipients = request.recipients, + lastFailureMessage = null ) } } @@ -64,18 +64,11 @@ internal class CancelGossipRequestWorker(context: Context, @Inject lateinit var eventBus: EventBus @Inject lateinit var credentials: Credentials - override suspend fun doWork(): Result { - val errorOutputData = Data.Builder().putBoolean("failed", true).build() - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success(errorOutputData) - - val sessionComponent = getSessionComponent(params.sessionId) - ?: return Result.success(errorOutputData).also { - // TODO, can this happen? should I update local echo? - Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") - } - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { val localId = LocalEcho.createLocalEchoId() val contentMap = MXUsersDevicesMap() val toDeviceContent = ShareRequestCancellation( @@ -107,13 +100,17 @@ internal class CancelGossipRequestWorker(context: Context, ) cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) return Result.success() - } catch (exception: Throwable) { - return if (exception.shouldBeRetried()) { + } catch (throwable: Throwable) { + return if (throwable.shouldBeRetried()) { Result.retry() } else { cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) - Result.success(errorOutputData) + buildErrorResult(params, throwable.localizedMessage ?: "error") } } } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } 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 f8fb5a35d0..c9d09ead5d 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 @@ -27,10 +27,16 @@ import androidx.lifecycle.LiveData import com.squareup.moshi.Types import com.zhuinden.monarchy.Monarchy import dagger.Lazy +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.crypto.MXCryptoConfig -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.crypto.CryptoService @@ -102,12 +108,6 @@ import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.fetchCopied -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import org.matrix.olm.OlmManager import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -345,13 +345,13 @@ internal class DefaultCryptoService @Inject constructor( // Open the store cryptoStore.open() // this can throw if no network - tryThis { + tryOrNull { uploadDeviceKeys() } oneTimeKeysUploader.maybeUploadOneTimeKeys() // this can throw if no backup - tryThis { + tryOrNull { keysBackupService.checkAndStartKeysBackup() } } @@ -1072,7 +1072,11 @@ internal class DefaultCryptoService @Inject constructor( throw Exception("Error") } - megolmSessionDataImporter.handle(importedSessions, true, progressListener) + megolmSessionDataImporter.handle( + megolmSessionsData = importedSessions, + fromBackup = false, + progressListener = progressListener + ) } }.foldToCallback(callback) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt index db85f2c246..1cbf0dd436 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt @@ -18,10 +18,9 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.session.events.model.Event @@ -34,40 +33,34 @@ import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber import javax.inject.Inject internal class SendGossipRequestWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( - val sessionId: String, + override val sessionId: String, val keyShareRequest: OutgoingRoomKeyRequest? = null, - val secretShareRequest: OutgoingSecretRequest? = null - ) + val secretShareRequest: OutgoingSecretRequest? = null, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams @Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var eventBus: EventBus @Inject lateinit var credentials: Credentials - override suspend fun doWork(): Result { - val errorOutputData = Data.Builder().putBoolean("failed", true).build() - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success(errorOutputData) - - val sessionComponent = getSessionComponent(params.sessionId) - ?: return Result.success(errorOutputData).also { - // TODO, can this happen? should I update local echo? - Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") - } - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { val localId = LocalEcho.createLocalEchoId() val contentMap = MXUsersDevicesMap() val eventType: String @@ -121,7 +114,7 @@ internal class SendGossipRequestWorker(context: Context, } } else -> { - return Result.success(errorOutputData).also { + return buildErrorResult(params, "Unknown empty gossiping request").also { Timber.e("Unknown empty gossiping request: $params") } } @@ -137,13 +130,17 @@ internal class SendGossipRequestWorker(context: Context, ) cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) return Result.success() - } catch (exception: Throwable) { - return if (exception.shouldBeRetried()) { + } catch (throwable: Throwable) { + return if (throwable.shouldBeRetried()) { Result.retry() } else { cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) - Result.success(errorOutputData) + buildErrorResult(params, throwable.localizedMessage ?: "error") } } } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt index a3eb476b51..c8bfdcd542 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt @@ -18,10 +18,9 @@ package org.matrix.android.sdk.internal.crypto import android.content.Context -import androidx.work.CoroutineWorker -import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.session.events.model.Event @@ -34,22 +33,23 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker +import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber import javax.inject.Inject internal class SendGossipWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( - val sessionId: String, + override val sessionId: String, val secretValue: String, - val request: IncomingSecretShareRequest - ) + val request: IncomingSecretShareRequest, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams @Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var cryptoStore: IMXCryptoStore @@ -58,18 +58,11 @@ internal class SendGossipWorker(context: Context, @Inject lateinit var messageEncrypter: MessageEncrypter @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction - override suspend fun doWork(): Result { - val errorOutputData = Data.Builder().putBoolean("failed", true).build() - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success(errorOutputData) - - val sessionComponent = getSessionComponent(params.sessionId) - ?: return Result.success(errorOutputData).also { - // TODO, can this happen? should I update local echo? - Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") - } - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { val localId = LocalEcho.createLocalEchoId() val eventType: String = EventType.SEND_SECRET @@ -81,7 +74,7 @@ internal class SendGossipWorker(context: Context, val requestingUserId = params.request.userId ?: "" val requestingDeviceId = params.request.deviceId ?: "" val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) - ?: return Result.success(errorOutputData).also { + ?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also { cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") } @@ -94,7 +87,7 @@ internal class SendGossipWorker(context: Context, if (olmSessionResult?.sessionId == null) { // no session with this device, probably because there // were no one-time keys. - return Result.success(errorOutputData).also { + return buildErrorResult(params, "no session with this device").also { cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) Timber.e("no session with this device, probably because there were no one-time keys.") } @@ -130,13 +123,17 @@ internal class SendGossipWorker(context: Context, ) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED) return Result.success() - } catch (exception: Throwable) { - return if (exception.shouldBeRetried()) { + } catch (throwable: Throwable) { + return if (throwable.shouldBeRetried()) { Result.retry() } else { cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) - Result.success(errorOutputData) + buildErrorResult(params, throwable.localizedMessage ?: "error") } } } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt index d9da459d7d..997a0d7658 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt @@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi * Must be call on the crypto coroutine thread * * @param megolmSessionsData megolm sessions. - * @param backUpKeys true to back up them to the homeserver. + * @param fromBackup true if the imported keys are already backed up on the server. * @param progressListener the progress listener * @return import room keys result */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index a7b9503a84..4f3f06beac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -20,6 +20,10 @@ package org.matrix.android.sdk.internal.crypto.store.db import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.Sort +import io.realm.kotlin.where import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.events.model.Event @@ -85,10 +89,6 @@ import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.session.SessionScope -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.Sort -import io.realm.kotlin.where import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import timber.log.Timber @@ -541,7 +541,7 @@ internal class RealmCryptoStore @Inject constructor( deviceId = it.deviceId ) } - monarchy.writeAsync { realm -> + doRealmTransactionAsync(realmConfiguration) { realm -> realm.where().findAll().deleteAllFromRealm() entities.forEach { realm.insertOrUpdate(it) @@ -1191,7 +1191,7 @@ internal class RealmCryptoStore @Inject constructor( .findAll() .mapNotNull { entity -> when (entity.type) { - GossipRequestType.KEY -> { + GossipRequestType.KEY -> { IncomingRoomKeyRequest( userId = entity.otherUserId, deviceId = entity.otherDeviceId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index fbc1eb6bb1..96125f5d81 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db import com.squareup.moshi.Moshi import com.squareup.moshi.Types -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper @@ -398,7 +398,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) ?.transform { deviceInfoEntity -> - tryThis { + tryOrNull { deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt index c15df27874..d7c1844d53 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.GossipingRequestState import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest @@ -45,7 +45,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String? var type: GossipRequestType get() { - return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY } set(value) { typeStr = value.name @@ -55,7 +55,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String? var requestState: GossipingRequestState get() { - return tryThis { GossipingRequestState.valueOf(requestStateStr) } + return tryOrNull { GossipingRequestState.valueOf(requestStateStr) } ?: GossipingRequestState.NONE } set(value) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt index 2880735d6b..5b32e9077d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Types -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState @@ -47,7 +47,7 @@ internal open class OutgoingGossipingRequestEntity( var type: GossipRequestType get() { - return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY + return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY } set(value) { typeStr = value.name @@ -57,7 +57,7 @@ internal open class OutgoingGossipingRequestEntity( var requestState: OutgoingGossipingRequestState get() { - return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } + return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) } ?: OutgoingGossipingRequestState.UNSENT } set(value) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt index 2b7d26e76b..2200b5c300 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt @@ -17,17 +17,17 @@ package org.matrix.android.sdk.internal.crypto.verification import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.session.crypto.CryptoService -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject @@ -37,56 +37,56 @@ import javax.inject.Inject */ internal class SendVerificationMessageWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val event: Event, + val eventId: String, override val lastFailureMessage: String? = null ) : SessionWorkerParams - @Inject - lateinit var sendVerificationMessageTask: SendVerificationMessageTask + @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask + @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var cryptoService: CryptoService + @Inject lateinit var cancelSendTracker: CancelSendTracker - @Inject - lateinit var cryptoService: CryptoService + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } - override suspend fun doWork(): Result { - val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success(errorOutputData) + override suspend fun doSafeWork(params: Params): Result { + val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found") + val localEventId = localEvent.eventId ?: "" + val roomId = localEvent.roomId ?: "" + + if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) { + return Result.success() + .also { + cancelSendTracker.markCancelled(localEventId, roomId) + Timber.e("## SendEvent: Event sending has been cancelled $localEventId") + } + } - val sessionComponent = getSessionComponent(params.sessionId) - ?: return Result.success(errorOutputData).also { - // TODO, can this happen? should I update local echo? - Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}") - } - sessionComponent.inject(this) - val localId = params.event.eventId ?: "" return try { - val eventId = sendVerificationMessageTask.execute( + val resultEventId = sendVerificationMessageTask.execute( SendVerificationMessageTask.Params( - event = params.event, + event = localEvent, cryptoService = cryptoService ) ) - Result.success(Data.Builder().putString(localId, eventId).build()) - } catch (exception: Throwable) { - if (exception.shouldBeRetried()) { + Result.success(Data.Builder().putString(localEventId, resultEventId).build()) + } catch (throwable: Throwable) { + if (throwable.shouldBeRetried()) { Result.retry() } else { - Result.success(errorOutputData) + buildErrorResult(params, throwable.localizedMessage ?: "error") } } } - companion object { - private const val OUTPUT_KEY_FAILED = "failed" - - fun hasFailed(outputData: Data): Boolean { - return outputData.getBoolean(OUTPUT_KEY_FAILED, false) - } + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt index 69f00ce359..3b2589d995 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -22,6 +22,9 @@ import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.Operation import androidx.work.WorkInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest @@ -51,10 +54,8 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.StringProvider +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID import java.util.concurrent.TimeUnit @@ -87,7 +88,7 @@ internal class VerificationTransportRoomMessage( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( sessionId = sessionId, - event = event + eventId = event.eventId ?: "" )) val enqueueInfo = enqueueSendWork(workerParams) @@ -115,20 +116,30 @@ internal class VerificationTransportRoomMessage( val observer = object : Observer> { override fun onChanged(workInfoList: List?) { workInfoList - ?.filter { it.state == WorkInfo.State.SUCCEEDED } ?.firstOrNull { it.id == enqueueInfo.second } ?.let { wInfo -> - if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { - Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") - tx?.cancel(onErrorReason) - } else { - if (onDone != null) { - onDone() - } else { - tx?.state = nextState + when (wInfo.state) { + WorkInfo.State.FAILED -> { + tx?.cancel(onErrorReason) + workLiveData.removeObserver(this) + } + WorkInfo.State.SUCCEEDED -> { + if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { + Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") + tx?.cancel(onErrorReason) + } else { + if (onDone != null) { + onDone() + } else { + tx?.state = nextState + } + } + workLiveData.removeObserver(this) + } + else -> { + // nop } } - workLiveData.removeObserver(this) } } } @@ -174,7 +185,7 @@ internal class VerificationTransportRoomMessage( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( sessionId = sessionId, - event = event + eventId = event.eventId ?: "" )) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() @@ -184,7 +195,7 @@ internal class VerificationTransportRoomMessage( .build() workManagerProvider.workManager - .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) + .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) .enqueue() // I cannot just listen to the given work request, because when used in a uniqueWork, @@ -199,7 +210,7 @@ internal class VerificationTransportRoomMessage( ?.filter { it.state == WorkInfo.State.SUCCEEDED } ?.firstOrNull { it.id == workRequest.id } ?.let { wInfo -> - if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { + if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) { callback(null, null) } else { val eventId = wInfo.outputData.getString(localId) @@ -229,7 +240,7 @@ internal class VerificationTransportRoomMessage( ) val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( sessionId = sessionId, - event = event + eventId = event.eventId ?: "" )) enqueueSendWork(workerParams) } @@ -249,7 +260,7 @@ internal class VerificationTransportRoomMessage( ) val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( sessionId = sessionId, - event = event + eventId = event.eventId ?: "" )) val enqueueInfo = enqueueSendWork(workerParams) @@ -280,7 +291,7 @@ internal class VerificationTransportRoomMessage( .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .build() return workManagerProvider.workManager - .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) + .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest) .enqueue() to workRequest.id } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt index c633dc5860..eec4fa2f88 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt @@ -16,31 +16,52 @@ */ package org.matrix.android.sdk.internal.database +import com.zhuinden.monarchy.Monarchy import io.realm.Realm import io.realm.RealmConfiguration +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import timber.log.Timber -suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) { - Realm.getInstance(config).use { bgRealm -> - bgRealm.beginTransaction() - val result: T - try { - val start = System.currentTimeMillis() - result = transaction(bgRealm) - if (isActive) { - bgRealm.commitTransaction() - val end = System.currentTimeMillis() - val time = end - start - Timber.v("Execute transaction in $time millis") - } - } finally { - if (bgRealm.isInTransaction) { - bgRealm.cancelTransaction() - } - } - result +internal fun CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) { + asyncTransaction(monarchy.realmConfiguration, transaction) +} + +internal fun CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) { + launch { + awaitTransaction(realmConfiguration, transaction) + } +} + +private val realmSemaphore = Semaphore(1) + +suspend fun awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T { + return realmSemaphore.withPermit { + withContext(Dispatchers.IO) { + Realm.getInstance(config).use { bgRealm -> + bgRealm.beginTransaction() + val result: T + try { + val start = System.currentTimeMillis() + result = transaction(bgRealm) + if (isActive) { + bgRealm.commitTransaction() + val end = System.currentTimeMillis() + val time = end - start + Timber.v("Execute transaction in $time millis") + } + } finally { + if (bgRealm.isInTransaction) { + bgRealm.cancelTransaction() + } + } + result + } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt index ab094e94b8..5002a1b225 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ContentMapper.kt @@ -20,21 +20,34 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType internal object ContentMapper { private val moshi = MoshiProvider.providesMoshi() - private val adapter = moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE) + private val castJsonNumberMoshi by lazy { + // We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row + // and we lost typing information doing so. + // We don't want this check to be done on all adapters, so we create a new moshi just for that. + MoshiProvider.providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + } - fun map(content: String?): Content? { + fun map(content: String?, castJsonNumbers: Boolean = false): Content? { return content?.let { - adapter.fromJson(it) + if (castJsonNumbers) { + castJsonNumberMoshi + } else { + moshi + }.adapter(JSON_DICT_PARAMETERIZED_TYPE).fromJson(it) } } fun map(content: Content?): String? { return content?.let { - adapter.toJson(it) + moshi.adapter(JSON_DICT_PARAMETERIZED_TYPE).toJson(it) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 61f09dcece..d7226a2a0b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -54,7 +54,7 @@ internal object EventMapper { return eventEntity } - fun map(eventEntity: EventEntity): Event { + fun map(eventEntity: EventEntity, castJsonNumbers: Boolean = false): Event { val ud = eventEntity.unsignedData ?.takeIf { it.isNotBlank() } ?.let { @@ -69,8 +69,8 @@ internal object EventMapper { return Event( type = eventEntity.type, eventId = eventEntity.eventId, - content = ContentMapper.map(eventEntity.content), - prevContent = ContentMapper.map(eventEntity.prevContent), + content = ContentMapper.map(eventEntity.content, castJsonNumbers), + prevContent = ContentMapper.map(eventEntity.prevContent, castJsonNumbers), originServerTs = eventEntity.originServerTs, senderId = eventEntity.sender, stateKey = eventEntity.stateKey, @@ -96,8 +96,8 @@ internal object EventMapper { } } -internal fun EventEntity.asDomain(): Event { - return EventMapper.map(this) +internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event { + return EventMapper.map(this, castJsonNumbers) } internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt index 98dec301ee..83f2d13459 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -24,7 +24,7 @@ import okio.BufferedSink import okio.ForwardingSink import okio.Sink import okio.buffer -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import java.io.IOException internal class ProgressRequestBody(private val delegate: RequestBody, @@ -40,7 +40,7 @@ internal class ProgressRequestBody(private val delegate: RequestBody, override fun isDuplex() = delegate.isDuplex() - val length = tryThis { delegate.contentLength() } ?: -1 + val length = tryOrNull { delegate.contentLength() } ?: -1 override fun contentLength() = length diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index d4ceca2006..40ba02ef6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -49,7 +49,7 @@ interface CheckNumberType { val numberAsString = reader.nextString() val decimal = BigDecimal(numberAsString) if (decimal.scale() <= 0) { - decimal.intValueExact() + decimal.longValueExact() } else { decimal.toDouble() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index a163cd4809..ff12030961 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -23,7 +23,7 @@ import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import arrow.core.Try import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.util.Cancelable @@ -174,7 +174,7 @@ internal class DefaultFileService @Inject constructor( } } toNotify?.forEach { otherCallbacks -> - tryThis { otherCallbacks.onFailure(it) } + tryOrNull { otherCallbacks.onFailure(it) } } }, { file -> callback.onSuccess(file) @@ -186,7 +186,7 @@ internal class DefaultFileService @Inject constructor( } Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") toNotify?.forEach { otherCallbacks -> - tryThis { otherCallbacks.onSuccess(file) } + tryOrNull { otherCallbacks.onSuccess(file) } } }) }.toCancelable() 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 d9bc66eddf..a314b5fc3c 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 @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.call import android.os.SystemClock import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallsListener @@ -210,7 +210,7 @@ internal class DefaultCallSignalingService @Inject constructor( private fun onCallHangup(hangup: CallHangupContent) { callListeners.toList().forEach { - tryThis { + tryOrNull { it.onCallHangupReceived(hangup) } } @@ -218,7 +218,7 @@ internal class DefaultCallSignalingService @Inject constructor( private fun onCallAnswer(answer: CallAnswerContent) { callListeners.toList().forEach { - tryThis { + tryOrNull { it.onCallAnswerReceived(answer) } } @@ -226,7 +226,7 @@ internal class DefaultCallSignalingService @Inject constructor( private fun onCallManageByOtherSession(callId: String) { callListeners.toList().forEach { - tryThis { + tryOrNull { it.onCallManagedByOtherSession(callId) } } @@ -237,7 +237,7 @@ internal class DefaultCallSignalingService @Inject constructor( if (incomingCall.otherUserId == userId) return callListeners.toList().forEach { - tryThis { + tryOrNull { it.onCallInviteReceived(incomingCall, invite) } } @@ -245,7 +245,7 @@ internal class DefaultCallSignalingService @Inject constructor( private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { callListeners.toList().forEach { - tryThis { + tryOrNull { it.onCallIceCandidateReceived(incomingCall, candidates) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 4ddf394b00..bb62b02112 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -32,7 +32,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import okio.source import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.ProgressRequestBody @@ -96,7 +96,7 @@ internal class FileUploader @Inject constructor(@Authenticated inputStream.copyTo(it) } return uploadFile(workingFile, filename, mimeType, progressListener).also { - tryThis { workingFile.delete() } + tryOrNull { workingFile.delete() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 6e70906d13..0b517e014f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -19,12 +19,10 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.BitmapFactory -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentAttachmentData -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -34,13 +32,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker +import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import java.io.File import java.util.UUID @@ -56,12 +59,13 @@ private data class NewImageAttributes( * Possible previous worker: None * Possible next worker : Always [MultipleEventSendingDispatcherWorker] */ -internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +internal class UploadContentWorker(val context: Context, params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val events: List, + val localEchoIds: List, val attachment: ContentAttachmentData, val isEncrypted: Boolean, val compressBeforeSending: Boolean, @@ -73,20 +77,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileService: DefaultFileService @Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var imageCompressor: ImageCompressor + @Inject lateinit var localEchoRepository: LocalEchoRepository - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { Timber.v("Starting upload media work with params $params") - - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } - } - // Just defensive code to ensure that we never have an uncaught exception that could break the queue return try { internalDoWork(params) @@ -96,11 +94,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } - private suspend fun internalDoWork(params: Params): Result { - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } - val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) } + private suspend fun internalDoWork(params: Params): Result { + val allCancelled = params.localEchoIds.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) } if (allCancelled) { // there is no point in uploading the image! return Result.success(inputData) @@ -214,18 +213,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } catch (e: Exception) { Timber.e(e, "## FileService: ERROR") - notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } - return Result.success( - WorkerParamsFactory.toData( - params.copy( - lastFailureMessage = e.localizedMessage - ) - ) - ) + return handleFailure(params, e) } finally { // Delete all temporary files filesToDelete.forEach { - tryThis { it.delete() } + tryOrNull { it.delete() } } } } @@ -289,46 +281,48 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) } - private fun handleSuccess(params: Params, - attachmentUrl: String, - encryptedFileInfo: EncryptedFileInfo?, - thumbnailUrl: String?, - thumbnailEncryptedFileInfo: EncryptedFileInfo?, - newImageAttributes: NewImageAttributes?): Result { + private suspend fun handleSuccess(params: Params, + attachmentUrl: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String?, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?): Result { notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } + params.localEchoIds.forEach { + updateEvent(it.eventId, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) + } - val updatedEvents = params.events - .map { - updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) - } - - val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) + val sendParams = MultipleEventSendingDispatcherWorker.Params( + sessionId = params.sessionId, + localEchoIds = params.localEchoIds, + isEncrypted = params.isEncrypted + ) return Result.success(WorkerParamsFactory.toData(sendParams)).also { Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") } } - private fun updateEvent(event: Event, - url: String, - encryptedFileInfo: EncryptedFileInfo?, - thumbnailUrl: String? = null, - thumbnailEncryptedFileInfo: EncryptedFileInfo?, - newImageAttributes: NewImageAttributes?): Event { - val messageContent: MessageContent = event.content.toModel() ?: return event - val updatedContent = when (messageContent) { - is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) - is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) - is MessageFileContent -> messageContent.update(url, encryptedFileInfo) - is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) - else -> messageContent + private suspend fun updateEvent(eventId: String, + url: String, + encryptedFileInfo: EncryptedFileInfo?, + thumbnailUrl: String? = null, + thumbnailEncryptedFileInfo: EncryptedFileInfo?, + newImageAttributes: NewImageAttributes?) { + localEchoRepository.updateEcho(eventId) { _, event -> + val messageContent: MessageContent? = event.asDomain().content.toModel() + val updatedContent = when (messageContent) { + is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo) + is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) + else -> messageContent + } + event.content = ContentMapper.map(updatedContent.toContent()) } - return event.copy(content = updatedContent.toContent()) } private fun notifyTracker(params: Params, function: (String) -> Unit) { - params.events - .mapNotNull { it.eventId } - .forEach { eventId -> function.invoke(eventId) } + params.localEchoIds.forEach { function.invoke(it.eventId) } } private fun MessageImageContent.update(url: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt index c4ba95af84..a1f305fb06 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.download import android.os.Handler import android.os.Looper -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber @@ -76,7 +76,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre Timber.v("## DL Progress Error code:$errorCode") updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) listeners[url]?.forEach { - tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + tryOrNull { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } } } } @@ -84,7 +84,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre private fun updateState(url: String, state: ContentDownloadStateTracker.State) { states[url] = state listeners[url]?.forEach { - tryThis { it.onDownloadStateUpdate(state) } + tryOrNull { it.onDownloadStateUpdate(state) } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt index 7a04f076e9..ac72b8ebfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt @@ -18,20 +18,19 @@ package org.matrix.android.sdk.internal.session.group import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import timber.log.Timber import javax.inject.Inject /** * Possible previous worker: None * Possible next worker : None */ -internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +internal class GetGroupDataWorker(context: Context, params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( @@ -41,13 +40,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : @Inject lateinit var getGroupDataTask: GetGroupDataTask - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - .also { Timber.e("Unable to parse work parameters") } + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) + override suspend fun doSafeWork(params: Params): Result { return runCatching { getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) }.fold( @@ -55,4 +52,8 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : { Result.retry() } ) } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt index d3a10764d3..fc841af2d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt @@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleRegistry import dagger.Lazy import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.SessionParams -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.events.model.toModel @@ -113,7 +113,7 @@ internal class DefaultIdentityService @Inject constructor( // Url has changed, we have to reset our store, update internal configuration and notify listeners identityStore.setUrl(baseUrl) updateIdentityAPI(baseUrl) - listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } } } } @@ -236,7 +236,7 @@ internal class DefaultIdentityService @Inject constructor( private suspend fun updateAccountData(url: String?) { // Also notify the listener withContext(coroutineDispatchers.main) { - listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } + listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } } } updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt index dcc0db8ad1..bf09e52284 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/RefreshUserThreePidsTask.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.task.Task import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject @@ -39,7 +40,7 @@ internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val p Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") // Store the list in DB - monarchy.writeAsync { realm -> + monarchy.awaitTransaction { realm -> realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() accountThreePidsResponse.threePids?.forEach { val entity = UserThreePidEntity( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt index b7f1fb2b93..62bb2ba7dd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddHttpPusherWorker.kt @@ -17,10 +17,10 @@ package org.matrix.android.sdk.internal.session.pushers import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.internal.database.mapper.toEntity @@ -28,16 +28,14 @@ import org.matrix.android.sdk.internal.database.model.PusherEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus -import timber.log.Timber import javax.inject.Inject internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( @@ -50,14 +48,11 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) @Inject @SessionDatabase lateinit var monarchy: Monarchy @Inject lateinit var eventBus: EventBus - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - .also { Timber.e("Unable to parse work parameters") } - - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { val pusher = params.pusher if (pusher.pushKey.isBlank()) { @@ -82,6 +77,10 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) } } + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + private suspend fun setPusher(pusher: JsonPusher) { executeRequest(eventBus) { apiCall = pushersAPI.setPusher(pusher) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 111551d66d..204a7f8aba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -202,13 +202,13 @@ internal class DefaultRelationService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, keepKeys: List?): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, event, keepKeys) + val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys) val sendWorkData = WorkerParamsFactory.toData(params) return timeLineSendEventWorkCommon.createWork(sendWorkData, true) } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt index fc7f712629..cd828f7b30 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.room.relation import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.greenrobot.eventbus.EventBus @@ -27,45 +26,38 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import timber.log.Timber import javax.inject.Inject // TODO This is not used. Delete? -internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +internal class SendRelationWorker(context: Context, params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, val roomId: String, - val event: Event, + val eventId: String, val relationType: String? = null, override val lastFailureMessage: String? = null ) : SessionWorkerParams @Inject lateinit var roomAPI: RoomAPI @Inject lateinit var eventBus: EventBus + @Inject lateinit var localEchoRepository: LocalEchoRepository - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - .also { Timber.e("Unable to parse work parameters") } + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } - } - - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) - - val localEvent = params.event - if (localEvent.eventId == null) { + override suspend fun doSafeWork(params: Params): Result { + val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) + if (localEvent?.eventId == null) { return Result.failure() } val relationContent = localEvent.content.toModel() @@ -88,6 +80,10 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : } } + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { executeRequest(eventBus) { apiCall = roomAPI.sendRelation( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 95cd1c699c..98b793923c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -336,7 +336,7 @@ internal class DefaultSendService @AssistedInject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - return EncryptEventWorker.Params(sessionId, event) + return EncryptEventWorker.Params(sessionId, event.eventId ?: "") .let { WorkerParamsFactory.toData(it) } .let { workManagerProvider.matrixOneTimeWorkRequestBuilder() @@ -360,7 +360,10 @@ internal class DefaultSendService @AssistedInject constructor( attachment: ContentAttachmentData, isRoomEncrypted: Boolean, compressBeforeSending: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) + val localEchoIds = allLocalEchos.map { + LocalEchoIdentifiers(it.roomId!!, it.eventId!!) + } + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, localEchoIds, attachment, isRoomEncrypted, compressBeforeSending) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return workManagerProvider.matrixOneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt index 6b2a2ab115..8781f75ee8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -18,21 +18,23 @@ package org.matrix.android.sdk.internal.session.room.send import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.crypto.CryptoService 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.toContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult +import org.matrix.android.sdk.internal.database.mapper.ContentMapper +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.util.awaitCallback +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject @@ -41,12 +43,12 @@ import javax.inject.Inject * Possible next worker : Always [SendEventWorker] */ internal class EncryptEventWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val event: Event, + val eventId: String, /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ val keepKeys: List? = null, override val lastFailureMessage: String? = null @@ -56,24 +58,15 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var cancelSendTracker: CancelSendTracker - override suspend fun doWork(): Result { - Timber.v("Start Encrypt work") - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } - Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}") - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } - } + override suspend fun doSafeWork(params: Params): Result { + Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}") - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) - - val localEvent = params.event - if (localEvent.eventId == null) { + val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) + if (localEvent?.eventId == null) { return Result.success() } @@ -106,15 +99,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) modifiedContent[toKeep] = it } } - val safeResult = result.copy(eventContent = modifiedContent) - val encryptedEvent = localEvent.copy( - type = safeResult.eventType, - content = safeResult.eventContent - ) // Better handling of local echo, to avoid decrypting transition on remote echo // Should I only do it for text messages? - if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { - val decryptionLocalEcho = MXEventDecryptionResult( + val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { + MXEventDecryptionResult( clearEvent = Event( type = localEvent.type, content = localEvent.content, @@ -124,10 +112,18 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) senderCurve25519Key = result.eventContent["sender_key"] as? String, claimedEd25519Key = crypto.getMyDevice().fingerprint() ) - localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) + } else { + null + } + localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho -> + localEcho.type = EventType.ENCRYPTED + localEcho.content = ContentMapper.map(modifiedContent) + decryptionLocalEcho?.also { + localEcho.setDecryptionResult(it) + } } - val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -138,10 +134,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) // always return success, or the chain will be stuck for ever! val nextWorkerParams = SendEventWorker.Params( sessionId = params.sessionId, - event = localEvent, + eventId = localEvent.eventId, lastFailureMessage = error?.localizedMessage ?: "Error" ) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoIdentifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoIdentifiers.kt new file mode 100644 index 0000000000..391fa2a3bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoIdentifiers.kt @@ -0,0 +1,25 @@ +/* + * 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 org.matrix.android.sdk.internal.session.room.send + +import com.squareup.moshi.JsonClass + +/** + * This is used as a holder to pass necessary data to some workers params. + */ +@JsonClass(generateAdapter = true) +internal data class LocalEchoIdentifiers(val roomId: String, val eventId: String) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 00c624a20d..50f5e13980 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.session.room.send import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import org.greenrobot.eventbus.EventBus -import org.matrix.android.sdk.api.session.events.model.Content 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.toModel @@ -27,11 +27,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.asyncTransaction import org.matrix.android.sdk.internal.database.helper.nextId -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -44,11 +44,13 @@ import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline +import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val taskExecutor: TaskExecutor, private val realmSessionProvider: RealmSessionProvider, private val roomSummaryUpdater: RoomSummaryUpdater, private val eventBus: EventBus, @@ -76,12 +78,12 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } val timelineEvent = timelineEventMapper.map(timelineEventEntity) eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) - monarchy.writeAsync { realm -> + taskExecutor.executorScope.asyncTransaction(monarchy) { realm -> val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { this.insertType = EventInsertType.LOCAL_ECHO } realm.insert(eventInsertEntity) - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync + val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@asyncTransaction roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomSummaryUpdater.updateSendingInformation(realm, roomId) } @@ -89,30 +91,41 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private fun updateSendState(eventId: String, sendState: SendState) { Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") - monarchy.writeAsync { realm -> + updateEchoAsync(eventId) { realm, sendingEventEntity -> + if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { + // If already synced, do not put as sent + } else { + sendingEventEntity.sendState = sendState + } + roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) + } + } + + suspend fun updateEcho(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) { + monarchy.awaitTransaction { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { - if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { - // If already synced, do not put as sent - } else { - sendingEventEntity.sendState = sendState - } - roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) + block(realm, sendingEventEntity) } } } - fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { - monarchy.writeAsync { realm -> + fun updateEchoAsync(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) { + taskExecutor.executorScope.asyncTransaction(monarchy) { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { - sendingEventEntity.type = EventType.ENCRYPTED - sendingEventEntity.content = ContentMapper.map(encryptedContent) - sendingEventEntity.setDecryptionResult(mxEventDecryptionResult) + block(realm, sendingEventEntity) } } } + suspend fun getUpToDateEcho(eventId: String): Event? { + // We are using awaitTransaction here to make sure this executes after other transactions + return monarchy.awaitTransaction { realm -> + EventEntity.where(realm, eventId).findFirst()?.asDomain(castJsonNumbers = true) + } + } + suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { deleteFailedEcho(roomId, localEcho.eventId) } @@ -150,7 +163,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES) } - fun getAllEventsWithStates(roomId: String, states : List): List { + fun getAllEventsWithStates(roomId: String, states: List): List { return realmSessionProvider.withRealm { realm -> TimelineEventEntity .findAllInRoomWithSendStates(realm, roomId, states) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 8e8d24c227..a5b6e3bfb0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -19,18 +19,17 @@ package org.matrix.android.sdk.internal.session.room.send import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.CoroutineWorker import androidx.work.OneTimeWorkRequest import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.di.WorkManagerProvider +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.startChain import timber.log.Timber import java.util.concurrent.TimeUnit @@ -43,12 +42,12 @@ import javax.inject.Inject * Possible next worker : None, but it will post new work to send events, encrypted or not */ internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - val events: List, + val localEchoIds: List, val isEncrypted: Boolean, override val lastFailureMessage: String? = null ) : SessionWorkerParams @@ -57,46 +56,48 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon @Inject lateinit var localEchoRepository: LocalEchoRepository - override suspend fun doWork(): Result { - Timber.v("## SendEvent: Start dispatch sending multiple event work") - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } - - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) - - if (params.lastFailureMessage != null) { - params.events.forEach { event -> - event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) } - } - // Transmit the error if needed? - return Result.success(inputData) - .also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") } + override fun doOnError(params: Params): Result { + params.localEchoIds.forEach { localEchoIds -> + localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED) } + return super.doOnError(params) + } + + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + Timber.v("## SendEvent: Start dispatch sending multiple event work") // Create a work for every event - params.events.forEach { event -> + params.localEchoIds.forEach { localEchoIds -> + val roomId = localEchoIds.roomId + val eventId = localEchoIds.eventId if (params.isEncrypted) { - localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING) - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") - val encryptWork = createEncryptEventWork(params.sessionId, event, true) + localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId") + val encryptWork = createEncryptEventWork(params.sessionId, eventId, true) // Note that event will be replaced by the result of the previous work - val sendWork = createSendEventWork(params.sessionId, event, false) - timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) + val sendWork = createSendEventWork(params.sessionId, eventId, false) + timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork) } else { - localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING) - Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") - val sendWork = createSendEventWork(params.sessionId, event, true) - timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) + localEchoRepository.updateSendState(eventId, SendState.SENDING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId") + val sendWork = createSendEventWork(params.sessionId, eventId, true) + timelineSendEventWorkCommon.postWork(roomId, sendWork) } } return Result.success() } - private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { - val params = EncryptEventWorker.Params(sessionId, event) + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + + private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest { + val params = EncryptEventWorker.Params(sessionId, eventId) val sendWorkData = WorkerParamsFactory.toData(params) return workManagerProvider.matrixOneTimeWorkRequestBuilder() @@ -107,8 +108,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo .build() } - private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) + private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt index e1e780c35a..d3b4c1f97d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RedactEventWorker.kt @@ -17,24 +17,24 @@ package org.matrix.android.sdk.internal.session.room.send import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus -import timber.log.Timber import javax.inject.Inject /** * Possible previous worker: None * Possible next worker : None */ -internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { +internal class RedactEventWorker(context: Context, params: WorkerParameters) + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( @@ -49,20 +49,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C @Inject lateinit var roomAPI: RoomAPI @Inject lateinit var eventBus: EventBus - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - .also { Timber.e("Unable to parse work parameters") } - - if (params.lastFailureMessage != null) { - // Transmit the error - return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } - } - - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + override suspend fun doSafeWork(params: Params): Result { val eventId = params.eventId return runCatching { executeRequest(eventBus) { @@ -91,4 +82,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C } ) } + + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt index 6085459a08..4ac8d87550 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -56,7 +56,7 @@ internal class RoomEventSender @Inject constructor( private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter - val params = EncryptEventWorker.Params(sessionId, event) + val params = EncryptEventWorker.Params(sessionId, event.eventId!!) val sendWorkData = WorkerParamsFactory.toData(params) return workManagerProvider.matrixOneTimeWorkRequestBuilder() @@ -68,7 +68,7 @@ internal class RoomEventSender @Inject constructor( } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 16acde7d16..96b33ce74f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -18,19 +18,19 @@ package org.matrix.android.sdk.internal.session.room.send import android.content.Context -import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import io.realm.RealmConfiguration import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.session.events.model.Content -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams -import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import javax.inject.Inject @@ -42,35 +42,29 @@ import javax.inject.Inject */ internal class SendEventWorker(context: Context, params: WorkerParameters) - : CoroutineWorker(context, params) { + : SessionSafeCoroutineWorker(context, params, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, override val lastFailureMessage: String? = null, - val event: Event? = null, - // Keep for compat at the moment, will be removed later - val eventId: String? = null + val eventId: String ) : SessionWorkerParams @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var roomAPI: RoomAPI @Inject lateinit var eventBus: EventBus @Inject lateinit var cancelSendTracker: CancelSendTracker + @SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration - override suspend fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("## SendEvent: Unable to parse work parameters") } - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } - val event = params.event + override suspend fun doSafeWork(params: Params): Result { + val event = localEchoRepository.getUpToDateEcho(params.eventId) if (event?.eventId == null || event.roomId == null) { - // Old way of sending - if (params.eventId != null) { - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) - } + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) return Result.success() .also { Timber.e("Work cancelled due to bad input data") } } @@ -106,6 +100,10 @@ internal class SendEventWorker(context: Context, } } + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) { localEchoRepository.updateSendState(eventId, SendState.SENDING) executeRequest(eventBus) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 2dead1d9cc..3873a43cf5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -29,6 +29,7 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toModel @@ -333,12 +334,22 @@ internal class DefaultTimeline( // Private methods ***************************************************************************** - private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { - return builtEventsIdMap[eventId]?.let { builtIndex -> - // Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = builder(te) - true + private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { + return tryOrNull { + builtEventsIdMap[eventId]?.let { builtIndex -> + // Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + val rebuiltEvent = builder(te) + // If rebuilt event is filtered its returned as null and should be removed. + if (rebuiltEvent == null) { + builtEventsIdMap.remove(eventId) + builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) } + builtEvents.removeAt(builtIndex) + } else { + builtEvents[builtIndex] = rebuiltEvent + } + true + } } } ?: false } @@ -413,14 +424,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): State { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (State) -> State) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -489,7 +500,8 @@ internal class DefaultTimeline( val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { - buildTimelineEvent(eventEntity) + val builtEvent = buildTimelineEvent(eventEntity) + listOf(builtEvent).filterEventsWithSettings().firstOrNull() } || postSnapshot } } @@ -730,10 +742,10 @@ internal class DefaultTimeline( return object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> @@ -775,9 +787,8 @@ internal class DefaultTimeline( } if (!filterEdits) return@filter false - val filterRedacted = !settings.filters.filterRedacted || it.root.isRedacted() - - filterRedacted + val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted() + !filterRedacted } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index b58727cbaa..764087a7b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -131,7 +131,7 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private /** * At the moment we don't get any group data through the sync, so we poll where every hour. - You can also force to refetch group data using [Group] API. + * You can also force to refetch group data using [Group] API. */ private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) { val groupIds = ArrayList() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt index 3e0a29ba72..e64e0b7933 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncWorker.kt @@ -18,18 +18,18 @@ package org.matrix.android.sdk.internal.session.sync.job import android.content.Context import androidx.work.BackoffPolicy -import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker +import org.matrix.android.sdk.internal.session.SessionComponent import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.task.TaskExecutor +import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory -import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -43,7 +43,7 @@ private const val DEFAULT_DELAY_TIMEOUT = 30_000L */ internal class SyncWorker(context: Context, workerParameters: WorkerParameters -) : CoroutineWorker(context, workerParameters) { +) : SessionSafeCoroutineWorker(context, workerParameters, Params::class.java) { @JsonClass(generateAdapter = true) internal data class Params( @@ -59,14 +59,13 @@ internal class SyncWorker(context: Context, @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker @Inject lateinit var workManagerProvider: WorkManagerProvider - override suspend fun doWork(): Result { - Timber.i("Sync work starting") - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } + override fun injectWith(injector: SessionComponent) { + injector.inject(this) + } + + override suspend fun doSafeWork(params: Params): Result { + Timber.i("Sync work starting") - val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() - sessionComponent.inject(this) return runCatching { doSync(params.timeout) }.fold( @@ -91,6 +90,10 @@ internal class SyncWorker(context: Context, ) } + override fun buildErrorParams(params: Params, message: String): Params { + return params.copy(lastFailureMessage = params.lastFailureMessage ?: message) + } + private suspend fun doSync(timeout: Long) { val taskParams = SyncTask.Params(timeout * 1000) syncTask.execute(taskParams) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt index cb9059b089..b167b5ca0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt @@ -202,6 +202,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager: stateKey = QueryStringValue.NoCondition ) val powerLevelsContent = powerLevelsEvent?.content?.toModel() ?: return false - return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) + return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt index 81f5af9ac6..91127790ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt @@ -18,9 +18,9 @@ package org.matrix.android.sdk.internal.util import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.internal.database.awaitTransaction import io.realm.Realm import io.realm.RealmModel +import org.matrix.android.sdk.internal.database.awaitTransaction import java.util.concurrent.atomic.AtomicReference internal suspend fun Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionSafeCoroutineWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionSafeCoroutineWorker.kt new file mode 100644 index 0000000000..dd662f5e6e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionSafeCoroutineWorker.kt @@ -0,0 +1,102 @@ +/* + * 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 org.matrix.android.sdk.internal.worker + +import android.content.Context +import androidx.annotation.CallSuper +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.session.SessionComponent +import timber.log.Timber + +/** + * This worker should only sends Result.Success when added to a unique queue to avoid breaking the unique queue. + * This abstract class handle the cases of problem when parsing parameter, and forward the error if any to + * the next workers. + */ +internal abstract class SessionSafeCoroutineWorker( + context: Context, + workerParameters: WorkerParameters, + private val paramClass: Class +) : CoroutineWorker(context, workerParameters) { + + @JsonClass(generateAdapter = true) + internal data class ErrorData( + override val sessionId: String, + override val lastFailureMessage: String? = null + ) : SessionWorkerParams + + final override suspend fun doWork(): Result { + val params = WorkerParamsFactory.fromData(paramClass, inputData) + ?: return buildErrorResult(null, "Unable to parse work parameters") + .also { Timber.e("Unable to parse work parameters") } + + return try { + val sessionComponent = getSessionComponent(params.sessionId) + ?: return buildErrorResult(params, "No session") + + // Make sure to inject before handling error as you may need some dependencies to process them. + injectWith(sessionComponent) + if (params.lastFailureMessage != null) { + // Forward error to the next workers + doOnError(params) + } else { + doSafeWork(params) + } + } catch (throwable: Throwable) { + buildErrorResult(params, throwable.localizedMessage ?: "error") + } + } + + abstract fun injectWith(injector: SessionComponent) + + /** + * Should only return Result.Success for workers added to a unique queue + */ + abstract suspend fun doSafeWork(params: PARAM): Result + + protected fun buildErrorResult(params: PARAM?, message: String): Result { + return Result.success( + if (params != null) { + WorkerParamsFactory.toData(paramClass, buildErrorParams(params, message)) + } else { + WorkerParamsFactory.toData(ErrorData::class.java, ErrorData(sessionId = "", lastFailureMessage = message)) + } + ) + } + + abstract fun buildErrorParams(params: PARAM, message: String): PARAM + + /** + * This is called when the input parameters are correct, but contain an error from the previous worker. + */ + @CallSuper + open fun doOnError(params: PARAM): Result { + // Forward the error + return Result.success(inputData) + .also { Timber.e("Work cancelled due to input error from parent") } + } + + companion object { + fun hasFailed(outputData: Data): Boolean { + return WorkerParamsFactory.fromData(ErrorData::class.java, outputData) + .let { it?.lastFailureMessage != null } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt index b162566403..6921a539f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt @@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.worker import androidx.work.Data +import com.squareup.moshi.Moshi +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.network.parsing.CheckNumberType internal object WorkerParamsFactory { - val moshi by lazy { + private val moshi: Moshi by lazy { // We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row // and we lost typing information doing so. // We don't want this check to be done on all adapters, so we just add it here. @@ -33,20 +35,24 @@ internal object WorkerParamsFactory { .build() } - const val KEY = "WORKER_PARAMS_JSON" + private const val KEY = "WORKER_PARAMS_JSON" - inline fun toData(params: T): Data { - val adapter = moshi.adapter(T::class.java) + inline fun toData(params: T) = toData(T::class.java, params) + + fun toData(clazz: Class, params: T): Data { + val adapter = moshi.adapter(clazz) val json = adapter.toJson(params) return Data.Builder().putString(KEY, json).build() } - inline fun fromData(data: Data): T? { + inline fun fromData(data: Data) = fromData(T::class.java, data) + + fun fromData(clazz: Class, data: Data): T? = tryOrNull("Unable to parse work parameters") { val json = data.getString(KEY) return if (json == null) { null } else { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(clazz) adapter.fromJson(json) } } diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml index 2ba369b93a..52b935c097 100644 --- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -151,21 +151,21 @@ %1$s alterou o nível de permissão de %2$s. %1$s de %2$s para %3$s - Primeira sincronização:↵ + Primeira sincronização: \nImportando a conta… - Primeira sincronização:↵ + Primeira sincronização: \nImportando as chaves de criptografia - Primeira sincronização:↵ + Primeira sincronização: \nImportando as salas - Primeira sincronização:↵ + Primeira sincronização: \nImportando as salas em que você entrou - Primeira sincronização:↵ + Primeira sincronização: \nImportando as salas em que você foi convidado - Primeira sincronização:↵ + Primeira sincronização: \nImportando as salas em que você saiu - Primeira sincronização:↵ + Primeira sincronização: \nImportando as comunidades - Primeira sincronização:↵ + Primeira sincronização: \nImportando os dados da conta Enviando mensagem… diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings_sas.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings_sas.xml new file mode 100644 index 0000000000..ed621e5477 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Cachorro + Gato + Leão + Cavalo + Unicórnio + Porco + Elefante + Coelho + Panda + Galo + Pinguim + Tartaruga + Peixe + Polvo + Borboleta + Flor + Árvore + Cacto + Cogumelo + Globo + Lua + Nuvem + Fogo + Banana + Maçã + Morango + Milho + Pizza + Bolo + Coração + Sorriso + Robô + Chapéu + Óculos + Chave inglesa + Papai-noel + Joinha + Guarda-chuva + Ampulheta + Relógio + Presente + Lâmpada + Livro + Lápis + Clipe de papel + Tesoura + Cadeado + Chave + Martelo + Telefone + Bandeira + Trem + Bicicleta + Avião + Foguete + Troféu + Bola + Guitarra + Trombeta + Sino + Âncora + Fones de ouvido + Pasta + Alfinete + diff --git a/matrix-sdk-android/src/main/res/values-sv/strings.xml b/matrix-sdk-android/src/main/res/values-sv/strings.xml index 491eb0bc49..8f8ba5e306 100644 --- a/matrix-sdk-android/src/main/res/values-sv/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sv/strings.xml @@ -27,8 +27,8 @@ Du bannade %1$s %1$s drog tillbaka inbjudan för %2$s Du drog tillbaka inbjudan för %1$s - %1$s ändrade sin avatar - Du ändrade din avatar + %1$s bytte sin avatar + Du bytte din avatar %1$s satte sitt visningsnamn till %2$s Du satte ditt visningsnamn till %1$s %1$s bytte sitt visningsnamn från %2$s till %3$s diff --git a/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml b/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml index 1e06452ba5..2d6e2f407a 100644 --- a/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml @@ -27,7 +27,7 @@ Banan Äpple Jordgubbe - Majskolv + Majs Pizza Tårta Hjärta @@ -41,7 +41,7 @@ Paraply Timglas Klocka - Paket + Present Lampa Bok Penna @@ -52,7 +52,7 @@ Hammare Telefon Flagga - Ånglok + Tåg Cykel Flygplan Raket diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml index 439615735a..8d50991805 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml @@ -7,4 +7,62 @@ 独角兽 + 大象 + 兔子 + 熊猫 + 公鸡 + 企鹅 + 乌龟 + + 章鱼 + 蝴蝶 + + + 仙人掌 + 蘑菇 + 地球 + 月亮 + + + 香蕉 + 苹果 + 草莓 + 玉米 + 披萨 + 蛋糕 + + 笑脸 + 机器人 + 帽子 + 眼镜 + 扳手 + 圣诞老人 + + + 沙漏 + 时钟 + 礼物 + 灯泡 + + 铅笔 + 回形针 + 剪刀 + + 钥匙 + 锤子 + 电话 + 旗帜 + 火车 + 自行车 + 飞机 + 火箭 + 奖杯 + + 吉他 + 喇叭 + 铃铛 + + 耳机 + 文件夹 + 图钉 diff --git a/vector/build.gradle b/vector/build.gradle index e0f401aaf6..ef0124dcc7 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -17,7 +17,7 @@ androidExtensions { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 7 +ext.versionPatch = 8 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' @@ -352,7 +352,7 @@ dependencies { implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'com.google.android:flexbox:1.1.1' implementation "androidx.autofill:autofill:$autofill_version" - implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta9' + implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta10' // Custom Tab implementation 'androidx.browser:browser:1.2.0' diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index d0e4c938cd..75f61a7b01 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -89,6 +89,7 @@ import im.vector.app.features.settings.VectorSettingsHelpAboutFragment import im.vector.app.features.settings.VectorSettingsLabsFragment import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment import im.vector.app.features.settings.VectorSettingsNotificationsTroubleshootFragment +import im.vector.app.features.settings.VectorSettingsPinFragment import im.vector.app.features.settings.VectorSettingsPreferencesFragment import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.app.features.settings.account.deactivation.DeactivateAccountFragment @@ -284,6 +285,11 @@ interface FragmentModule { @FragmentKey(VectorSettingsLabsFragment::class) fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VectorSettingsPinFragment::class) + fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment + @Binds @IntoMap @FragmentKey(PushRulesFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index c750384485..4ba3d6ba13 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -36,6 +36,7 @@ import im.vector.app.features.crypto.verification.IncomingVerificationRequestHan import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.HomeRoomListDataSource +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.login.ReAuthHelper @@ -71,6 +72,8 @@ interface VectorComponent { fun matrix(): Matrix + fun matrixItemColorProvider(): MatrixItemColorProvider + fun sessionListener(): SessionListener fun currentSession(): Session diff --git a/vector/src/main/java/im/vector/app/core/extensions/FragmentManager.kt b/vector/src/main/java/im/vector/app/core/extensions/FragmentManager.kt index 49b28fb190..7456719455 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/FragmentManager.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/FragmentManager.kt @@ -17,12 +17,12 @@ package im.vector.app.core.extensions import androidx.fragment.app.FragmentTransaction -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) { // Could throw and make the app crash // e.g sharedActionViewModel.observe() - tryThis("Failed to commitTransactionNow") { + tryOrNull("Failed to commitTransactionNow") { beginTransaction().func().commitNow() } } diff --git a/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt b/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt index 5d91370963..2e451cfc1e 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/ThreePid.kt @@ -18,14 +18,14 @@ package im.vector.app.core.extensions import com.google.i18n.phonenumbers.PhoneNumberUtil import org.matrix.android.sdk.api.extensions.ensurePrefix -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.identity.ThreePid fun ThreePid.getFormattedValue(): String { return when (this) { is ThreePid.Email -> email is ThreePid.Msisdn -> { - tryThis(message = "Unable to parse the phone number") { + tryOrNull(message = "Unable to parse the phone number") { PhoneNumberUtil.getInstance().parse(msisdn.ensurePrefix("+"), null) } ?.let { diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt index 4835ce5af5..81f73556a5 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseActivity.kt @@ -84,7 +84,7 @@ import im.vector.app.receivers.DebugReceiver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.GlobalError import timber.log.Timber import kotlin.system.measureTimeMillis @@ -318,11 +318,17 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { if (requestCode == PinActivity.PIN_REQUEST_CODE) { when (resultCode) { Activity.RESULT_OK -> { + Timber.v("Pin ok, unlock app") pinLocker.unlock() + + // Cancel any new started PinActivity, after a screen rotation for instance + finishActivity(PinActivity.PIN_REQUEST_CODE) } else -> { - pinLocker.block() - moveTaskToBack(true) + if (pinLocker.getLiveState().value != PinLocker.State.UNLOCKED) { + // Remove the task, to be sure that PIN code will be requested when resumed + finishAndRemoveTask() + } } } } @@ -362,7 +368,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector { super.onPostResume() synchronized(postResumeScheduledActions) { postResumeScheduledActions.forEach { - tryThis { it.invoke() } + tryOrNull { it.invoke() } } postResumeScheduledActions.clear() } diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericItemWithValue.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericItemWithValue.kt index f9d32a9f1e..0e719a1cd2 100644 --- a/vector/src/main/java/im/vector/app/core/ui/list/GenericItemWithValue.kt +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericItemWithValue.kt @@ -56,6 +56,9 @@ abstract class GenericItemWithValue : VectorEpoxyModel hash = (hash shl 5) - hash + chr.toInt() } - - return when (abs(hash) % 8) { - 1 -> R.color.riotx_username_2 - 2 -> R.color.riotx_username_3 - 3 -> R.color.riotx_username_4 - 4 -> R.color.riotx_username_5 - 5 -> R.color.riotx_username_6 - 6 -> R.color.riotx_username_7 - 7 -> R.color.riotx_username_8 - else -> R.color.riotx_username_1 - } -} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index 1d6ea7339d..b9ccad9455 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -42,7 +42,6 @@ import im.vector.app.core.extensions.getMeasurements import im.vector.app.core.utils.PERMISSIONS_EMPTY import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import kotlin.math.max @@ -215,10 +214,10 @@ class AttachmentTypeSelectorView(context: Context, */ enum class Type(val permissionsBit: Int) { CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), - GALLERY(PERMISSIONS_FOR_WRITING_FILES), - FILE(PERMISSIONS_FOR_WRITING_FILES), + GALLERY(PERMISSIONS_EMPTY), + FILE(PERMISSIONS_EMPTY), STICKER(PERMISSIONS_EMPTY), - AUDIO(PERMISSIONS_FOR_WRITING_FILES), + AUDIO(PERMISSIONS_EMPTY), CONTACT(PERMISSIONS_FOR_PICKING_CONTACT) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index a822c49e9b..387cfb2261 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -42,17 +42,13 @@ import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.OnSnapPositionChangeListener -import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT import im.vector.app.core.utils.SnapOnScrollListener -import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.attachSnapHelperWithListener -import im.vector.app.core.utils.checkPermissions import im.vector.app.features.media.createUCropWithDefaultSettings -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.content.ContentAttachmentData import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_attachments_preview.* +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber import java.io.File import javax.inject.Inject @@ -102,7 +98,7 @@ class AttachmentsPreviewFragment @Inject constructor( handleRemoveAction() true } - R.id.attachmentsPreviewEditAction -> { + R.id.attachmentsPreviewEditAction -> { handleEditAction() true } @@ -183,22 +179,7 @@ class AttachmentsPreviewFragment @Inject constructor( viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment) } - private fun handleEditAction() { - // check permissions - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT)) { - doHandleEditAction() - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT && allGranted(grantResults)) { - doHandleEditAction() - } - } - - private fun doHandleEditAction() = withState(viewModel) { + private fun handleEditAction() = withState(viewModel) { val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") val uri = currentAttachment.queryUri diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt index 3230e97809..6d3f8b5885 100644 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt @@ -27,7 +27,7 @@ import io.reactivex.disposables.Disposable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.ReplaySubject import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull 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.CallsListener @@ -95,7 +95,7 @@ class WebRtcPeerConnectionManager @Inject constructor( val callAudioManager = CallAudioManager(context.applicationContext) { currentCallsListeners.forEach { - tryThis { it.onAudioDevicesChange() } + tryOrNull { it.onAudioDevicesChange() } } } @@ -174,7 +174,7 @@ class WebRtcPeerConnectionManager @Inject constructor( set(value) { field = value currentCallsListeners.forEach { - tryThis { it.onCaptureStateChanged() } + tryOrNull { it.onCaptureStateChanged() } } } @@ -205,7 +205,7 @@ class WebRtcPeerConnectionManager @Inject constructor( set(value) { field = value currentCallsListeners.forEach { - tryThis { it.onCurrentCallChange(value?.mxCall) } + tryOrNull { it.onCurrentCallChange(value?.mxCall) } } } @@ -745,7 +745,7 @@ class WebRtcPeerConnectionManager @Inject constructor( } currentCallsListeners.forEach { - tryThis { it.onCameraChange() } + tryOrNull { it.onCameraChange() } } } @@ -771,7 +771,7 @@ class WebRtcPeerConnectionManager @Inject constructor( // videoCapturer?.stopCapture() videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) currentCaptureMode = format - currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged() } } + currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } } } } diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt index e26cb23ac7..5a4c25a8ad 100644 --- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt @@ -38,7 +38,7 @@ import org.jitsi.meet.sdk.JitsiMeetActivityInterface import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetView import org.jitsi.meet.sdk.JitsiMeetViewListener -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import java.net.URL import javax.inject.Inject @@ -100,7 +100,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface, Ji .setVideoMuted(!viewState.enableVideo) .setUserInfo(viewState.userInfo) .apply { - tryThis { URL(viewState.jitsiUrl) }?.let { + tryOrNull { URL(viewState.jitsiUrl) }?.let { setServerURL(it) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index f8b3f4d6a4..ee47a4d8e9 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -27,7 +27,7 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* import java.util.concurrent.TimeUnit @@ -84,7 +84,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { data?.data?.let { dataURI -> - tryThis { + tryOrNull { activity?.contentResolver?.openInputStream(dataURI) ?.bufferedReader() ?.use { it.readText() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt index e43e2f2493..a89e08988c 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -37,7 +37,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.startImportTextFromFileIntent -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.crypto.keysbackup.util.isValidRecoveryKey import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText @@ -150,7 +150,7 @@ class BootstrapMigrateBackupFragment @Inject constructor( override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { data?.data?.let { dataURI -> - tryThis { + tryOrNull { activity?.contentResolver?.openInputStream(dataURI) ?.bufferedReader() ?.use { it.readText() } diff --git a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt index 94221b8f4e..21f65ec9ef 100644 --- a/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/discovery/DiscoverySettingsFragment.kt @@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.ensureProtocol import im.vector.app.features.discovery.change.SetIdentityServerFragment +import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.terms.ReviewTermsActivity import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.ThreePid @@ -178,10 +179,6 @@ class DiscoverySettingsFragment @Inject constructor( } private fun navigateToChangeIdentityServerFragment() { - parentFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom) - .replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null) - .addToBackStack(null) - .commit() + (vectorBaseActivity as? VectorSettingsActivity)?.navigateTo(SetIdentityServerFragment::class.java) } } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index e15f1e90cb..08f18a00ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -16,13 +16,11 @@ package im.vector.app.features.home -import android.content.Context import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.annotation.AnyThread import androidx.annotation.UiThread -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions @@ -33,8 +31,8 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequests -import im.vector.app.core.utils.getColorFromUserId -import org.matrix.android.sdk.api.extensions.tryThis +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject @@ -43,7 +41,8 @@ import javax.inject.Inject * This helper centralise ways to retrieve avatar into ImageView or even generic Target */ -class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { +class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, + private val matrixItemColorProvider: MatrixItemColorProvider) { companion object { private const val THUMBNAIL_SIZE = 250 @@ -51,21 +50,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @UiThread fun render(matrixItem: MatrixItem, imageView: ImageView) { - render(imageView.context, - GlideApp.with(imageView), + render(GlideApp.with(imageView), matrixItem, DrawableImageViewTarget(imageView)) } fun clear(imageView: ImageView) { // It can be called after recycler view is destroyed, just silently catch - tryThis { GlideApp.with(imageView).clear(imageView) } + tryOrNull { GlideApp.with(imageView).clear(imageView) } } @UiThread fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { - render(imageView.context, - glideRequests, + render(glideRequests, matrixItem, DrawableImageViewTarget(imageView)) } @@ -79,7 +76,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active displayName = mappedContact.displayName ) - val placeholder = getPlaceholderDrawable(imageView.context, matrixItem) + val placeholder = getPlaceholderDrawable(matrixItem) GlideApp.with(imageView) .load(mappedContact.photoURI) .apply(RequestOptions.circleCropTransform()) @@ -88,11 +85,10 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active } @UiThread - fun render(context: Context, - glideRequests: GlideRequests, + fun render(glideRequests: GlideRequests, matrixItem: MatrixItem, target: Target) { - val placeholder = getPlaceholderDrawable(context, matrixItem) + val placeholder = getPlaceholderDrawable(matrixItem) buildGlideRequest(glideRequests, matrixItem.avatarUrl) .placeholder(placeholder) .into(target) @@ -100,7 +96,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active @AnyThread @Throws - fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { + fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { return glideRequests .asBitmap() .apply { @@ -108,7 +104,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active if (resolvedUrl != null) { load(resolvedUrl) } else { - val avatarColor = avatarColor(matrixItem, context) + val avatarColor = matrixItemColorProvider.getColor(matrixItem) load(TextDrawable.builder() .beginConfig() .bold() @@ -130,8 +126,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active } @AnyThread - fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { - val avatarColor = avatarColor(matrixItem, context) + fun getPlaceholderDrawable(matrixItem: MatrixItem): Drawable { + val avatarColor = matrixItemColorProvider.getColor(matrixItem) return TextDrawable.builder() .beginConfig() .bold() @@ -152,11 +148,4 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() ?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) } - - private fun avatarColor(matrixItem: MatrixItem, context: Context): Int { - return when (matrixItem) { - is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id)) - else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id)) - } - } } diff --git a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt index ad0209845f..1a476913f3 100644 --- a/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/ShortcutsHandler.kt @@ -70,7 +70,7 @@ class ShortcutsHandler @Inject constructor( .map { room -> val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) val bitmap = try { - avatarRenderer.shortcutDrawable(context, GlideApp.with(context), room.toMatrixItem(), iconSize) + avatarRenderer.shortcutDrawable(GlideApp.with(context), room.toMatrixItem(), iconSize) } catch (failure: Throwable) { null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 7635c3042d..7c3ac6011e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -97,7 +97,6 @@ import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createUIHandler -import im.vector.app.core.utils.getColorFromUserId import im.vector.app.core.utils.isValidUrl import im.vector.app.core.utils.onPermissionResultAudioIpCall import im.vector.app.core.utils.onPermissionResultVideoIpCall @@ -127,6 +126,7 @@ import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem @@ -217,7 +217,9 @@ class RoomDetailFragment @Inject constructor( private val vectorPreferences: VectorPreferences, private val colorProvider: ColorProvider, private val notificationUtils: NotificationUtils, - private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) : + private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, + private val matrixItemColorProvider: MatrixItemColorProvider +) : VectorBaseFragment(), TimelineEventController.Callback, VectorInviteView.Callback, @@ -610,6 +612,16 @@ class RoomDetailFragment @Inject constructor( it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) } withState(roomDetailViewModel) { state -> + // Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions + val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { + 1 -> false + 2 -> state.isAllowedToStartWebRTCCall + else -> state.isAllowedToManageWidgets + } + setOf(R.id.voice_call, R.id.video_call).forEach { + menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 + } + val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 if (widgetsCount > 0) { @@ -687,6 +699,8 @@ class RoomDetailFragment @Inject constructor( // webRtcPeerConnectionManager.endCall() // safeStartCall(it, isVideoCall) // } + } else if (!state.isAllowedToStartWebRTCCall) { + showDialogWithMessage(getString(R.string.no_permissions_to_start_webrtc_call)) } else { safeStartCall(isVideoCall) } @@ -778,7 +792,7 @@ class RoomDetailFragment @Inject constructor( // switch to expanded bar composerLayout.composerRelatedMessageTitle.apply { text = event.senderInfo.disambiguatedDisplayName - setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) + setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@"))) } val messageContent: MessageContent? = event.getLastMessageContent() 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 9b2239bd4b..9d52933168 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 @@ -57,7 +57,7 @@ import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.NoOpMatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +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 @@ -181,10 +181,12 @@ class RoomDetailViewModel @AssistedInject constructor( .subscribe { val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) + val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) setState { copy( canSendMessage = canSendMessage, - isAllowedToManageWidgets = isAllowedToManageWidgets + isAllowedToManageWidgets = isAllowedToManageWidgets, + isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall ) } } @@ -334,7 +336,7 @@ class RoomDetailViewModel @AssistedInject constructor( val roomId: String = room.roomId val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale) - val preferredJitsiDomain = tryThis { + val preferredJitsiDomain = tryOrNull { rawService.getElementWellknown(session.myUserId) ?.jitsiServer ?.preferredDomain @@ -988,7 +990,7 @@ class RoomDetailViewModel @AssistedInject constructor( && mxcUrl?.startsWith("content://") ?: false val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false if (isLocalSendingFile) { - tryThis { Uri.parse(mxcUrl) }?.let { + tryOrNull { Uri.parse(mxcUrl) }?.let { _viewEvents.post(RoomDetailViewEvents.OpenFile( action.messageFileContent.mimeType, it, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index a031803fa6..16a7379b6a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -67,7 +67,8 @@ data class RoomDetailViewState( val canShowJumpToReadMarker: Boolean = true, val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown, val canSendMessage: Boolean = true, - val isAllowedToManageWidgets: Boolean = false + val isAllowedToManageWidgets: Boolean = false, + val isAllowedToStartWebRTCCall: Boolean = true ) : MvRxState { constructor(args: RoomDetailArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/MessageColorProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/MessageColorProvider.kt index 66e2c745a4..d1bef4c8c7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/MessageColorProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/MessageColorProvider.kt @@ -19,18 +19,20 @@ package im.vector.app.features.home.room.detail.timeline import androidx.annotation.ColorInt import im.vector.app.R import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.getColorFromUserId +import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject class MessageColorProvider @Inject constructor( private val colorProvider: ColorProvider, + private val matrixItemColorProvider: MatrixItemColorProvider, private val vectorPreferences: VectorPreferences) { @ColorInt - fun getMemberNameTextColor(userId: String): Int { - return colorProvider.getColor(getColorFromUserId(userId)) + fun getMemberNameTextColor(matrixItem: MatrixItem): Int { + return matrixItemColorProvider.getColor(matrixItem) } @ColorInt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt new file mode 100644 index 0000000000..6a590206cb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MatrixItemColorProvider.kt @@ -0,0 +1,76 @@ +/* + * 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.home.room.detail.timeline.helper + +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.VisibleForTesting +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.math.abs + +@Singleton +class MatrixItemColorProvider @Inject constructor( + private val colorProvider: ColorProvider +) { + private val cache = mutableMapOf() + + @ColorInt + fun getColor(matrixItem: MatrixItem): Int { + return cache.getOrPut(matrixItem.id) { + colorProvider.getColor( + when (matrixItem) { + is MatrixItem.UserItem -> getColorFromUserId(matrixItem.id) + else -> getColorFromRoomId(matrixItem.id) + } + ) + } + } + + companion object { + @ColorRes + @VisibleForTesting + fun getColorFromUserId(userId: String?): Int { + var hash = 0 + + userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() } + + return when (abs(hash) % 8) { + 1 -> R.color.riotx_username_2 + 2 -> R.color.riotx_username_3 + 3 -> R.color.riotx_username_4 + 4 -> R.color.riotx_username_5 + 5 -> R.color.riotx_username_6 + 6 -> R.color.riotx_username_7 + 7 -> R.color.riotx_username_8 + else -> R.color.riotx_username_1 + } + } + + @ColorRes + private fun getColorFromRoomId(roomId: String?): Int { + return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) { + 1 -> R.color.riotx_avatar_fill_2 + 2 -> R.color.riotx_avatar_fill_3 + else -> R.color.riotx_avatar_fill_1 + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index e050889dbd..f7a1a18d9f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -21,13 +21,13 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.extensions.localDateTime -import im.vector.app.core.resources.ColorProvider import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType @@ -49,7 +49,7 @@ import javax.inject.Inject class MessageInformationDataFactory @Inject constructor(private val session: Session, private val roomSummaryHolder: RoomSummaryHolder, private val dateFormatter: VectorDateFormatter, - private val colorProvider: ColorProvider) { + private val vectorPreferences: VectorPreferences) { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { // Non nullability has been tested before @@ -81,6 +81,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses avatarUrl = event.senderInfo.avatarUrl, memberName = event.senderInfo.disambiguatedDisplayName, showInformation = showInformation, + forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(), orderedReactionList = event.annotations?.reactionsSummary // ?.filter { isSingleEmoji(it.key) } ?.map { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 7c2a6286b9..e0f67fdd30 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -21,6 +21,8 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.R import im.vector.app.core.utils.DebouncedClickListener @@ -69,8 +71,14 @@ abstract class AbsMessageItem : AbsBaseMessageItem holder.avatarImageView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null) holder.avatarImageView.visibility = View.GONE - holder.memberNameView.visibility = View.GONE - holder.timeView.visibility = View.GONE + if (attributes.informationData.forceShowTimestamp) { + holder.memberNameView.isInvisible = true + holder.timeView.isVisible = true + holder.timeView.text = attributes.informationData.time + } else { + holder.memberNameView.isVisible = false + holder.timeView.isVisible = false + } holder.avatarImageView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null) } @@ -85,7 +93,7 @@ abstract class AbsMessageItem : AbsBaseMessageItem super.unbind(holder) } - private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(informationData.senderId) + private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(informationData.matrixItem) abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) { val avatarImageView by bind(R.id.messageAvatarImageView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index b26c9315d4..fd8a9653e6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -32,6 +32,7 @@ data class MessageInformationData( val avatarUrl: String?, val memberName: CharSequence? = null, val showInformation: Boolean = true, + val forceShowTimestamp: Boolean = false, /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val pollResponseAggregatedSummary: PollResponseData? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt index a58b8291c7..691b6ec4ee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/widget/RoomWidgetItem.kt @@ -27,7 +27,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.onClick -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.widgets.model.Widget import java.net.URL @@ -42,7 +42,7 @@ abstract class RoomWidgetItem : EpoxyModelWithHolder() { override fun bind(holder: Holder) { super.bind(holder) holder.widgetName.text = widget.name - holder.widgetUrl.text = tryThis { URL(widget.computedUrl) }?.host ?: widget.computedUrl + holder.widgetUrl.text = tryOrNull { URL(widget.computedUrl) }?.host ?: widget.computedUrl if (iconRes != null) { holder.iconImage.isVisible = true holder.iconImage.setImageResource(iconRes!!) diff --git a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt index 0252f20425..f6c60f6a6d 100644 --- a/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/homeserver/HomeServerCapabilitiesViewModel.kt @@ -31,7 +31,7 @@ import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.userdirectory.KnownUsersFragment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities @@ -68,7 +68,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor( private fun initAdminE2eByDefault() { viewModelScope.launch(Dispatchers.IO) { - val adminE2EByDefault = tryThis { + val adminE2EByDefault = tryOrNull { rawService.getElementWellknown(session.myUserId) ?.isE2EByDefault() ?: true diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt index 2602dfd6c7..76947e8d3d 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt @@ -53,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - avatarRenderer.render(context, glideRequests, matrixItem, target) + avatarRenderer.render(glideRequests, matrixItem, target) } // ReplacementSpan ***************************************************************************** @@ -99,7 +99,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, val icon = try { avatarRenderer.getCachedDrawable(glideRequests, matrixItem) } catch (exception: Exception) { - avatarRenderer.getPlaceholderDrawable(context, matrixItem) + avatarRenderer.getPlaceholderDrawable(matrixItem) } return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 74aca87bd1..2a692c2d53 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -19,10 +19,12 @@ package im.vector.app.features.login import android.content.Context import android.content.Intent import android.view.View +import android.view.ViewGroup import androidx.annotation.CallSuper import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat +import androidx.core.view.children import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -42,10 +44,10 @@ import im.vector.app.features.login.terms.LoginTermsFragment import im.vector.app.features.login.terms.LoginTermsFragmentArgument import im.vector.app.features.login.terms.toLocalizedLoginTerms import im.vector.app.features.pin.UnlockedActivity +import kotlinx.android.synthetic.main.activity_login.* import org.matrix.android.sdk.api.auth.registration.FlowResult import org.matrix.android.sdk.api.auth.registration.Stage -import org.matrix.android.sdk.api.extensions.tryThis -import kotlinx.android.synthetic.main.activity_login.* +import org.matrix.android.sdk.api.extensions.tryOrNull import javax.inject.Inject /** @@ -72,6 +74,13 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) private val commonOption: (FragmentTransaction) -> Unit = { ft -> + // Find the loginLogo on the current Fragment, this should not return null + (topFragment?.view as? ViewGroup) + // Find findViewById does not work, I do not know why + // findViewById(R.id.loginLogo) + ?.children + ?.firstOrNull { it.id == R.id.loginLogo } + ?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) } @@ -127,7 +136,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc is LoginViewEvents.OutdatedHomeserver -> { AlertDialog.Builder(this) .setTitle(R.string.login_error_outdated_homeserver_title) - .setMessage(R.string.login_error_outdated_homeserver_content) + .setMessage(R.string.login_error_outdated_homeserver_warning_content) .setPositiveButton(R.string.ok, null) .show() Unit @@ -136,6 +145,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc addFragmentToBackstack(R.id.loginFragmentContainer, LoginServerSelectionFragment::class.java, option = { ft -> + findViewById(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } // TODO Disabled because it provokes a flickering @@ -262,7 +272,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc super.onNewIntent(intent) intent?.data - ?.let { tryThis { it.getQueryParameter("loginToken") } } + ?.let { tryOrNull { it.getQueryParameter("loginToken") } } ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index aed186bcd6..f986227961 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -748,34 +748,21 @@ class LoginViewModel @AssistedInject constructor( else -> LoginMode.Unsupported } - if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { - notSupported() - } else { - // FIXME We should post a view event here normally? - setState { - copy( - asyncHomeServerLoginFlowRequest = Uninitialized, - homeServerUrl = data.homeServerUrl, - loginMode = loginMode, - loginModeSupportedTypes = data.supportedLoginTypes.toList() - ) - } + // FIXME We should post a view event here normally? + setState { + copy( + asyncHomeServerLoginFlowRequest = Uninitialized, + homeServerUrl = data.homeServerUrl, + loginMode = loginMode, + loginModeSupportedTypes = data.supportedLoginTypes.toList() + ) + } + if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) + || data.isOutdatedHomeserver) { + // Notify the UI + _viewEvents.post(LoginViewEvents.OutdatedHomeserver) } } - is LoginFlowResult.OutdatedHomeserver -> { - notSupported() - } - } - } - - private fun notSupported() { - // Notify the UI - _viewEvents.post(LoginViewEvents.OutdatedHomeserver) - - setState { - copy( - asyncHomeServerLoginFlowRequest = Uninitialized - ) } } }) diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index cf1381a1cf..0336e2d03c 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -39,7 +39,7 @@ import im.vector.app.core.utils.isLocalFile import kotlinx.android.parcel.Parcelize import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import java.io.File import javax.inject.Inject @@ -109,7 +109,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: fun clear(imageView: ImageView) { // It can be called after recycler view is destroyed, just silently catch // We'd better keep ref to requestManager, but we don't have it - tryThis { + tryOrNull { GlideApp .with(imageView).clear(imageView) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index e689f9df3f..26f00fcef9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -27,9 +27,9 @@ import im.vector.app.BuildConfig import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.settings.VectorPreferences +import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.content.ContentUrlResolver -import me.gujun.android.span.span import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -72,6 +72,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context private val currentSession: Session? get() = activeSessionDataSource.currentValue?.orNull() + private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat() + /** Should be called as soon as a new event is ready to be displayed. The notification corresponding to this event will not be displayed until @@ -243,8 +245,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context roomEvents.add(event) } } - is InviteNotifiableEvent -> invitationEvents.add(event) - is SimpleNotifiableEvent -> simpleEvents.add(event) + is InviteNotifiableEvent -> invitationEvents.add(event) + is SimpleNotifiableEvent -> simpleEvents.add(event) else -> Timber.w("Type not handled") } } @@ -253,6 +255,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context var globalLastMessageTimestamp = 0L + val newSettings = vectorPreferences.useCompleteNotificationFormat() + if (newSettings != useCompleteNotificationFormat) { + // Settings has changed, remove all current notifications + notificationUtils.cancelAllNotifications() + useCompleteNotificationFormat = newSettings + } + + var simpleNotificationRoomCounter = 0 + var simpleNotificationMessageCounter = 0 + // events have been grouped by roomId for ((roomId, events) in roomIdToEventMap) { // Build the notification for the room @@ -263,6 +275,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context continue } + simpleNotificationRoomCounter++ val roomName = events[0].roomName ?: events[0].senderName ?: "" val roomEventGroupInfo = RoomEventGroupInfo( @@ -303,6 +316,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context roomEventGroupInfo.hasSmartReplyError = true } else { if (!event.isRedacted) { + simpleNotificationMessageCounter++ style.addMessage(event.body, event.timestamp, senderPerson) } } @@ -361,16 +375,18 @@ class NotificationDrawerManager @Inject constructor(private val context: Context stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description) } - val notification = notificationUtils.buildMessagesListNotification( - style, - roomEventGroupInfo, - largeBitmap, - lastMessageTimestamp, - myUserDisplayName, - tickerText) + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildMessagesListNotification( + style, + roomEventGroupInfo, + largeBitmap, + lastMessageTimestamp, + myUserDisplayName, + tickerText) - // is there an id for this room? - notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) + // is there an id for this room? + notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) + } hasNewEvent = true summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing @@ -383,8 +399,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in invitationEvents) { // We build a invitation notification if (firstTime || !event.hasBeenDisplayed) { - val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) + notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) + } event.hasBeenDisplayed = true // we can consider it as displayed hasNewEvent = true summaryIsNoisy = summaryIsNoisy || event.noisy @@ -396,8 +414,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context for (event in simpleEvents) { // We build a simple notification if (firstTime || !event.hasBeenDisplayed) { - val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) - notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) + notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) + } event.hasBeenDisplayed = true // we can consider it as displayed hasNewEvent = true summaryIsNoisy = summaryIsNoisy || event.noisy @@ -421,19 +441,76 @@ class NotificationDrawerManager @Inject constructor(private val context: Context if (eventList.isEmpty() || eventList.all { it.isRedacted }) { notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) } else { + // FIXME roomIdToEventMap.size is not correct, this is the number of rooms val nbEvents = roomIdToEventMap.size + simpleEvents.size val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) summaryInboxStyle.setBigContentTitle(sumTitle) // TODO get latest event? .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) - val notification = notificationUtils.buildSummaryListNotification( - summaryInboxStyle, - sumTitle, - noisy = hasNewEvent && summaryIsNoisy, - lastMessageTimestamp = globalLastMessageTimestamp) + if (useCompleteNotificationFormat) { + val notification = notificationUtils.buildSummaryListNotification( + summaryInboxStyle, + sumTitle, + noisy = hasNewEvent && summaryIsNoisy, + lastMessageTimestamp = globalLastMessageTimestamp) - notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) + } else { + // Add the simple events as message (?) + simpleNotificationMessageCounter += simpleEvents.size + val numberOfInvitations = invitationEvents.size + + val privacyTitle = if (numberOfInvitations > 0) { + val invitationsStr = stringProvider.getQuantityString(R.plurals.notification_invitations, numberOfInvitations, numberOfInvitations) + if (simpleNotificationMessageCounter > 0) { + // Invitation and message + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + simpleNotificationMessageCounter, simpleNotificationMessageCounter) + if (simpleNotificationRoomCounter > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, + simpleNotificationRoomCounter, simpleNotificationRoomCounter) + stringProvider.getString( + R.string.notification_unread_notified_messages_in_room_and_invitation, + messageStr, + roomStr, + invitationsStr + ) + } else { + // In one room + stringProvider.getString( + R.string.notification_unread_notified_messages_and_invitation, + messageStr, + invitationsStr + ) + } + } else { + // Only invitation + invitationsStr + } + } else { + // No invitation, only messages + val messageStr = stringProvider.getQuantityString(R.plurals.room_new_messages_notification, + simpleNotificationMessageCounter, simpleNotificationMessageCounter) + if (simpleNotificationRoomCounter > 1) { + // In several rooms + val roomStr = stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages_in_room_rooms, + simpleNotificationRoomCounter, simpleNotificationRoomCounter) + stringProvider.getString(R.string.notification_unread_notified_messages_in_room, messageStr, roomStr) + } else { + // In one room + messageStr + } + } + val notification = notificationUtils.buildSummaryListNotification( + style = null, + compatSummary = privacyTitle, + noisy = hasNewEvent && summaryIsNoisy, + lastMessageTimestamp = globalLastMessageTimestamp) + + notificationUtils.showNotificationMessage(null, SUMMARY_NOTIFICATION_ID, notification) + } if (hasNewEvent && summaryIsNoisy) { try { diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 2b070e540e..9d89168bb8 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -772,7 +772,7 @@ class NotificationUtils @Inject constructor(private val context: Context, /** * Build the summary notification */ - fun buildSummaryListNotification(style: NotificationCompat.InboxStyle, + fun buildSummaryListNotification(style: NotificationCompat.InboxStyle?, compatSummary: String, noisy: Boolean, lastMessageTimestamp: Long): Notification { diff --git a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt index a2930c6129..0216ec512e 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinActivity.kt @@ -28,7 +28,6 @@ import im.vector.app.core.platform.VectorBaseActivity class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { companion object { - const val PIN_REQUEST_CODE = 17890 fun newIntent(context: Context, args: PinArgs): Intent { diff --git a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt index fedc705176..b6e238c2dc 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinFragment.kt @@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.toast import im.vector.app.features.MainActivity import im.vector.app.features.MainActivityArgs +import im.vector.app.features.settings.VectorPreferences import kotlinx.android.parcel.Parcelize import kotlinx.coroutines.launch import javax.inject.Inject @@ -42,7 +43,8 @@ data class PinArgs( ) : Parcelable class PinFragment @Inject constructor( - private val pinCodeStore: PinCodeStore + private val pinCodeStore: PinCodeStore, + private val vectorPreferences: VectorPreferences ) : VectorBaseFragment() { private val fragmentArgs: PinArgs by args() @@ -53,54 +55,10 @@ class PinFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) when (fragmentArgs.pinMode) { PinMode.CREATE -> showCreateFragment() - PinMode.DELETE -> showDeleteFragment() PinMode.AUTH -> showAuthFragment() } } - private fun showDeleteFragment() { - val encodedPin = pinCodeStore.getEncodedPin() ?: return - val authFragment = PFLockScreenFragment() - val builder = PFFLockScreenConfiguration.Builder(requireContext()) - .setUseBiometric(pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0) - .setTitle(getString(R.string.auth_pin_confirm_to_disable_title)) - .setClearCodeOnError(true) - .setMode(PFFLockScreenConfiguration.MODE_AUTH) - authFragment.setConfiguration(builder.build()) - authFragment.setEncodedPinCode(encodedPin) - authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener { - override fun onPinLoginFailed() { - onWrongPin() - } - - override fun onBiometricAuthSuccessful() { - lifecycleScope.launch { - pinCodeStore.deleteEncodedPin() - vectorBaseActivity.setResult(Activity.RESULT_OK) - vectorBaseActivity.finish() - } - } - - override fun onBiometricAuthLoginFailed() { - val remainingAttempts = pinCodeStore.onWrongBiometrics() - if (remainingAttempts <= 0) { - // Disable Biometrics - builder.setUseBiometric(false) - authFragment.setConfiguration(builder.build()) - } - } - - override fun onCodeInputSuccessful() { - lifecycleScope.launch { - pinCodeStore.deleteEncodedPin() - vectorBaseActivity.setResult(Activity.RESULT_OK) - vectorBaseActivity.finish() - } - } - }) - replaceFragment(R.id.pinFragmentContainer, authFragment) - } - private fun showCreateFragment() { val createFragment = PFLockScreenFragment() val builder = PFFLockScreenConfiguration.Builder(requireContext()) @@ -131,9 +89,8 @@ class PinFragment @Inject constructor( val authFragment = PFLockScreenFragment() val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0 val builder = PFFLockScreenConfiguration.Builder(requireContext()) - .setUseBiometric(true) .setAutoShowBiometric(true) - .setUseBiometric(canUseBiometrics) + .setUseBiometric(vectorPreferences.useBiometricsToUnlock() && canUseBiometrics) .setAutoShowBiometric(canUseBiometrics) .setTitle(getString(R.string.auth_pin_title)) .setLeftButton(getString(R.string.auth_pin_forgot)) diff --git a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt index aa7202a7ae..3fc4152ee6 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinLocker.kt @@ -22,12 +22,14 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.OnLifecycleEvent +import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +// 2 minutes, when enabled private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L /** @@ -35,24 +37,22 @@ private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L * It automatically locks when entering background/foreground with a grace period. * You can force to unlock with unlock method, use it whenever the pin code has been validated. */ - @Singleton -class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : LifecycleObserver { +class PinLocker @Inject constructor( + private val pinCodeStore: PinCodeStore, + private val vectorPreferences: VectorPreferences +) : LifecycleObserver { enum class State { // App is locked, can be unlock LOCKED, - // App is blocked and can't be unlocked as long as the app is in foreground - BLOCKED, - - // is unlocked, the app can be used + // App is unlocked, the app can be used UNLOCKED } private val liveState = MutableLiveData() - private var isBlocked = false private var shouldBeLocked = true private var entersBackgroundTs = 0L @@ -62,13 +62,13 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li private fun computeState() { GlobalScope.launch { - val state = if (isBlocked) { - State.BLOCKED - } else if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { + val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) { State.LOCKED } else { State.UNLOCKED } + .also { Timber.v("New state: $it") } + if (liveState.value != state) { liveState.postValue(state) } @@ -81,23 +81,25 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li computeState() } - fun block() { - Timber.v("Block app") - isBlocked = true - computeState() - } - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun entersForeground() { val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs - shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= PERIOD_OF_GRACE_IN_MS - Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background") + shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod() + Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked") computeState() } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun entersBackground() { - isBlocked = false + Timber.v("App enters background") entersBackgroundTs = SystemClock.elapsedRealtime() } + + private fun getGracePeriod(): Long { + return if (vectorPreferences.useGracePeriod()) { + PERIOD_OF_GRACE_IN_MS + } else { + 0L + } + } } diff --git a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt index 732b55fd58..c24ac5adf2 100644 --- a/vector/src/main/java/im/vector/app/features/pin/PinMode.kt +++ b/vector/src/main/java/im/vector/app/features/pin/PinMode.kt @@ -18,6 +18,5 @@ package im.vector.app.features.pin enum class PinMode { CREATE, - DELETE, AUTH } diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt index 82f989342e..119be66f94 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownExt.kt @@ -16,12 +16,13 @@ package im.vector.app.features.raw.wellknown +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.internal.util.awaitCallback suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? { - return awaitCallback { getWellknown(userId, it) } - .let { ElementWellKnownMapper.from(it) } + return tryOrNull { awaitCallback { getWellknown(userId, it) } } + ?.let { ElementWellKnownMapper.from(it) } } fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true diff --git a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownMapper.kt b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownMapper.kt index 07e496f85f..899a7d3fe6 100644 --- a/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownMapper.kt +++ b/vector/src/main/java/im/vector/app/features/raw/wellknown/ElementWellKnownMapper.kt @@ -17,6 +17,7 @@ package im.vector.app.features.raw.wellknown import com.squareup.moshi.JsonAdapter +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.internal.di.MoshiProvider object ElementWellKnownMapper { @@ -24,6 +25,6 @@ object ElementWellKnownMapper { val adapter: JsonAdapter = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java) fun from(value: String): ElementWellKnown? { - return adapter.fromJson(value) + return tryOrNull("Unable to parse well-known data") { adapter.fromJson(value) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index be4bbed15b..c213992258 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -34,7 +34,7 @@ import im.vector.app.features.roomdirectory.RoomDirectoryActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility @@ -59,7 +59,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr private fun initAdminE2eByDefault() { viewModelScope.launch(Dispatchers.IO) { - adminE2EByDefault = tryThis { + adminE2EByDefault = tryOrNull { rawService.getElementWellknown(session.myUserId) ?.isE2EByDefault() ?: true diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 9fc566aac4..9ccb8d7031 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -28,7 +28,7 @@ import im.vector.app.R import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.themes.ThemeUtils -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import timber.log.Timber import javax.inject.Inject @@ -166,6 +166,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { // Security const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE" const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG" + private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG" + private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG" + const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" // other const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" @@ -424,7 +427,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun getUnknownDeviceDismissedList(): List { - return tryThis { + return tryOrNull { defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() }.orEmpty() } @@ -839,14 +842,31 @@ class VectorPreferences @Inject constructor(private val context: Context) { } /** - * The user enable protecting app access with pin code + * The user enable protecting app access with pin code. + * Currently we use the pin code store to know if the pin is enabled, so this is not used */ fun useFlagPinCode(): Boolean { return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false) } + fun useBiometricsToUnlock(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, true) + } + + fun useGracePeriod(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG, true) + } + + /** + * Return true if Pin code is disabled, or if user set the settings to see full notification content + */ + fun useCompleteNotificationFormat(): Boolean { + return !useFlagPinCode() + || defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true) + } + fun backgroundSyncTimeOut(): Int { - return tryThis { + return tryOrNull { // The xml pref is saved as a string so use getString and parse defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt() } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS @@ -860,7 +880,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { } fun backgroundSyncDelay(): Int { - return tryThis { + return tryOrNull { // The xml pref is saved as a string so use getString and parse defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt() } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt index dda1579db4..50692ef255 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsActivity.kt @@ -17,6 +17,7 @@ package im.vector.app.features.settings import android.content.Context import android.content.Intent +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat @@ -134,6 +135,14 @@ class VectorSettingsActivity : VectorBaseActivity(), } } + fun navigateTo(fragmentClass: Class) { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.right_in, R.anim.fade_out, R.anim.fade_in, R.anim.right_out) + .replace(R.id.vector_settings_page, fragmentClass, null) + .addToBackStack(null) + .commit() + } + companion object { fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt index 3f74c4db75..d6d9cf6e31 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsNotificationPreferenceFragment.kt @@ -37,7 +37,7 @@ import im.vector.app.core.utils.requestDisablingBatteryOptimization import im.vector.app.features.notifications.NotificationUtils import im.vector.app.push.fcm.FcmHelper import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryThis +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleKind import javax.inject.Inject @@ -88,7 +88,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut()) it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - val syncTimeout = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS + val syncTimeout = tryOrNull { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS vectorPreferences.setBackgroundSyncTimeout(maxOf(0, syncTimeout)) refreshBackgroundSyncPrefs() } @@ -101,7 +101,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor( it.summary = secondsToText(vectorPreferences.backgroundSyncDelay()) it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> if (newValue is String) { - val syncDelay = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS + val syncDelay = tryOrNull { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay)) refreshBackgroundSyncPrefs() } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt new file mode 100644 index 0000000000..4a6965618e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPinFragment.kt @@ -0,0 +1,83 @@ +/* + * 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.features.settings + +import android.content.Intent +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.SwitchPreference +import im.vector.app.R +import im.vector.app.features.navigation.Navigator +import im.vector.app.features.notifications.NotificationDrawerManager +import im.vector.app.features.pin.PinActivity +import im.vector.app.features.pin.PinCodeStore +import im.vector.app.features.pin.PinMode +import kotlinx.coroutines.launch +import javax.inject.Inject + +class VectorSettingsPinFragment @Inject constructor( + private val pinCodeStore: PinCodeStore, + private val navigator: Navigator, + private val notificationDrawerManager: NotificationDrawerManager +) : VectorSettingsBaseFragment() { + + override var titleRes = R.string.settings_security_application_protection_screen_title + override val preferenceXmlRes = R.xml.vector_settings_pin + + private val usePinCodePref by lazy { + findPreference(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!! + } + + private val useCompleteNotificationPref by lazy { + findPreference(VectorPreferences.SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG)!! + } + + override fun bindPref() { + refreshPinCodeStatus() + + useCompleteNotificationPref.setOnPreferenceChangeListener { _, _ -> + // Refresh the drawer for an immediate effect of this change + notificationDrawerManager.refreshNotificationDrawer() + true + } + } + + private fun refreshPinCodeStatus() { + lifecycleScope.launchWhenResumed { + val hasPinCode = pinCodeStore.hasEncodedPin() + usePinCodePref.isChecked = hasPinCode + usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + if (hasPinCode) { + lifecycleScope.launch { + pinCodeStore.deleteEncodedPin() + refreshPinCodeStatus() + } + } else { + navigator.openPinCode(this@VectorSettingsPinFragment, PinMode.CREATE) + } + true + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == PinActivity.PIN_REQUEST_CODE) { + refreshPinCodeStatus() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 034fa617f0..44cef8f974 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -24,6 +24,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import android.widget.Button +import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat @@ -38,6 +39,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.dialogs.ExportKeysDialog import im.vector.app.core.extensions.queryExportKeys +import im.vector.app.core.extensions.showPassword import im.vector.app.core.intent.ExternalIntentData import im.vector.app.core.intent.analyseIntent import im.vector.app.core.intent.getFilenameFromUri @@ -54,9 +56,8 @@ import im.vector.app.features.crypto.recover.BootstrapBottomSheet import im.vector.app.features.navigation.Navigator import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinCodeStore -import im.vector.app.features.pin.PinLocker import im.vector.app.features.pin.PinMode -import im.vector.app.features.raw.wellknown.ElementWellKnownMapper +import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.themes.ThemeUtils import io.reactivex.android.schedulers.AndroidSchedulers @@ -74,7 +75,6 @@ import javax.inject.Inject class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val vectorPreferences: VectorPreferences, - private val pinLocker: PinLocker, private val activeSessionHolder: ActiveSessionHolder, private val pinCodeStore: PinCodeStore, private val navigator: Navigator @@ -126,8 +126,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( findPreference(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!! } - private val usePinCodePref by lazy { - findPreference(VectorPreferences.SETTINGS_SECURITY_USE_PIN_CODE_FLAG)!! + private val openPinCodeSettingsPref by lazy { + findPreference("SETTINGS_SECURITY_PIN")!! } override fun onCreateRecyclerView(inflater: LayoutInflater?, parent: ViewGroup?, savedInstanceState: Bundle?): RecyclerView { @@ -153,14 +153,13 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( disposables.add(it) } - vectorActivity.getVectorComponent() - .rawService() - .getWellknown(session.myUserId, object : MatrixCallback { - override fun onSuccess(data: String) { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible = - ElementWellKnownMapper.from(data)?.isE2EByDefault() == false - } - }) + lifecycleScope.launchWhenResumed { + findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_HS_ADMIN_DISABLED_E2E_DEFAULT)?.isVisible = + vectorActivity.getVectorComponent() + .rawService() + .getElementWellknown(session.myUserId) + ?.isE2EByDefault() == false + } } private val secureBackupCategory by lazy { @@ -263,7 +262,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( } } - refreshPinCodeStatus() + openPinCodeSettingsPref.setOnPreferenceClickListener { + openPinCodePreferenceScreen() + true + } refreshXSigningStatus() @@ -319,62 +321,64 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) { - val uri = data?.data - if (resultCode == Activity.RESULT_OK && uri != null) { - activity?.let { activity -> - ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { - override fun onPassphrase(passphrase: String) { - displayLoadingView() + if (resultCode == Activity.RESULT_OK) { + when (requestCode) { + REQUEST_CODE_SAVE_MEGOLM_EXPORT -> { + val uri = data?.data + if (uri != null) { + activity?.let { activity -> + ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + displayLoadingView() - KeysExporter(session) - .export(requireContext(), - passphrase, - uri, - object : MatrixCallback { - override fun onSuccess(data: Boolean) { - if (data) { - requireActivity().toast(getString(R.string.encryption_exported_successfully)) - } else { - requireActivity().toast(getString(R.string.unexpected_error)) - } - hideLoadingView() - } + KeysExporter(session) + .export(requireContext(), + passphrase, + uri, + object : MatrixCallback { + override fun onSuccess(data: Boolean) { + if (data) { + requireActivity().toast(getString(R.string.encryption_exported_successfully)) + } else { + requireActivity().toast(getString(R.string.unexpected_error)) + } + hideLoadingView() + } - override fun onFailure(failure: Throwable) { - onCommonDone(failure.localizedMessage) - } - }) + override fun onFailure(failure: Throwable) { + onCommonDone(failure.localizedMessage) + } + }) + } + }) } - }) + } + } + PinActivity.PIN_REQUEST_CODE -> { + doOpenPinCodePreferenceScreen() + } + REQUEST_E2E_FILE_REQUEST_CODE -> { + importKeys(data) } - } - } else if (requestCode == PinActivity.PIN_REQUEST_CODE) { - pinLocker.unlock() - refreshPinCodeStatus() - } else if (requestCode == REQUEST_E2E_FILE_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK) { - importKeys(data) } } } - private fun refreshPinCodeStatus() { + private fun openPinCodePreferenceScreen() { lifecycleScope.launchWhenResumed { val hasPinCode = pinCodeStore.hasEncodedPin() - usePinCodePref.isChecked = hasPinCode - usePinCodePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val pinMode = if (hasPinCode) { - PinMode.DELETE - } else { - PinMode.CREATE - } - navigator.openPinCode(this@VectorSettingsSecurityPrivacyFragment, pinMode) - true + if (hasPinCode) { + navigator.openPinCode(this@VectorSettingsSecurityPrivacyFragment, PinMode.AUTH) + } else { + doOpenPinCodePreferenceScreen() } } } + private fun doOpenPinCodePreferenceScreen() { + (vectorActivity as? VectorSettingsActivity)?.navigateTo(VectorSettingsPinFragment::class.java) + } + private fun refreshKeysManagementSection() { // If crypto is not enabled parent section will be removed // TODO notice that this will not work when no network @@ -458,6 +462,15 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( val passPhraseEditText = dialogLayout.findViewById(R.id.dialog_e2e_keys_passphrase_edit_text) val importButton = dialogLayout.findViewById