Merge branch 'release/1.0.8'

This commit is contained in:
Benoit Marty 2020-09-25 14:08:25 +02:00
commit 34760a00be
144 changed files with 1987 additions and 1224 deletions

10
.github/ISSUE_TEMPLATE/matrix-sdk.md vendored Normal file
View file

@ -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: ''
---
<!-- This issue template should be used by third party application maintainers, to report a bug or to request a feature on the SDK module of the application Element Android-->

View file

@ -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) Changes in Element 1.0.7 (2020-09-17)
=================================================== ===================================================

View file

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest 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.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -212,7 +212,7 @@ class UnwedgingTest : InstrumentedTest {
mTestHelper.waitWithLatch { mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) { mTestHelper.retryPeriodicallyWithLatch(it) {
// we should get back the key and be able to decrypt // we should get back the key and be able to decrypt
val result = tryThis { val result = tryOrNull {
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
} }
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")

View file

@ -20,7 +20,7 @@ import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback 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.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -227,7 +227,7 @@ class WithHeldTests : InstrumentedTest {
mTestHelper.retryPeriodicallyWithLatch(latch) { mTestHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
// try to decrypt and force key request // try to decrypt and force key request
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
} }
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null timeLineEvent != null

View file

@ -17,13 +17,11 @@
package org.matrix.android.sdk.api.auth.data 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 { sealed class LoginFlowResult {
data class Success( data class Success(
val supportedLoginTypes: List<String>, val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
) : LoginFlowResult() ) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
} }

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.extensions
import timber.log.Timber import timber.log.Timber
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? { inline fun <A> tryOrNull(message: String? = null, operation: () -> A): A? {
return try { return try {
operation() operation()
} catch (any: Throwable) { } catch (any: Throwable) {

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.api.failure 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.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException import java.io.IOException
@ -49,7 +49,7 @@ fun Throwable.isInvalidPassword(): Boolean {
*/ */
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
return if (this is Failure.OtherServerError && this.httpCode == 401) { return if (this is Failure.OtherServerError && this.httpCode == 401) {
tryThis { tryOrNull {
MoshiProvider.providesMoshi() MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java) .adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody) .fromJson(this.errorBody)

View file

@ -273,16 +273,16 @@ internal class DefaultAuthenticationService @Inject constructor(
} }
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
return if (versions.isSupportedBySdk()) { // Get the login flow
// Get the login flow val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) { apiCall = authAPI.getLoginFlows()
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver
} }
return LoginFlowResult.Success(
loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl,
!versions.isSupportedBySdk()
)
} }
override fun getRegistrationWizard(): RegistrationWizard { override fun getRegistrationWizard(): RegistrationWizard {

View file

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event 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.model.rest.ShareRequestCancellation
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class CancelGossipRequestWorker(context: Context, internal class CancelGossipRequestWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<CancelGossipRequestWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val requestId: String, val requestId: String,
val recipients: Map<String, List<String>> val recipients: Map<String, List<String>>,
) { override val lastFailureMessage: String? = null
) : SessionWorkerParams {
companion object { companion object {
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
return Params( return Params(
sessionId = sessionId, sessionId = sessionId,
requestId = request.requestId, 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 eventBus: EventBus
@Inject lateinit var credentials: Credentials @Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(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 suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val toDeviceContent = ShareRequestCancellation( val toDeviceContent = ShareRequestCancellation(
@ -107,13 +100,17 @@ internal class CancelGossipRequestWorker(context: Context,
) )
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) 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)
}
} }

View file

@ -27,10 +27,16 @@ import androidx.lifecycle.LiveData
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Lazy 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.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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.failure.Failure
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.CryptoService 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.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.fetchCopied 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 org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -345,13 +345,13 @@ internal class DefaultCryptoService @Inject constructor(
// Open the store // Open the store
cryptoStore.open() cryptoStore.open()
// this can throw if no network // this can throw if no network
tryThis { tryOrNull {
uploadDeviceKeys() uploadDeviceKeys()
} }
oneTimeKeysUploader.maybeUploadOneTimeKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys()
// this can throw if no backup // this can throw if no backup
tryThis { tryOrNull {
keysBackupService.checkAndStartKeysBackup() keysBackupService.checkAndStartKeysBackup()
} }
} }
@ -1072,7 +1072,11 @@ internal class DefaultCryptoService @Inject constructor(
throw Exception("Error") throw Exception("Error")
} }
megolmSessionDataImporter.handle(importedSessions, true, progressListener) megolmSessionDataImporter.handle(
megolmSessionsData = importedSessions,
fromBackup = false,
progressListener = progressListener
)
} }
}.foldToCallback(callback) }.foldToCallback(callback)
} }

View file

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event 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.model.rest.SecretShareRequest
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class SendGossipRequestWorker(context: Context, internal class SendGossipRequestWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendGossipRequestWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val keyShareRequest: OutgoingRoomKeyRequest? = null, 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 sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials @Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(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 suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val eventType: String val eventType: String
@ -121,7 +114,7 @@ internal class SendGossipRequestWorker(context: Context,
} }
} }
else -> { else -> {
return Result.success(errorOutputData).also { return buildErrorResult(params, "Unknown empty gossiping request").also {
Timber.e("Unknown empty gossiping request: $params") Timber.e("Unknown empty gossiping request: $params")
} }
} }
@ -137,13 +130,17 @@ internal class SendGossipRequestWorker(context: Context,
) )
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) 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)
}
} }

View file

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event 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.model.event.SecretSendEventContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class SendGossipWorker(context: Context, internal class SendGossipWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val secretValue: String, val secretValue: String,
val request: IncomingSecretShareRequest val request: IncomingSecretShareRequest,
) override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var cryptoStore: IMXCryptoStore
@ -58,18 +58,11 @@ internal class SendGossipWorker(context: Context,
@Inject lateinit var messageEncrypter: MessageEncrypter @Inject lateinit var messageEncrypter: MessageEncrypter
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(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 suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val eventType: String = EventType.SEND_SECRET val eventType: String = EventType.SEND_SECRET
@ -81,7 +74,7 @@ internal class SendGossipWorker(context: Context,
val requestingUserId = params.request.userId ?: "" val requestingUserId = params.request.userId ?: ""
val requestingDeviceId = params.request.deviceId ?: "" val requestingDeviceId = params.request.deviceId ?: ""
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) 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) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
} }
@ -94,7 +87,7 @@ internal class SendGossipWorker(context: Context,
if (olmSessionResult?.sessionId == null) { if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there // no session with this device, probably because there
// were no one-time keys. // 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) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("no session with this device, probably because there were no one-time keys.") 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) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) 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)
}
} }

View file

@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
* Must be call on the crypto coroutine thread * Must be call on the crypto coroutine thread
* *
* @param megolmSessionsData megolm sessions. * @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 * @param progressListener the progress listener
* @return import room keys result * @return import room keys result
*/ */

View file

@ -20,6 +20,10 @@ package org.matrix.android.sdk.internal.crypto.store.db
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy 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.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.events.model.Event 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.CryptoDatabase
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.SessionScope 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.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import timber.log.Timber import timber.log.Timber
@ -541,7 +541,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceId = it.deviceId deviceId = it.deviceId
) )
} }
monarchy.writeAsync { realm -> doRealmTransactionAsync(realmConfiguration) { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm() realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach { entities.forEach {
realm.insertOrUpdate(it) realm.insertOrUpdate(it)
@ -1191,7 +1191,7 @@ internal class RealmCryptoStore @Inject constructor(
.findAll() .findAll()
.mapNotNull { entity -> .mapNotNull { entity ->
when (entity.type) { when (entity.type) {
GossipRequestType.KEY -> { GossipRequestType.KEY -> {
IncomingRoomKeyRequest( IncomingRoomKeyRequest(
userId = entity.otherUserId, userId = entity.otherUserId,
deviceId = entity.otherDeviceId, deviceId = entity.otherDeviceId,

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types 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.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper 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) ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity -> ?.transform { deviceInfoEntity ->
tryThis { tryOrNull {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
} }
} }

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.crypto.store.db.model 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.GossipRequestType
import org.matrix.android.sdk.internal.crypto.GossipingRequestState import org.matrix.android.sdk.internal.crypto.GossipingRequestState
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
@ -45,7 +45,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
var type: GossipRequestType var type: GossipRequestType
get() { get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
} }
set(value) { set(value) {
typeStr = value.name typeStr = value.name
@ -55,7 +55,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
var requestState: GossipingRequestState var requestState: GossipingRequestState
get() { get() {
return tryThis { GossipingRequestState.valueOf(requestStateStr) } return tryOrNull { GossipingRequestState.valueOf(requestStateStr) }
?: GossipingRequestState.NONE ?: GossipingRequestState.NONE
} }
set(value) { set(value) {

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types 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.GossipRequestType
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
@ -47,7 +47,7 @@ internal open class OutgoingGossipingRequestEntity(
var type: GossipRequestType var type: GossipRequestType
get() { get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
} }
set(value) { set(value) {
typeStr = value.name typeStr = value.name
@ -57,7 +57,7 @@ internal open class OutgoingGossipingRequestEntity(
var requestState: OutgoingGossipingRequestState var requestState: OutgoingGossipingRequestState
get() { get() {
return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) }
?: OutgoingGossipingRequestState.UNSENT ?: OutgoingGossipingRequestState.UNSENT
} }
set(value) { set(value) {

View file

@ -17,17 +17,17 @@
package org.matrix.android.sdk.internal.crypto.verification package org.matrix.android.sdk.internal.crypto.verification
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.CryptoService 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.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.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -37,56 +37,56 @@ import javax.inject.Inject
*/ */
internal class SendVerificationMessageWorker(context: Context, internal class SendVerificationMessageWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendVerificationMessageWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val event: Event, val eventId: String,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
lateinit var sendVerificationMessageTask: SendVerificationMessageTask @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject override fun injectWith(injector: SessionComponent) {
lateinit var cryptoService: CryptoService injector.inject(this)
}
override suspend fun doWork(): Result { override suspend fun doSafeWork(params: Params): Result {
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found")
val params = WorkerParamsFactory.fromData<Params>(inputData) val localEventId = localEvent.eventId ?: ""
?: return Result.success(errorOutputData) 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 { return try {
val eventId = sendVerificationMessageTask.execute( val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params( SendVerificationMessageTask.Params(
event = params.event, event = localEvent,
cryptoService = cryptoService cryptoService = cryptoService
) )
) )
Result.success(Data.Builder().putString(localId, eventId).build()) Result.success(Data.Builder().putString(localEventId, resultEventId).build())
} catch (exception: Throwable) { } catch (throwable: Throwable) {
if (exception.shouldBeRetried()) { if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
Result.success(errorOutputData) buildErrorResult(params, throwable.localizedMessage ?: "error")
} }
} }
} }
companion object { override fun buildErrorParams(params: Params, message: String): Params {
private const val OUTPUT_KEY_FAILED = "failed" return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
fun hasFailed(outputData: Data): Boolean {
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
}
} }
} }

View file

@ -22,6 +22,9 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.Operation import androidx.work.Operation
import androidx.work.WorkInfo 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.R
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest 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.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.StringProvider 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 org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -87,7 +88,7 @@ internal class VerificationTransportRoomMessage(
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val enqueueInfo = enqueueSendWork(workerParams) val enqueueInfo = enqueueSendWork(workerParams)
@ -115,20 +116,30 @@ internal class VerificationTransportRoomMessage(
val observer = object : Observer<List<WorkInfo>> { val observer = object : Observer<List<WorkInfo>> {
override fun onChanged(workInfoList: List<WorkInfo>?) { override fun onChanged(workInfoList: List<WorkInfo>?) {
workInfoList workInfoList
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == enqueueInfo.second } ?.firstOrNull { it.id == enqueueInfo.second }
?.let { wInfo -> ?.let { wInfo ->
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { when (wInfo.state) {
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") WorkInfo.State.FAILED -> {
tx?.cancel(onErrorReason) tx?.cancel(onErrorReason)
} else { workLiveData.removeObserver(this)
if (onDone != null) { }
onDone() WorkInfo.State.SUCCEEDED -> {
} else { if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
tx?.state = nextState 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( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
@ -184,7 +195,7 @@ internal class VerificationTransportRoomMessage(
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() .enqueue()
// I cannot just listen to the given work request, because when used in a uniqueWork, // 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 } ?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == workRequest.id } ?.firstOrNull { it.id == workRequest.id }
?.let { wInfo -> ?.let { wInfo ->
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
callback(null, null) callback(null, null)
} else { } else {
val eventId = wInfo.outputData.getString(localId) val eventId = wInfo.outputData.getString(localId)
@ -229,7 +240,7 @@ internal class VerificationTransportRoomMessage(
) )
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
enqueueSendWork(workerParams) enqueueSendWork(workerParams)
} }
@ -249,7 +260,7 @@ internal class VerificationTransportRoomMessage(
) )
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val enqueueInfo = enqueueSendWork(workerParams) val enqueueInfo = enqueueSendWork(workerParams)
@ -280,7 +291,7 @@ internal class VerificationTransportRoomMessage(
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.build() .build()
return workManagerProvider.workManager return workManagerProvider.workManager
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() to workRequest.id .enqueue() to workRequest.id
} }

View file

@ -16,31 +16,52 @@
*/ */
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) { internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) {
Realm.getInstance(config).use { bgRealm -> asyncTransaction(monarchy.realmConfiguration, transaction)
bgRealm.beginTransaction() }
val result: T
try { internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) {
val start = System.currentTimeMillis() launch {
result = transaction(bgRealm) awaitTransaction(realmConfiguration, transaction)
if (isActive) { }
bgRealm.commitTransaction() }
val end = System.currentTimeMillis()
val time = end - start private val realmSemaphore = Semaphore(1)
Timber.v("Execute transaction in $time millis")
} suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
} finally { return realmSemaphore.withPermit {
if (bgRealm.isInTransaction) { withContext(Dispatchers.IO) {
bgRealm.cancelTransaction() Realm.getInstance(config).use { bgRealm ->
} bgRealm.beginTransaction()
} val result: T
result 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
}
}
} }
} }

View file

@ -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.session.events.model.Content
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE 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.di.MoshiProvider
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
internal object ContentMapper { internal object ContentMapper {
private val moshi = MoshiProvider.providesMoshi() private val moshi = MoshiProvider.providesMoshi()
private val adapter = moshi.adapter<Content>(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 { return content?.let {
adapter.fromJson(it) if (castJsonNumbers) {
castJsonNumberMoshi
} else {
moshi
}.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).fromJson(it)
} }
} }
fun map(content: Content?): String? { fun map(content: Content?): String? {
return content?.let { return content?.let {
adapter.toJson(it) moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).toJson(it)
} }
} }
} }

View file

@ -54,7 +54,7 @@ internal object EventMapper {
return eventEntity return eventEntity
} }
fun map(eventEntity: EventEntity): Event { fun map(eventEntity: EventEntity, castJsonNumbers: Boolean = false): Event {
val ud = eventEntity.unsignedData val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { ?.let {
@ -69,8 +69,8 @@ internal object EventMapper {
return Event( return Event(
type = eventEntity.type, type = eventEntity.type,
eventId = eventEntity.eventId, eventId = eventEntity.eventId,
content = ContentMapper.map(eventEntity.content), content = ContentMapper.map(eventEntity.content, castJsonNumbers),
prevContent = ContentMapper.map(eventEntity.prevContent), prevContent = ContentMapper.map(eventEntity.prevContent, castJsonNumbers),
originServerTs = eventEntity.originServerTs, originServerTs = eventEntity.originServerTs,
senderId = eventEntity.sender, senderId = eventEntity.sender,
stateKey = eventEntity.stateKey, stateKey = eventEntity.stateKey,
@ -96,8 +96,8 @@ internal object EventMapper {
} }
} }
internal fun EventEntity.asDomain(): Event { internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
return EventMapper.map(this) return EventMapper.map(this, castJsonNumbers)
} }
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {

View file

@ -24,7 +24,7 @@ import okio.BufferedSink
import okio.ForwardingSink import okio.ForwardingSink
import okio.Sink import okio.Sink
import okio.buffer import okio.buffer
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import java.io.IOException import java.io.IOException
internal class ProgressRequestBody(private val delegate: RequestBody, internal class ProgressRequestBody(private val delegate: RequestBody,
@ -40,7 +40,7 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
override fun isDuplex() = delegate.isDuplex() override fun isDuplex() = delegate.isDuplex()
val length = tryThis { delegate.contentLength() } ?: -1 val length = tryOrNull { delegate.contentLength() } ?: -1
override fun contentLength() = length override fun contentLength() = length

View file

@ -49,7 +49,7 @@ interface CheckNumberType {
val numberAsString = reader.nextString() val numberAsString = reader.nextString()
val decimal = BigDecimal(numberAsString) val decimal = BigDecimal(numberAsString)
if (decimal.scale() <= 0) { if (decimal.scale() <= 0) {
decimal.intValueExact() decimal.longValueExact()
} else { } else {
decimal.toDouble() decimal.toDouble()
} }

View file

@ -23,7 +23,7 @@ import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import arrow.core.Try import arrow.core.Try
import org.matrix.android.sdk.api.MatrixCallback 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.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -174,7 +174,7 @@ internal class DefaultFileService @Inject constructor(
} }
} }
toNotify?.forEach { otherCallbacks -> toNotify?.forEach { otherCallbacks ->
tryThis { otherCallbacks.onFailure(it) } tryOrNull { otherCallbacks.onFailure(it) }
} }
}, { file -> }, { file ->
callback.onSuccess(file) callback.onSuccess(file)
@ -186,7 +186,7 @@ internal class DefaultFileService @Inject constructor(
} }
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks -> toNotify?.forEach { otherCallbacks ->
tryThis { otherCallbacks.onSuccess(file) } tryOrNull { otherCallbacks.onSuccess(file) }
} }
}) })
}.toCancelable() }.toCancelable()

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.call
import android.os.SystemClock import android.os.SystemClock
import org.matrix.android.sdk.api.MatrixCallback 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.CallSignalingService
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.CallsListener import org.matrix.android.sdk.api.session.call.CallsListener
@ -210,7 +210,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallHangup(hangup: CallHangupContent) { private fun onCallHangup(hangup: CallHangupContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallHangupReceived(hangup) it.onCallHangupReceived(hangup)
} }
} }
@ -218,7 +218,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallAnswer(answer: CallAnswerContent) { private fun onCallAnswer(answer: CallAnswerContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallAnswerReceived(answer) it.onCallAnswerReceived(answer)
} }
} }
@ -226,7 +226,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallManageByOtherSession(callId: String) { private fun onCallManageByOtherSession(callId: String) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallManagedByOtherSession(callId) it.onCallManagedByOtherSession(callId)
} }
} }
@ -237,7 +237,7 @@ internal class DefaultCallSignalingService @Inject constructor(
if (incomingCall.otherUserId == userId) return if (incomingCall.otherUserId == userId) return
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallInviteReceived(incomingCall, invite) it.onCallInviteReceived(incomingCall, invite)
} }
} }
@ -245,7 +245,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallIceCandidateReceived(incomingCall, candidates) it.onCallIceCandidateReceived(incomingCall, candidates)
} }
} }

View file

@ -32,7 +32,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink import okio.BufferedSink
import okio.source import okio.source
import org.greenrobot.eventbus.EventBus 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.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.ProgressRequestBody
@ -96,7 +96,7 @@ internal class FileUploader @Inject constructor(@Authenticated
inputStream.copyTo(it) inputStream.copyTo(it)
} }
return uploadFile(workingFile, filename, mimeType, progressListener).also { return uploadFile(workingFile, filename, mimeType, progressListener).also {
tryThis { workingFile.delete() } tryOrNull { workingFile.delete() }
} }
} }

View file

@ -19,12 +19,10 @@ package org.matrix.android.sdk.internal.session.content
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent 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.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments 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.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.network.ProgressRequestBody
import org.matrix.android.sdk.internal.session.DefaultFileService 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.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.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.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -56,12 +59,13 @@ private data class NewImageAttributes(
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : Always [MultipleEventSendingDispatcherWorker] * 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<UploadContentWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val localEchoIds: List<LocalEchoIdentifiers>,
val attachment: ContentAttachmentData, val attachment: ContentAttachmentData,
val isEncrypted: Boolean, val isEncrypted: Boolean,
val compressBeforeSending: Boolean, val compressBeforeSending: Boolean,
@ -73,20 +77,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
@Inject lateinit var fileService: DefaultFileService @Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject lateinit var imageCompressor: ImageCompressor @Inject lateinit var imageCompressor: ImageCompressor
@Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.success() }
.also { Timber.e("Unable to parse work parameters") }
override suspend fun doSafeWork(params: Params): Result {
Timber.v("Starting upload media work with params $params") 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 // Just defensive code to ensure that we never have an uncaught exception that could break the queue
return try { return try {
internalDoWork(params) internalDoWork(params)
@ -96,11 +94,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} }
private suspend fun internalDoWork(params: Params): Result { override fun buildErrorParams(params: Params, message: String): Params {
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
sessionComponent.inject(this) }
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) { if (allCancelled) {
// there is no point in uploading the image! // there is no point in uploading the image!
return Result.success(inputData) return Result.success(inputData)
@ -214,18 +213,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## FileService: ERROR") Timber.e(e, "## FileService: ERROR")
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return handleFailure(params, e)
return Result.success(
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = e.localizedMessage
)
)
)
} finally { } finally {
// Delete all temporary files // Delete all temporary files
filesToDelete.forEach { 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, private suspend fun handleSuccess(params: Params,
attachmentUrl: String, attachmentUrl: String,
encryptedFileInfo: EncryptedFileInfo?, encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String?, thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?, thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Result { newImageAttributes: NewImageAttributes?): Result {
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
params.localEchoIds.forEach {
updateEvent(it.eventId, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
}
val updatedEvents = params.events val sendParams = MultipleEventSendingDispatcherWorker.Params(
.map { sessionId = params.sessionId,
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) localEchoIds = params.localEchoIds,
} isEncrypted = params.isEncrypted
)
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams)).also { return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
} }
} }
private fun updateEvent(event: Event, private suspend fun updateEvent(eventId: String,
url: String, url: String,
encryptedFileInfo: EncryptedFileInfo?, encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String? = null, thumbnailUrl: String? = null,
thumbnailEncryptedFileInfo: EncryptedFileInfo?, thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Event { newImageAttributes: NewImageAttributes?) {
val messageContent: MessageContent = event.content.toModel() ?: return event localEchoRepository.updateEcho(eventId) { _, event ->
val updatedContent = when (messageContent) { val messageContent: MessageContent? = event.asDomain().content.toModel()
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) val updatedContent = when (messageContent) {
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes)
is MessageFileContent -> messageContent.update(url, encryptedFileInfo) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
else -> messageContent 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) { private fun notifyTracker(params: Params, function: (String) -> Unit) {
params.events params.localEchoIds.forEach { function.invoke(it.eventId) }
.mapNotNull { it.eventId }
.forEach { eventId -> function.invoke(eventId) }
} }
private fun MessageImageContent.update(url: String, private fun MessageImageContent.update(url: String,

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.download
import android.os.Handler import android.os.Handler
import android.os.Looper 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.api.session.file.ContentDownloadStateTracker
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
@ -76,7 +76,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
Timber.v("## DL Progress Error code:$errorCode") Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach { 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) { private fun updateState(url: String, state: ContentDownloadStateTracker.State) {
states[url] = state states[url] = state
listeners[url]?.forEach { listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(state) } tryOrNull { it.onDownloadStateUpdate(state) }
} }
} }
} }

View file

@ -18,20 +18,19 @@
package org.matrix.android.sdk.internal.session.group package org.matrix.android.sdk.internal.session.group
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.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 import javax.inject.Inject
/** /**
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : None * Possible next worker : None
*/ */
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class GetGroupDataWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<GetGroupDataWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -41,13 +40,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
@Inject lateinit var getGroupDataTask: GetGroupDataTask @Inject lateinit var getGroupDataTask: GetGroupDataTask
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() override suspend fun doSafeWork(params: Params): Result {
sessionComponent.inject(this)
return runCatching { return runCatching {
getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
}.fold( }.fold(
@ -55,4 +52,8 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
{ Result.retry() } { Result.retry() }
) )
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View file

@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleRegistry
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.SessionParams 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.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.events.model.toModel 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 // Url has changed, we have to reset our store, update internal configuration and notify listeners
identityStore.setUrl(baseUrl) identityStore.setUrl(baseUrl)
updateIdentityAPI(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?) { private suspend fun updateAccountData(url: String?) {
// Also notify the listener // Also notify the listener
withContext(coroutineDispatchers.main) { withContext(coroutineDispatchers.main) {
listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
} }
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams(

View file

@ -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.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +40,7 @@ internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val p
Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids")
// Store the list in DB // Store the list in DB
monarchy.writeAsync { realm -> monarchy.awaitTransaction { realm ->
realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm()
accountThreePidsResponse.threePids?.forEach { accountThreePidsResponse.threePids?.forEach {
val entity = UserThreePidEntity( val entity = UserThreePidEntity(

View file

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.session.pushers package org.matrix.android.sdk.internal.session.pushers
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.mapper.toEntity 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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest 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.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.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 import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<AddHttpPusherWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -50,14 +48,11 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
@Inject @SessionDatabase lateinit var monarchy: Monarchy @Inject @SessionDatabase lateinit var monarchy: Monarchy
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val pusher = params.pusher val pusher = params.pusher
if (pusher.pushKey.isBlank()) { 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) { private suspend fun setPusher(pusher: JsonPusher) {
executeRequest<Unit>(eventBus) { executeRequest<Unit>(eventBus) {
apiCall = pushersAPI.setPusher(pusher) apiCall = pushersAPI.setPusher(pusher)

View file

@ -202,13 +202,13 @@ internal class DefaultRelationService @AssistedInject constructor(
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(sessionId, event, keepKeys) val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true) return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
} }
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { 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) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }

View file

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.relation package org.matrix.android.sdk.internal.session.room.relation
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus 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.ReactionContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo 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.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.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.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.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 import javax.inject.Inject
// TODO This is not used. Delete? // TODO This is not used. Delete?
internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class SendRelationWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<SendRelationWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val roomId: String, val roomId: String,
val event: Event, val eventId: String,
val relationType: String? = null, val relationType: String? = null,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) { override suspend fun doSafeWork(params: Params): Result {
// Transmit the error val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
return Result.success(inputData) if (localEvent?.eventId == null) {
.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) {
return Result.failure() return Result.failure()
} }
val relationContent = localEvent.content.toModel<ReactionContent>() val relationContent = localEvent.content.toModel<ReactionContent>()
@ -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) { private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) {
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.sendRelation( apiCall = roomAPI.sendRelation(

View file

@ -336,7 +336,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter // Same parameter
return EncryptEventWorker.Params(sessionId, event) return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
.let { WorkerParamsFactory.toData(it) } .let { WorkerParamsFactory.toData(it) }
.let { .let {
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -360,7 +360,10 @@ internal class DefaultSendService @AssistedInject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
isRoomEncrypted: Boolean, isRoomEncrypted: Boolean,
compressBeforeSending: Boolean): OneTimeWorkRequest { 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) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()

View file

@ -18,21 +18,23 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.crypto.CryptoService 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.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.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendState 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.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult 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.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.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -41,12 +43,12 @@ import javax.inject.Inject
* Possible next worker : Always [SendEventWorker] * Possible next worker : Always [SendEventWorker]
*/ */
internal class EncryptEventWorker(context: Context, params: WorkerParameters) internal class EncryptEventWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<EncryptEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, 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) */ /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
val keepKeys: List<String>? = null, val keepKeys: List<String>? = null,
override val lastFailureMessage: String? = 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 localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
Timber.v("Start Encrypt work") injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}") override suspend fun doSafeWork(params: Params): Result {
if (params.lastFailureMessage != null) { Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
// 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() val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
sessionComponent.inject(this) if (localEvent?.eventId == null) {
val localEvent = params.event
if (localEvent.eventId == null) {
return Result.success() return Result.success()
} }
@ -106,15 +99,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
modifiedContent[toKeep] = it 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 // Better handling of local echo, to avoid decrypting transition on remote echo
// Should I only do it for text messages? // Should I only do it for text messages?
if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
val decryptionLocalEcho = MXEventDecryptionResult( MXEventDecryptionResult(
clearEvent = Event( clearEvent = Event(
type = localEvent.type, type = localEvent.type,
content = localEvent.content, content = localEvent.content,
@ -124,10 +112,18 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
senderCurve25519Key = result.eventContent["sender_key"] as? String, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint() 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)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} else { } else {
val sendState = when (error) { 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! // always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params( val nextWorkerParams = SendEventWorker.Params(
sessionId = params.sessionId, sessionId = params.sessionId,
event = localEvent, eventId = localEvent.eventId,
lastFailureMessage = error?.localizedMessage ?: "Error" lastFailureMessage = error?.localizedMessage ?: "Error"
) )
return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View file

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

View file

@ -18,8 +18,8 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.greenrobot.eventbus.EventBus 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.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.model.message.MessageType
import org.matrix.android.sdk.api.session.room.send.SendState 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.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.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.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.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.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity 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.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater 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.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val realmSessionProvider: RealmSessionProvider, private val realmSessionProvider: RealmSessionProvider,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus, private val eventBus: EventBus,
@ -76,12 +78,12 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
val timelineEvent = timelineEventMapper.map(timelineEventEntity) val timelineEvent = timelineEventMapper.map(timelineEventEntity)
eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent))
monarchy.writeAsync { realm -> taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply {
this.insertType = EventInsertType.LOCAL_ECHO this.insertType = EventInsertType.LOCAL_ECHO
} }
realm.insert(eventInsertEntity) 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) roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.updateSendingInformation(realm, roomId) roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
@ -89,30 +91,41 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
fun updateSendState(eventId: String, sendState: SendState) { fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") 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() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { block(realm, sendingEventEntity)
// If already synced, do not put as sent
} else {
sendingEventEntity.sendState = sendState
}
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
} }
} }
} }
fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { fun updateEchoAsync(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) {
monarchy.writeAsync { realm -> taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
sendingEventEntity.type = EventType.ENCRYPTED block(realm, sendingEventEntity)
sendingEventEntity.content = ContentMapper.map(encryptedContent)
sendingEventEntity.setDecryptionResult(mxEventDecryptionResult)
} }
} }
} }
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) { suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
deleteFailedEcho(roomId, localEcho.eventId) deleteFailedEcho(roomId, localEcho.eventId)
} }
@ -150,7 +163,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES) return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
} }
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> { fun getAllEventsWithStates(roomId: String, states: List<SendState>): List<TimelineEvent> {
return realmSessionProvider.withRealm { realm -> return realmSessionProvider.withRealm { realm ->
TimelineEventEntity TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, states) .findAllInRoomWithSendStates(realm, roomId, states)

View file

@ -19,18 +19,17 @@ package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.WorkManagerProvider 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.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon 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.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory 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 org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit 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 * Possible next worker : None, but it will post new work to send events, encrypted or not
*/ */
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<MultipleEventSendingDispatcherWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val localEchoIds: List<LocalEchoIdentifiers>,
val isEncrypted: Boolean, val isEncrypted: Boolean,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@ -57,46 +56,48 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
@Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun doOnError(params: Params): Result {
Timber.v("## SendEvent: Start dispatch sending multiple event work") params.localEchoIds.forEach { localEchoIds ->
val params = WorkerParamsFactory.fromData<Params>(inputData) localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED)
?: 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}") }
} }
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 // 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) { if (params.isEncrypted) {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING) localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
val encryptWork = createEncryptEventWork(params.sessionId, event, true) val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
// Note that event will be replaced by the result of the previous work // Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(params.sessionId, event, false) val sendWork = createSendEventWork(params.sessionId, eventId, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
} else { } else {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING) localEchoRepository.updateSendState(eventId, SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
val sendWork = createSendEventWork(params.sessionId, event, true) val sendWork = createSendEventWork(params.sessionId, eventId, true)
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) timelineSendEventWorkCommon.postWork(roomId, sendWork)
} }
} }
return Result.success() return Result.success()
} }
private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { override fun buildErrorParams(params: Params, message: String): Params {
val params = EncryptEventWorker.Params(sessionId, event) 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) val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -107,8 +108,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
.build() .build()
} }
private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)

View file

@ -17,24 +17,24 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.internal.network.executeRequest 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.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory 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 import javax.inject.Inject
/** /**
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : None * Possible next worker : None
*/ */
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class RedactEventWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<RedactEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -49,20 +49,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: 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 suspend fun doSafeWork(params: Params): Result {
val eventId = params.eventId val eventId = params.eventId
return runCatching { return runCatching {
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(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)
}
} }

View file

@ -56,7 +56,7 @@ internal class RoomEventSender @Inject constructor(
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(sessionId, event) val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -68,7 +68,7 @@ internal class RoomEventSender @Inject constructor(
} }
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { 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) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)

View file

@ -18,19 +18,19 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.shouldBeRetried 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.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.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.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.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -42,35 +42,29 @@ import javax.inject.Inject
*/ */
internal class SendEventWorker(context: Context, internal class SendEventWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
override val lastFailureMessage: String? = null, override val lastFailureMessage: String? = null,
val event: Event? = null, val eventId: String
// Keep for compat at the moment, will be removed later
val eventId: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.success() }
.also { Timber.e("## SendEvent: Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.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) { if (event?.eventId == null || event.roomId == null) {
// Old way of sending localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
if (params.eventId != null) {
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
}
return Result.success() return Result.success()
.also { Timber.e("Work cancelled due to bad input data") } .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?) { private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
localEchoRepository.updateSendState(eventId, SendState.SENDING) localEchoRepository.updateSendState(eventId, SendState.SENDING)
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {

View file

@ -29,6 +29,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType 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.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -333,12 +334,22 @@ internal class DefaultTimeline(
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean { private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex -> return tryOrNull {
// Update the relation of existing event builtEventsIdMap[eventId]?.let { builtIndex ->
builtEvents[builtIndex]?.let { te -> // Update the relation of existing event
builtEvents[builtIndex] = builder(te) builtEvents[builtIndex]?.let { te ->
true 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 } ?: false
} }
@ -413,14 +424,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): State { private fun getState(direction: Timeline.Direction): State {
return when (direction) { return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get()
} }
} }
private fun updateState(direction: Timeline.Direction, update: (State) -> State) { private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
val stateReference = when (direction) { val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState Timeline.Direction.BACKWARDS -> backwardsState
} }
val currentValue = stateReference.get() val currentValue = stateReference.get()
@ -489,7 +500,8 @@ internal class DefaultTimeline(
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
postSnapshot = rebuildEvent(eventId) { postSnapshot = rebuildEvent(eventId) {
buildTimelineEvent(eventEntity) val builtEvent = buildTimelineEvent(eventEntity)
listOf(builtEvent).filterEventsWithSettings().firstOrNull()
} || postSnapshot } || postSnapshot
} }
} }
@ -730,10 +742,10 @@ internal class DefaultTimeline(
return object : MatrixCallback<TokenChunkEventPersistor.Result> { return object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) { when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> { TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request") Timber.v("Success fetching $limit items $direction from pagination request")
} }
TokenChunkEventPersistor.Result.REACHED_END -> { TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot() postSnapshot()
} }
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
@ -775,9 +787,8 @@ internal class DefaultTimeline(
} }
if (!filterEdits) return@filter false if (!filterEdits) return@filter false
val filterRedacted = !settings.filters.filterRedacted || it.root.isRedacted() val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted()
!filterRedacted
filterRedacted
} }
} }

View file

@ -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. * 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) { private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) {
val groupIds = ArrayList<String>() val groupIds = ArrayList<String>()

View file

@ -18,18 +18,18 @@ package org.matrix.android.sdk.internal.session.sync.job
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker 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.session.sync.SyncTask
import org.matrix.android.sdk.internal.task.TaskExecutor 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.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -43,7 +43,7 @@ private const val DEFAULT_DELAY_TIMEOUT = 30_000L
*/ */
internal class SyncWorker(context: Context, internal class SyncWorker(context: Context,
workerParameters: WorkerParameters workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) { ) : SessionSafeCoroutineWorker<SyncWorker.Params>(context, workerParameters, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -59,14 +59,13 @@ internal class SyncWorker(context: Context,
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
@Inject lateinit var workManagerProvider: WorkManagerProvider @Inject lateinit var workManagerProvider: WorkManagerProvider
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
Timber.i("Sync work starting") injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") } 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 { return runCatching {
doSync(params.timeout) doSync(params.timeout)
}.fold( }.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) { private suspend fun doSync(timeout: Long) {
val taskParams = SyncTask.Params(timeout * 1000) val taskParams = SyncTask.Params(timeout * 1000)
syncTask.execute(taskParams) syncTask.execute(taskParams)

View file

@ -202,6 +202,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
stateKey = QueryStringValue.NoCondition stateKey = QueryStringValue.NoCondition
) )
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
} }
} }

View file

@ -18,9 +18,9 @@
package org.matrix.android.sdk.internal.util package org.matrix.android.sdk.internal.util
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.internal.database.awaitTransaction
import io.realm.Realm import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import org.matrix.android.sdk.internal.database.awaitTransaction
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T { internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm: Realm) -> T): T {

View file

@ -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<PARAM : SessionWorkerParams>(
context: Context,
workerParameters: WorkerParameters,
private val paramClass: Class<PARAM>
) : 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 }
}
}
}

View file

@ -18,12 +18,14 @@
package org.matrix.android.sdk.internal.worker package org.matrix.android.sdk.internal.worker
import androidx.work.Data 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.di.MoshiProvider
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
internal object WorkerParamsFactory { 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 // We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row
// and we lost typing information doing so. // 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. // 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() .build()
} }
const val KEY = "WORKER_PARAMS_JSON" private const val KEY = "WORKER_PARAMS_JSON"
inline fun <reified T> toData(params: T): Data { inline fun <reified T> toData(params: T) = toData(T::class.java, params)
val adapter = moshi.adapter(T::class.java)
fun <T> toData(clazz: Class<T>, params: T): Data {
val adapter = moshi.adapter(clazz)
val json = adapter.toJson(params) val json = adapter.toJson(params)
return Data.Builder().putString(KEY, json).build() return Data.Builder().putString(KEY, json).build()
} }
inline fun <reified T> fromData(data: Data): T? { inline fun <reified T> fromData(data: Data) = fromData(T::class.java, data)
fun <T> fromData(clazz: Class<T>, data: Data): T? = tryOrNull("Unable to parse work parameters") {
val json = data.getString(KEY) val json = data.getString(KEY)
return if (json == null) { return if (json == null) {
null null
} else { } else {
val adapter = moshi.adapter(T::class.java) val adapter = moshi.adapter(clazz)
adapter.fromJson(json) adapter.fromJson(json)
} }
} }

View file

@ -151,21 +151,21 @@
<string name="notice_power_level_changed">%1$s alterou o nível de permissão de %2$s.</string> <string name="notice_power_level_changed">%1$s alterou o nível de permissão de %2$s.</string>
<string name="notice_power_level_diff">%1$s de %2$s para %3$s</string> <string name="notice_power_level_diff">%1$s de %2$s para %3$s</string>
<string name="initial_sync_start_importing_account">Primeira sincronização: <string name="initial_sync_start_importing_account">Primeira sincronização:
\nImportando a conta…</string> \nImportando a conta…</string>
<string name="initial_sync_start_importing_account_crypto">Primeira sincronização: <string name="initial_sync_start_importing_account_crypto">Primeira sincronização:
\nImportando as chaves de criptografia</string> \nImportando as chaves de criptografia</string>
<string name="initial_sync_start_importing_account_rooms">Primeira sincronização: <string name="initial_sync_start_importing_account_rooms">Primeira sincronização:
\nImportando as salas</string> \nImportando as salas</string>
<string name="initial_sync_start_importing_account_joined_rooms">Primeira sincronização: <string name="initial_sync_start_importing_account_joined_rooms">Primeira sincronização:
\nImportando as salas em que você entrou</string> \nImportando as salas em que você entrou</string>
<string name="initial_sync_start_importing_account_invited_rooms">Primeira sincronização: <string name="initial_sync_start_importing_account_invited_rooms">Primeira sincronização:
\nImportando as salas em que você foi convidado</string> \nImportando as salas em que você foi convidado</string>
<string name="initial_sync_start_importing_account_left_rooms">Primeira sincronização: <string name="initial_sync_start_importing_account_left_rooms">Primeira sincronização:
\nImportando as salas em que você saiu</string> \nImportando as salas em que você saiu</string>
<string name="initial_sync_start_importing_account_groups">Primeira sincronização: <string name="initial_sync_start_importing_account_groups">Primeira sincronização:
\nImportando as comunidades</string> \nImportando as comunidades</string>
<string name="initial_sync_start_importing_account_data">Primeira sincronização: <string name="initial_sync_start_importing_account_data">Primeira sincronização:
\nImportando os dados da conta</string> \nImportando os dados da conta</string>
<string name="event_status_sending_message">Enviando mensagem…</string> <string name="event_status_sending_message">Enviando mensagem…</string>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Generated file, do not edit -->
<string name="verification_emoji_dog">Cachorro</string>
<string name="verification_emoji_cat">Gato</string>
<string name="verification_emoji_lion">Leão</string>
<string name="verification_emoji_horse">Cavalo</string>
<string name="verification_emoji_unicorn">Unicórnio</string>
<string name="verification_emoji_pig">Porco</string>
<string name="verification_emoji_elephant">Elefante</string>
<string name="verification_emoji_rabbit">Coelho</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Galo</string>
<string name="verification_emoji_penguin">Pinguim</string>
<string name="verification_emoji_turtle">Tartaruga</string>
<string name="verification_emoji_fish">Peixe</string>
<string name="verification_emoji_octopus">Polvo</string>
<string name="verification_emoji_butterfly">Borboleta</string>
<string name="verification_emoji_flower">Flor</string>
<string name="verification_emoji_tree">Árvore</string>
<string name="verification_emoji_cactus">Cacto</string>
<string name="verification_emoji_mushroom">Cogumelo</string>
<string name="verification_emoji_globe">Globo</string>
<string name="verification_emoji_moon">Lua</string>
<string name="verification_emoji_cloud">Nuvem</string>
<string name="verification_emoji_fire">Fogo</string>
<string name="verification_emoji_banana">Banana</string>
<string name="verification_emoji_apple">Maçã</string>
<string name="verification_emoji_strawberry">Morango</string>
<string name="verification_emoji_corn">Milho</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Bolo</string>
<string name="verification_emoji_heart">Coração</string>
<string name="verification_emoji_smiley">Sorriso</string>
<string name="verification_emoji_robot">Robô</string>
<string name="verification_emoji_hat">Chapéu</string>
<string name="verification_emoji_glasses">Óculos</string>
<string name="verification_emoji_spanner">Chave inglesa</string>
<string name="verification_emoji_santa">Papai-noel</string>
<string name="verification_emoji_thumbs_up">Joinha</string>
<string name="verification_emoji_umbrella">Guarda-chuva</string>
<string name="verification_emoji_hourglass">Ampulheta</string>
<string name="verification_emoji_clock">Relógio</string>
<string name="verification_emoji_gift">Presente</string>
<string name="verification_emoji_light_bulb">Lâmpada</string>
<string name="verification_emoji_book">Livro</string>
<string name="verification_emoji_pencil">Lápis</string>
<string name="verification_emoji_paperclip">Clipe de papel</string>
<string name="verification_emoji_scissors">Tesoura</string>
<string name="verification_emoji_lock">Cadeado</string>
<string name="verification_emoji_key">Chave</string>
<string name="verification_emoji_hammer">Martelo</string>
<string name="verification_emoji_telephone">Telefone</string>
<string name="verification_emoji_flag">Bandeira</string>
<string name="verification_emoji_train">Trem</string>
<string name="verification_emoji_bicycle">Bicicleta</string>
<string name="verification_emoji_aeroplane">Avião</string>
<string name="verification_emoji_rocket">Foguete</string>
<string name="verification_emoji_trophy">Troféu</string>
<string name="verification_emoji_ball">Bola</string>
<string name="verification_emoji_guitar">Guitarra</string>
<string name="verification_emoji_trumpet">Trombeta</string>
<string name="verification_emoji_bell">Sino</string>
<string name="verification_emoji_anchor">Âncora</string>
<string name="verification_emoji_headphones">Fones de ouvido</string>
<string name="verification_emoji_folder">Pasta</string>
<string name="verification_emoji_pin">Alfinete</string>
</resources>

View file

@ -27,8 +27,8 @@
<string name="notice_room_ban_by_you">Du bannade %1$s</string> <string name="notice_room_ban_by_you">Du bannade %1$s</string>
<string name="notice_room_withdraw">%1$s drog tillbaka inbjudan för %2$s</string> <string name="notice_room_withdraw">%1$s drog tillbaka inbjudan för %2$s</string>
<string name="notice_room_withdraw_by_you">Du drog tillbaka inbjudan för %1$s</string> <string name="notice_room_withdraw_by_you">Du drog tillbaka inbjudan för %1$s</string>
<string name="notice_avatar_url_changed">%1$s ändrade sin avatar</string> <string name="notice_avatar_url_changed">%1$s bytte sin avatar</string>
<string name="notice_avatar_url_changed_by_you">Du ändrade din avatar</string> <string name="notice_avatar_url_changed_by_you">Du bytte din avatar</string>
<string name="notice_display_name_set">%1$s satte sitt visningsnamn till %2$s</string> <string name="notice_display_name_set">%1$s satte sitt visningsnamn till %2$s</string>
<string name="notice_display_name_set_by_you">Du satte ditt visningsnamn till %1$s</string> <string name="notice_display_name_set_by_you">Du satte ditt visningsnamn till %1$s</string>
<string name="notice_display_name_changed_from">%1$s bytte sitt visningsnamn från %2$s till %3$s</string> <string name="notice_display_name_changed_from">%1$s bytte sitt visningsnamn från %2$s till %3$s</string>

View file

@ -27,7 +27,7 @@
<string name="verification_emoji_banana">Banan</string> <string name="verification_emoji_banana">Banan</string>
<string name="verification_emoji_apple">Äpple</string> <string name="verification_emoji_apple">Äpple</string>
<string name="verification_emoji_strawberry">Jordgubbe</string> <string name="verification_emoji_strawberry">Jordgubbe</string>
<string name="verification_emoji_corn">Majskolv</string> <string name="verification_emoji_corn">Majs</string>
<string name="verification_emoji_pizza">Pizza</string> <string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Tårta</string> <string name="verification_emoji_cake">Tårta</string>
<string name="verification_emoji_heart">Hjärta</string> <string name="verification_emoji_heart">Hjärta</string>
@ -41,7 +41,7 @@
<string name="verification_emoji_umbrella">Paraply</string> <string name="verification_emoji_umbrella">Paraply</string>
<string name="verification_emoji_hourglass">Timglas</string> <string name="verification_emoji_hourglass">Timglas</string>
<string name="verification_emoji_clock">Klocka</string> <string name="verification_emoji_clock">Klocka</string>
<string name="verification_emoji_gift">Paket</string> <string name="verification_emoji_gift">Present</string>
<string name="verification_emoji_light_bulb">Lampa</string> <string name="verification_emoji_light_bulb">Lampa</string>
<string name="verification_emoji_book">Bok</string> <string name="verification_emoji_book">Bok</string>
<string name="verification_emoji_pencil">Penna</string> <string name="verification_emoji_pencil">Penna</string>
@ -52,7 +52,7 @@
<string name="verification_emoji_hammer">Hammare</string> <string name="verification_emoji_hammer">Hammare</string>
<string name="verification_emoji_telephone">Telefon</string> <string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Flagga</string> <string name="verification_emoji_flag">Flagga</string>
<string name="verification_emoji_train">Ånglok</string> <string name="verification_emoji_train">Tåg</string>
<string name="verification_emoji_bicycle">Cykel</string> <string name="verification_emoji_bicycle">Cykel</string>
<string name="verification_emoji_aeroplane">Flygplan</string> <string name="verification_emoji_aeroplane">Flygplan</string>
<string name="verification_emoji_rocket">Raket</string> <string name="verification_emoji_rocket">Raket</string>

View file

@ -7,4 +7,62 @@
<string name="verification_emoji_horse"></string> <string name="verification_emoji_horse"></string>
<string name="verification_emoji_unicorn">独角兽</string> <string name="verification_emoji_unicorn">独角兽</string>
<string name="verification_emoji_pig"></string> <string name="verification_emoji_pig"></string>
<string name="verification_emoji_elephant">大象</string>
<string name="verification_emoji_rabbit">兔子</string>
<string name="verification_emoji_panda">熊猫</string>
<string name="verification_emoji_rooster">公鸡</string>
<string name="verification_emoji_penguin">企鹅</string>
<string name="verification_emoji_turtle">乌龟</string>
<string name="verification_emoji_fish"></string>
<string name="verification_emoji_octopus">章鱼</string>
<string name="verification_emoji_butterfly">蝴蝶</string>
<string name="verification_emoji_flower"></string>
<string name="verification_emoji_tree"></string>
<string name="verification_emoji_cactus">仙人掌</string>
<string name="verification_emoji_mushroom">蘑菇</string>
<string name="verification_emoji_globe">地球</string>
<string name="verification_emoji_moon">月亮</string>
<string name="verification_emoji_cloud"></string>
<string name="verification_emoji_fire"></string>
<string name="verification_emoji_banana">香蕉</string>
<string name="verification_emoji_apple">苹果</string>
<string name="verification_emoji_strawberry">草莓</string>
<string name="verification_emoji_corn">玉米</string>
<string name="verification_emoji_pizza">披萨</string>
<string name="verification_emoji_cake">蛋糕</string>
<string name="verification_emoji_heart"></string>
<string name="verification_emoji_smiley">笑脸</string>
<string name="verification_emoji_robot">机器人</string>
<string name="verification_emoji_hat">帽子</string>
<string name="verification_emoji_glasses">眼镜</string>
<string name="verification_emoji_spanner">扳手</string>
<string name="verification_emoji_santa">圣诞老人</string>
<string name="verification_emoji_thumbs_up"></string>
<string name="verification_emoji_umbrella"></string>
<string name="verification_emoji_hourglass">沙漏</string>
<string name="verification_emoji_clock">时钟</string>
<string name="verification_emoji_gift">礼物</string>
<string name="verification_emoji_light_bulb">灯泡</string>
<string name="verification_emoji_book"></string>
<string name="verification_emoji_pencil">铅笔</string>
<string name="verification_emoji_paperclip">回形针</string>
<string name="verification_emoji_scissors">剪刀</string>
<string name="verification_emoji_lock"></string>
<string name="verification_emoji_key">钥匙</string>
<string name="verification_emoji_hammer">锤子</string>
<string name="verification_emoji_telephone">电话</string>
<string name="verification_emoji_flag">旗帜</string>
<string name="verification_emoji_train">火车</string>
<string name="verification_emoji_bicycle">自行车</string>
<string name="verification_emoji_aeroplane">飞机</string>
<string name="verification_emoji_rocket">火箭</string>
<string name="verification_emoji_trophy">奖杯</string>
<string name="verification_emoji_ball"></string>
<string name="verification_emoji_guitar">吉他</string>
<string name="verification_emoji_trumpet">喇叭</string>
<string name="verification_emoji_bell">铃铛</string>
<string name="verification_emoji_anchor"></string>
<string name="verification_emoji_headphones">耳机</string>
<string name="verification_emoji_folder">文件夹</string>
<string name="verification_emoji_pin">图钉</string>
</resources> </resources>

View file

@ -17,7 +17,7 @@ androidExtensions {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 0 ext.versionMinor = 0
ext.versionPatch = 7 ext.versionPatch = 8
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'
@ -352,7 +352,7 @@ dependencies {
implementation 'me.saket:better-link-movement-method:2.2.0' implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1' implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version" 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 // Custom Tab
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'

View file

@ -89,6 +89,7 @@ import im.vector.app.features.settings.VectorSettingsHelpAboutFragment
import im.vector.app.features.settings.VectorSettingsLabsFragment import im.vector.app.features.settings.VectorSettingsLabsFragment
import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment import im.vector.app.features.settings.VectorSettingsNotificationPreferenceFragment
import im.vector.app.features.settings.VectorSettingsNotificationsTroubleshootFragment 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.VectorSettingsPreferencesFragment
import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.app.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.app.features.settings.account.deactivation.DeactivateAccountFragment import im.vector.app.features.settings.account.deactivation.DeactivateAccountFragment
@ -284,6 +285,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsLabsFragment::class) @FragmentKey(VectorSettingsLabsFragment::class)
fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment fun bindVectorSettingsLabsFragment(fragment: VectorSettingsLabsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsPinFragment::class)
fun bindVectorSettingsPinFragment(fragment: VectorSettingsPinFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(PushRulesFragment::class) @FragmentKey(PushRulesFragment::class)

View file

@ -36,6 +36,7 @@ import im.vector.app.features.crypto.verification.IncomingVerificationRequestHan
import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.HomeRoomListDataSource 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.EventHtmlRenderer
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
@ -71,6 +72,8 @@ interface VectorComponent {
fun matrix(): Matrix fun matrix(): Matrix
fun matrixItemColorProvider(): MatrixItemColorProvider
fun sessionListener(): SessionListener fun sessionListener(): SessionListener
fun currentSession(): Session fun currentSession(): Session

View file

@ -17,12 +17,12 @@
package im.vector.app.core.extensions package im.vector.app.core.extensions
import androidx.fragment.app.FragmentTransaction 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) { inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
// Could throw and make the app crash // Could throw and make the app crash
// e.g sharedActionViewModel.observe() // e.g sharedActionViewModel.observe()
tryThis("Failed to commitTransactionNow") { tryOrNull("Failed to commitTransactionNow") {
beginTransaction().func().commitNow() beginTransaction().func().commitNow()
} }
} }

View file

@ -18,14 +18,14 @@ package im.vector.app.core.extensions
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.matrix.android.sdk.api.extensions.ensurePrefix 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 import org.matrix.android.sdk.api.session.identity.ThreePid
fun ThreePid.getFormattedValue(): String { fun ThreePid.getFormattedValue(): String {
return when (this) { return when (this) {
is ThreePid.Email -> email is ThreePid.Email -> email
is ThreePid.Msisdn -> { 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) PhoneNumberUtil.getInstance().parse(msisdn.ensurePrefix("+"), null)
} }
?.let { ?.let {

View file

@ -84,7 +84,7 @@ import im.vector.app.receivers.DebugReceiver
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable 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 org.matrix.android.sdk.api.failure.GlobalError
import timber.log.Timber import timber.log.Timber
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
@ -318,11 +318,17 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
if (requestCode == PinActivity.PIN_REQUEST_CODE) { if (requestCode == PinActivity.PIN_REQUEST_CODE) {
when (resultCode) { when (resultCode) {
Activity.RESULT_OK -> { Activity.RESULT_OK -> {
Timber.v("Pin ok, unlock app")
pinLocker.unlock() pinLocker.unlock()
// Cancel any new started PinActivity, after a screen rotation for instance
finishActivity(PinActivity.PIN_REQUEST_CODE)
} }
else -> { else -> {
pinLocker.block() if (pinLocker.getLiveState().value != PinLocker.State.UNLOCKED) {
moveTaskToBack(true) // 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() super.onPostResume()
synchronized(postResumeScheduledActions) { synchronized(postResumeScheduledActions) {
postResumeScheduledActions.forEach { postResumeScheduledActions.forEach {
tryThis { it.invoke() } tryOrNull { it.invoke() }
} }
postResumeScheduledActions.clear() postResumeScheduledActions.clear()
} }

View file

@ -56,6 +56,9 @@ abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Hold
@EpoxyAttribute @EpoxyAttribute
var itemClickAction: View.OnClickListener? = null var itemClickAction: View.OnClickListener? = null
@EpoxyAttribute
var itemLongClickAction: View.OnLongClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.titleText.setTextOrHide(title) holder.titleText.setTextOrHide(title)
@ -76,6 +79,7 @@ abstract class GenericItemWithValue : VectorEpoxyModel<GenericItemWithValue.Hold
} }
holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) }) holder.view.setOnClickListener(itemClickAction?.let { DebouncedClickListener(it) })
holder.view.setOnLongClickListener(itemLongClickAction)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View file

@ -40,7 +40,7 @@ import androidx.fragment.app.Fragment
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -530,7 +530,7 @@ fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: Strin
return null return null
} finally { } finally {
// Close resources // Close resources
tryThis { inputStream?.close() } tryOrNull { inputStream?.close() }
tryThis { outputStream?.close() } tryOrNull { outputStream?.close() }
} }
} }

View file

@ -30,8 +30,6 @@ import androidx.fragment.app.Fragment
import im.vector.app.R import im.vector.app.R
import timber.log.Timber import timber.log.Timber
private const val LOG_TAG = "PermissionUtils"
// Android M permission request code management // Android M permission request code management
private const val PERMISSIONS_GRANTED = true private const val PERMISSIONS_GRANTED = true
private const val PERMISSIONS_DENIED = !PERMISSIONS_GRANTED private const val PERMISSIONS_DENIED = !PERMISSIONS_GRANTED
@ -42,16 +40,18 @@ const val PERMISSION_CAMERA = 0x1
private const val PERMISSION_WRITE_EXTERNAL_STORAGE = 0x1 shl 1 private const val PERMISSION_WRITE_EXTERNAL_STORAGE = 0x1 shl 1
private const val PERMISSION_RECORD_AUDIO = 0x1 shl 2 private const val PERMISSION_RECORD_AUDIO = 0x1 shl 2
private const val PERMISSION_READ_CONTACTS = 0x1 shl 3 private const val PERMISSION_READ_CONTACTS = 0x1 shl 3
private const val PERMISSION_READ_EXTERNAL_STORAGE = 0x1 shl 4
// Permissions sets // Permissions sets
const val PERMISSIONS_FOR_AUDIO_IP_CALL = PERMISSION_RECORD_AUDIO const val PERMISSIONS_FOR_AUDIO_IP_CALL = PERMISSION_RECORD_AUDIO
const val PERMISSIONS_FOR_VIDEO_IP_CALL = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO const val PERMISSIONS_FOR_VIDEO_IP_CALL = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
const val PERMISSIONS_FOR_TAKING_PHOTO = PERMISSION_CAMERA or PERMISSION_WRITE_EXTERNAL_STORAGE const val PERMISSIONS_FOR_TAKING_PHOTO = PERMISSION_CAMERA
const val PERMISSIONS_FOR_MEMBERS_SEARCH = PERMISSION_READ_CONTACTS const val PERMISSIONS_FOR_MEMBERS_SEARCH = PERMISSION_READ_CONTACTS
const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
const val PERMISSIONS_FOR_READING_FILES = PERMISSION_READ_EXTERNAL_STORAGE
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
@ -67,7 +67,6 @@ const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575 const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576 const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577 const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579 const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
/** /**
@ -79,6 +78,7 @@ fun logPermissionStatuses(context: Context) {
Manifest.permission.CAMERA, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO, Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_CONTACTS) Manifest.permission.READ_CONTACTS)
Timber.v("## logPermissionStatuses() : log the permissions status used by the app") Timber.v("## logPermissionStatuses() : log the permissions status used by the app")
@ -145,7 +145,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
fragment: Fragment?, fragment: Fragment?,
requestCode: Int, requestCode: Int,
@StringRes rationaleMessage: Int @StringRes rationaleMessage: Int
): Boolean { ): Boolean {
var isPermissionGranted = false var isPermissionGranted = false
// sanity check // sanity check
@ -161,7 +161,8 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
&& PERMISSIONS_FOR_MEMBER_DETAILS != permissionsToBeGrantedBitMap && PERMISSIONS_FOR_MEMBER_DETAILS != permissionsToBeGrantedBitMap
&& PERMISSIONS_FOR_ROOM_AVATAR != permissionsToBeGrantedBitMap && PERMISSIONS_FOR_ROOM_AVATAR != permissionsToBeGrantedBitMap
&& PERMISSIONS_FOR_VIDEO_RECORDING != permissionsToBeGrantedBitMap && PERMISSIONS_FOR_VIDEO_RECORDING != permissionsToBeGrantedBitMap
&& PERMISSIONS_FOR_WRITING_FILES != permissionsToBeGrantedBitMap) { && PERMISSIONS_FOR_WRITING_FILES != permissionsToBeGrantedBitMap
&& PERMISSIONS_FOR_READING_FILES != permissionsToBeGrantedBitMap) {
Timber.w("## checkPermissions(): permissions to be granted are not supported") Timber.w("## checkPermissions(): permissions to be granted are not supported")
isPermissionGranted = false isPermissionGranted = false
} else { } else {
@ -188,6 +189,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType) updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType)
} }
if (PERMISSION_READ_EXTERNAL_STORAGE == permissionsToBeGrantedBitMap and PERMISSION_READ_EXTERNAL_STORAGE) {
val permissionType = Manifest.permission.READ_EXTERNAL_STORAGE
isRequestPermissionRequired = isRequestPermissionRequired or
updatePermissionsToBeGranted(activity, permissionListAlreadyDenied, permissionsListToBeGranted, permissionType)
}
// the contact book access is requested for any android platforms // the contact book access is requested for any android platforms
// for android M, we use the system preferences // for android M, we use the system preferences
// for android < M, we use a dedicated settings // for android < M, we use a dedicated settings

View file

@ -1,39 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.utils
import androidx.annotation.ColorRes
import im.vector.app.R
import kotlin.math.abs
@ColorRes
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
}
}

View file

@ -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_EMPTY
import im.vector.app.core.utils.PERMISSIONS_FOR_PICKING_CONTACT 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_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max import kotlin.math.max
@ -215,10 +214,10 @@ class AttachmentTypeSelectorView(context: Context,
*/ */
enum class Type(val permissionsBit: Int) { enum class Type(val permissionsBit: Int) {
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO), CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
GALLERY(PERMISSIONS_FOR_WRITING_FILES), GALLERY(PERMISSIONS_EMPTY),
FILE(PERMISSIONS_FOR_WRITING_FILES), FILE(PERMISSIONS_EMPTY),
STICKER(PERMISSIONS_EMPTY), STICKER(PERMISSIONS_EMPTY),
AUDIO(PERMISSIONS_FOR_WRITING_FILES), AUDIO(PERMISSIONS_EMPTY),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT) CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
} }
} }

View file

@ -42,17 +42,13 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.OnSnapPositionChangeListener 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.SnapOnScrollListener
import im.vector.app.core.utils.allGranted
import im.vector.app.core.utils.attachSnapHelperWithListener import im.vector.app.core.utils.attachSnapHelperWithListener
import im.vector.app.core.utils.checkPermissions
import im.vector.app.features.media.createUCropWithDefaultSettings 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.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_attachments_preview.* 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 timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -102,7 +98,7 @@ class AttachmentsPreviewFragment @Inject constructor(
handleRemoveAction() handleRemoveAction()
true true
} }
R.id.attachmentsPreviewEditAction -> { R.id.attachmentsPreviewEditAction -> {
handleEditAction() handleEditAction()
true true
} }
@ -183,22 +179,7 @@ class AttachmentsPreviewFragment @Inject constructor(
viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment) viewModel.handle(AttachmentsPreviewAction.RemoveCurrentAttachment)
} }
private fun handleEditAction() { private fun handleEditAction() = withState(viewModel) {
// check permissions
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT)) {
doHandleEditAction()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT && allGranted(grantResults)) {
doHandleEditAction()
}
}
private fun doHandleEditAction() = withState(viewModel) {
val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState val currentAttachment = it.attachments.getOrNull(it.currentAttachmentIndex) ?: return@withState
val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}") val destinationFile = File(requireContext().cacheDir, "${currentAttachment.name}_edited_image_${System.currentTimeMillis()}")
val uri = currentAttachment.queryUri val uri = currentAttachment.queryUri

View file

@ -27,7 +27,7 @@ import io.reactivex.disposables.Disposable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.ReplaySubject import io.reactivex.subjects.ReplaySubject
import org.matrix.android.sdk.api.MatrixCallback 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.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.CallsListener import org.matrix.android.sdk.api.session.call.CallsListener
@ -95,7 +95,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
val callAudioManager = CallAudioManager(context.applicationContext) { val callAudioManager = CallAudioManager(context.applicationContext) {
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onAudioDevicesChange() } tryOrNull { it.onAudioDevicesChange() }
} }
} }
@ -174,7 +174,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
set(value) { set(value) {
field = value field = value
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onCaptureStateChanged() } tryOrNull { it.onCaptureStateChanged() }
} }
} }
@ -205,7 +205,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
set(value) { set(value) {
field = value field = value
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onCurrentCallChange(value?.mxCall) } tryOrNull { it.onCurrentCallChange(value?.mxCall) }
} }
} }
@ -745,7 +745,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
} }
currentCallsListeners.forEach { currentCallsListeners.forEach {
tryThis { it.onCameraChange() } tryOrNull { it.onCameraChange() }
} }
} }
@ -771,7 +771,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
// videoCapturer?.stopCapture() // videoCapturer?.stopCapture()
videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps)
currentCaptureMode = format currentCaptureMode = format
currentCallsListeners.forEach { tryThis { it.onCaptureStateChanged() } } currentCallsListeners.forEach { tryOrNull { it.onCaptureStateChanged() } }
} }
} }

View file

@ -38,7 +38,7 @@ import org.jitsi.meet.sdk.JitsiMeetActivityInterface
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions import org.jitsi.meet.sdk.JitsiMeetConferenceOptions
import org.jitsi.meet.sdk.JitsiMeetView import org.jitsi.meet.sdk.JitsiMeetView
import org.jitsi.meet.sdk.JitsiMeetViewListener 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 java.net.URL
import javax.inject.Inject import javax.inject.Inject
@ -100,7 +100,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMeetActivityInterface, Ji
.setVideoMuted(!viewState.enableVideo) .setVideoMuted(!viewState.enableVideo)
.setUserInfo(viewState.userInfo) .setUserInfo(viewState.userInfo)
.apply { .apply {
tryThis { URL(viewState.jitsiUrl) }?.let { tryOrNull { URL(viewState.jitsiUrl) }?.let {
setServerURL(it) setServerURL(it)
} }
} }

View file

@ -27,7 +27,7 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startImportTextFromFileIntent 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 io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -84,7 +84,7 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) {
data?.data?.let { dataURI -> data?.data?.let { dataURI ->
tryThis { tryOrNull {
activity?.contentResolver?.openInputStream(dataURI) activity?.contentResolver?.openInputStream(dataURI)
?.bufferedReader() ?.bufferedReader()
?.use { it.readText() } ?.use { it.readText() }

View file

@ -37,7 +37,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.core.utils.startImportTextFromFileIntent 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 org.matrix.android.sdk.internal.crypto.keysbackup.util.isValidRecoveryKey
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText 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?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) {
data?.data?.let { dataURI -> data?.data?.let { dataURI ->
tryThis { tryOrNull {
activity?.contentResolver?.openInputStream(dataURI) activity?.contentResolver?.openInputStream(dataURI)
?.bufferedReader() ?.bufferedReader()
?.use { it.readText() } ?.use { it.readText() }

View file

@ -31,6 +31,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.ensureProtocol import im.vector.app.core.utils.ensureProtocol
import im.vector.app.features.discovery.change.SetIdentityServerFragment import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.terms.ReviewTermsActivity import im.vector.app.features.terms.ReviewTermsActivity
import org.matrix.android.sdk.api.session.identity.SharedState import org.matrix.android.sdk.api.session.identity.SharedState
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
@ -178,10 +179,6 @@ class DiscoverySettingsFragment @Inject constructor(
} }
private fun navigateToChangeIdentityServerFragment() { private fun navigateToChangeIdentityServerFragment() {
parentFragmentManager.beginTransaction() (vectorBaseActivity as? VectorSettingsActivity)?.navigateTo(SetIdentityServerFragment::class.java)
.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()
} }
} }

View file

@ -16,13 +16,11 @@
package im.vector.app.features.home package im.vector.app.features.home
import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions 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.GlideApp
import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
import im.vector.app.core.utils.getColorFromUserId import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
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.content.ContentUrlResolver
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject 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<Drawable> * This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
*/ */
class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { class AvatarRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val matrixItemColorProvider: MatrixItemColorProvider) {
companion object { companion object {
private const val THUMBNAIL_SIZE = 250 private const val THUMBNAIL_SIZE = 250
@ -51,21 +50,19 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
@UiThread @UiThread
fun render(matrixItem: MatrixItem, imageView: ImageView) { fun render(matrixItem: MatrixItem, imageView: ImageView) {
render(imageView.context, render(GlideApp.with(imageView),
GlideApp.with(imageView),
matrixItem, matrixItem,
DrawableImageViewTarget(imageView)) DrawableImageViewTarget(imageView))
} }
fun clear(imageView: ImageView) { fun clear(imageView: ImageView) {
// It can be called after recycler view is destroyed, just silently catch // 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 @UiThread
fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) { fun render(matrixItem: MatrixItem, imageView: ImageView, glideRequests: GlideRequests) {
render(imageView.context, render(glideRequests,
glideRequests,
matrixItem, matrixItem,
DrawableImageViewTarget(imageView)) DrawableImageViewTarget(imageView))
} }
@ -79,7 +76,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
displayName = mappedContact.displayName displayName = mappedContact.displayName
) )
val placeholder = getPlaceholderDrawable(imageView.context, matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
GlideApp.with(imageView) GlideApp.with(imageView)
.load(mappedContact.photoURI) .load(mappedContact.photoURI)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
@ -88,11 +85,10 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
} }
@UiThread @UiThread
fun render(context: Context, fun render(glideRequests: GlideRequests,
glideRequests: GlideRequests,
matrixItem: MatrixItem, matrixItem: MatrixItem,
target: Target<Drawable>) { target: Target<Drawable>) {
val placeholder = getPlaceholderDrawable(context, matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
buildGlideRequest(glideRequests, matrixItem.avatarUrl) buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
@ -100,7 +96,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
@AnyThread @AnyThread
@Throws @Throws
fun shortcutDrawable(context: Context, glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap { fun shortcutDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem, iconSize: Int): Bitmap {
return glideRequests return glideRequests
.asBitmap() .asBitmap()
.apply { .apply {
@ -108,7 +104,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
if (resolvedUrl != null) { if (resolvedUrl != null) {
load(resolvedUrl) load(resolvedUrl)
} else { } else {
val avatarColor = avatarColor(matrixItem, context) val avatarColor = matrixItemColorProvider.getColor(matrixItem)
load(TextDrawable.builder() load(TextDrawable.builder()
.beginConfig() .beginConfig()
.bold() .bold()
@ -130,8 +126,8 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
} }
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable { fun getPlaceholderDrawable(matrixItem: MatrixItem): Drawable {
val avatarColor = avatarColor(matrixItem, context) val avatarColor = matrixItemColorProvider.getColor(matrixItem)
return TextDrawable.builder() return TextDrawable.builder()
.beginConfig() .beginConfig()
.bold() .bold()
@ -152,11 +148,4 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver() return activeSessionHolder.getSafeActiveSession()?.contentUrlResolver()
?.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) ?.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))
}
}
} }

View file

@ -70,7 +70,7 @@ class ShortcutsHandler @Inject constructor(
.map { room -> .map { room ->
val intent = RoomDetailActivity.shortcutIntent(context, room.roomId) val intent = RoomDetailActivity.shortcutIntent(context, room.roomId)
val bitmap = try { val bitmap = try {
avatarRenderer.shortcutDrawable(context, GlideApp.with(context), room.toMatrixItem(), iconSize) avatarRenderer.shortcutDrawable(GlideApp.with(context), room.toMatrixItem(), iconSize)
} catch (failure: Throwable) { } catch (failure: Throwable) {
null null
} }

View file

@ -97,7 +97,6 @@ import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.core.utils.copyToClipboard import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.createJSonViewerStyleProvider import im.vector.app.core.utils.createJSonViewerStyleProvider
import im.vector.app.core.utils.createUIHandler 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.isValidUrl
import im.vector.app.core.utils.onPermissionResultAudioIpCall import im.vector.app.core.utils.onPermissionResultAudioIpCall
import im.vector.app.core.utils.onPermissionResultVideoIpCall 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.MessageActionsBottomSheet
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel 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.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.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem 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 vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val notificationUtils: NotificationUtils, private val notificationUtils: NotificationUtils,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) : private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider
) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
@ -610,6 +612,16 @@ class RoomDetailFragment @Inject constructor(
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId) it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
} }
withState(roomDetailViewModel) { state -> 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 matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
if (widgetsCount > 0) { if (widgetsCount > 0) {
@ -687,6 +699,8 @@ class RoomDetailFragment @Inject constructor(
// webRtcPeerConnectionManager.endCall() // webRtcPeerConnectionManager.endCall()
// safeStartCall(it, isVideoCall) // safeStartCall(it, isVideoCall)
// } // }
} else if (!state.isAllowedToStartWebRTCCall) {
showDialogWithMessage(getString(R.string.no_permissions_to_start_webrtc_call))
} else { } else {
safeStartCall(isVideoCall) safeStartCall(isVideoCall)
} }
@ -778,7 +792,7 @@ class RoomDetailFragment @Inject constructor(
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.senderInfo.disambiguatedDisplayName 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() val messageContent: MessageContent? = event.getLastMessageContent()

View file

@ -57,7 +57,7 @@ import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.NoOpMatrixCallback 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.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -181,10 +181,12 @@ class RoomDetailViewModel @AssistedInject constructor(
.subscribe { .subscribe {
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
setState { setState {
copy( copy(
canSendMessage = canSendMessage, canSendMessage = canSendMessage,
isAllowedToManageWidgets = isAllowedToManageWidgets isAllowedToManageWidgets = isAllowedToManageWidgets,
isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall
) )
} }
} }
@ -334,7 +336,7 @@ class RoomDetailViewModel @AssistedInject constructor(
val roomId: String = room.roomId val roomId: String = room.roomId
val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale) val confId = roomId.substring(1, roomId.indexOf(":") - 1) + widgetSessionId.toLowerCase(VectorLocale.applicationLocale)
val preferredJitsiDomain = tryThis { val preferredJitsiDomain = tryOrNull {
rawService.getElementWellknown(session.myUserId) rawService.getElementWellknown(session.myUserId)
?.jitsiServer ?.jitsiServer
?.preferredDomain ?.preferredDomain
@ -988,7 +990,7 @@ class RoomDetailViewModel @AssistedInject constructor(
&& mxcUrl?.startsWith("content://") ?: false && mxcUrl?.startsWith("content://") ?: false
val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false val isDownloaded = mxcUrl?.let { session.fileService().isFileInCache(it, action.messageFileContent.mimeType) } ?: false
if (isLocalSendingFile) { if (isLocalSendingFile) {
tryThis { Uri.parse(mxcUrl) }?.let { tryOrNull { Uri.parse(mxcUrl) }?.let {
_viewEvents.post(RoomDetailViewEvents.OpenFile( _viewEvents.post(RoomDetailViewEvents.OpenFile(
action.messageFileContent.mimeType, action.messageFileContent.mimeType,
it, it,

View file

@ -67,7 +67,8 @@ data class RoomDetailViewState(
val canShowJumpToReadMarker: Boolean = true, val canShowJumpToReadMarker: Boolean = true,
val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown, val changeMembershipState: ChangeMembershipState = ChangeMembershipState.Unknown,
val canSendMessage: Boolean = true, val canSendMessage: Boolean = true,
val isAllowedToManageWidgets: Boolean = false val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this( constructor(args: RoomDetailArgs) : this(

View file

@ -19,18 +19,20 @@ package im.vector.app.features.home.room.detail.timeline
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.ColorProvider 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 im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
class MessageColorProvider @Inject constructor( class MessageColorProvider @Inject constructor(
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val matrixItemColorProvider: MatrixItemColorProvider,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences) {
@ColorInt @ColorInt
fun getMemberNameTextColor(userId: String): Int { fun getMemberNameTextColor(matrixItem: MatrixItem): Int {
return colorProvider.getColor(getColorFromUserId(userId)) return matrixItemColorProvider.getColor(matrixItem)
} }
@ColorInt @ColorInt

View file

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

View file

@ -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.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime 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.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData 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.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData 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.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData 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.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType 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, class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val colorProvider: ColorProvider) { private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before // Non nullability has been tested before
@ -81,6 +81,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl, avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName, memberName = event.senderInfo.disambiguatedDisplayName,
showInformation = showInformation, showInformation = showInformation,
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(),
orderedReactionList = event.annotations?.reactionsSummary orderedReactionList = event.annotations?.reactionsSummary
// ?.filter { isSingleEmoji(it.key) } // ?.filter { isSingleEmoji(it.key) }
?.map { ?.map {

View file

@ -21,6 +21,8 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
@ -69,8 +71,14 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.avatarImageView.setOnClickListener(null) holder.avatarImageView.setOnClickListener(null)
holder.memberNameView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null)
holder.avatarImageView.visibility = View.GONE holder.avatarImageView.visibility = View.GONE
holder.memberNameView.visibility = View.GONE if (attributes.informationData.forceShowTimestamp) {
holder.timeView.visibility = View.GONE 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.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
} }
@ -85,7 +93,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
super.unbind(holder) 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) { abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)

View file

@ -32,6 +32,7 @@ data class MessageInformationData(
val avatarUrl: String?, val avatarUrl: String?,
val memberName: CharSequence? = null, val memberName: CharSequence? = null,
val showInformation: Boolean = true, val showInformation: Boolean = true,
val forceShowTimestamp: Boolean = false,
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val pollResponseAggregatedSummary: PollResponseData? = null, val pollResponseAggregatedSummary: PollResponseData? = null,

View file

@ -27,7 +27,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick 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 org.matrix.android.sdk.api.session.widgets.model.Widget
import java.net.URL import java.net.URL
@ -42,7 +42,7 @@ abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.widgetName.text = widget.name 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) { if (iconRes != null) {
holder.iconImage.isVisible = true holder.iconImage.isVisible = true
holder.iconImage.setImageResource(iconRes!!) holder.iconImage.setImageResource(iconRes!!)

View file

@ -31,7 +31,7 @@ import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.userdirectory.KnownUsersFragment import im.vector.app.features.userdirectory.KnownUsersFragment
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@ -68,7 +68,7 @@ class HomeServerCapabilitiesViewModel @AssistedInject constructor(
private fun initAdminE2eByDefault() { private fun initAdminE2eByDefault() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val adminE2EByDefault = tryThis { val adminE2EByDefault = tryOrNull {
rawService.getElementWellknown(session.myUserId) rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault() ?.isE2EByDefault()
?: true ?: true

View file

@ -53,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
@UiThread @UiThread
fun bind(textView: TextView) { fun bind(textView: TextView) {
tv = WeakReference(textView) tv = WeakReference(textView)
avatarRenderer.render(context, glideRequests, matrixItem, target) avatarRenderer.render(glideRequests, matrixItem, target)
} }
// ReplacementSpan ***************************************************************************** // ReplacementSpan *****************************************************************************
@ -99,7 +99,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
val icon = try { val icon = try {
avatarRenderer.getCachedDrawable(glideRequests, matrixItem) avatarRenderer.getCachedDrawable(glideRequests, matrixItem)
} catch (exception: Exception) { } catch (exception: Exception) {
avatarRenderer.getPlaceholderDrawable(context, matrixItem) avatarRenderer.getPlaceholderDrawable(matrixItem)
} }
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply { return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {

View file

@ -19,10 +19,12 @@ package im.vector.app.features.login
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager 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.LoginTermsFragmentArgument
import im.vector.app.features.login.terms.toLocalizedLoginTerms import im.vector.app.features.login.terms.toLocalizedLoginTerms
import im.vector.app.features.pin.UnlockedActivity 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.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -72,6 +74,13 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer) get() = supportFragmentManager.findFragmentById(R.id.loginFragmentContainer)
private val commonOption: (FragmentTransaction) -> Unit = { ft -> 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<View?>(R.id.loginLogo)
?.children
?.firstOrNull { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim) ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
} }
@ -127,7 +136,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
is LoginViewEvents.OutdatedHomeserver -> { is LoginViewEvents.OutdatedHomeserver -> {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.login_error_outdated_homeserver_title) .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) .setPositiveButton(R.string.ok, null)
.show() .show()
Unit Unit
@ -136,6 +145,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
addFragmentToBackstack(R.id.loginFragmentContainer, addFragmentToBackstack(R.id.loginFragmentContainer,
LoginServerSelectionFragment::class.java, LoginServerSelectionFragment::class.java,
option = { ft -> option = { ft ->
findViewById<View?>(R.id.loginSplashLogo)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById<View?>(R.id.loginSplashTitle)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") } findViewById<View?>(R.id.loginSplashSubmit)?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO Disabled because it provokes a flickering // TODO Disabled because it provokes a flickering
@ -262,7 +272,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedAc
super.onNewIntent(intent) super.onNewIntent(intent)
intent?.data intent?.data
?.let { tryThis { it.getQueryParameter("loginToken") } } ?.let { tryOrNull { it.getQueryParameter("loginToken") } }
?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) } ?.let { loginViewModel.handle(LoginAction.LoginWithToken(it)) }
} }

View file

@ -748,34 +748,21 @@ class LoginViewModel @AssistedInject constructor(
else -> LoginMode.Unsupported else -> LoginMode.Unsupported
} }
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) { // FIXME We should post a view event here normally?
notSupported() setState {
} else { copy(
// FIXME We should post a view event here normally? asyncHomeServerLoginFlowRequest = Uninitialized,
setState { homeServerUrl = data.homeServerUrl,
copy( loginMode = loginMode,
asyncHomeServerLoginFlowRequest = Uninitialized, loginModeSupportedTypes = data.supportedLoginTypes.toList()
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
)
} }
} }
}) })

View file

@ -39,7 +39,7 @@ import im.vector.app.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt 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 timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -109,7 +109,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
fun clear(imageView: ImageView) { fun clear(imageView: ImageView) {
// It can be called after recycler view is destroyed, just silently catch // 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 // We'd better keep ref to requestManager, but we don't have it
tryThis { tryOrNull {
GlideApp GlideApp
.with(imageView).clear(imageView) .with(imageView).clear(imageView)
} }

View file

@ -27,9 +27,9 @@ import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.VectorPreferences 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.Session
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import me.gujun.android.span.span
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -72,6 +72,8 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private val currentSession: Session? private val currentSession: Session?
get() = activeSessionDataSource.currentValue?.orNull() get() = activeSessionDataSource.currentValue?.orNull()
private var useCompleteNotificationFormat = vectorPreferences.useCompleteNotificationFormat()
/** /**
Should be called as soon as a new event is ready to be displayed. 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 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) roomEvents.add(event)
} }
} }
is InviteNotifiableEvent -> invitationEvents.add(event) is InviteNotifiableEvent -> invitationEvents.add(event)
is SimpleNotifiableEvent -> simpleEvents.add(event) is SimpleNotifiableEvent -> simpleEvents.add(event)
else -> Timber.w("Type not handled") else -> Timber.w("Type not handled")
} }
} }
@ -253,6 +255,16 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
var globalLastMessageTimestamp = 0L 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 // events have been grouped by roomId
for ((roomId, events) in roomIdToEventMap) { for ((roomId, events) in roomIdToEventMap) {
// Build the notification for the room // Build the notification for the room
@ -263,6 +275,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
continue continue
} }
simpleNotificationRoomCounter++
val roomName = events[0].roomName ?: events[0].senderName ?: "" val roomName = events[0].roomName ?: events[0].senderName ?: ""
val roomEventGroupInfo = RoomEventGroupInfo( val roomEventGroupInfo = RoomEventGroupInfo(
@ -303,6 +316,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
roomEventGroupInfo.hasSmartReplyError = true roomEventGroupInfo.hasSmartReplyError = true
} else { } else {
if (!event.isRedacted) { if (!event.isRedacted) {
simpleNotificationMessageCounter++
style.addMessage(event.body, event.timestamp, senderPerson) 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) stringProvider.getString(R.string.notification_ticker_text_group, roomName, events.last().senderName, events.last().description)
} }
val notification = notificationUtils.buildMessagesListNotification( if (useCompleteNotificationFormat) {
style, val notification = notificationUtils.buildMessagesListNotification(
roomEventGroupInfo, style,
largeBitmap, roomEventGroupInfo,
lastMessageTimestamp, largeBitmap,
myUserDisplayName, lastMessageTimestamp,
tickerText) myUserDisplayName,
tickerText)
// is there an id for this room? // is there an id for this room?
notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification) notificationUtils.showNotificationMessage(roomId, ROOM_MESSAGES_NOTIFICATION_ID, notification)
}
hasNewEvent = true hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing summaryIsNoisy = summaryIsNoisy || roomEventGroupInfo.shouldBing
@ -383,8 +399,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in invitationEvents) { for (event in invitationEvents) {
// We build a invitation notification // We build a invitation notification
if (firstTime || !event.hasBeenDisplayed) { if (firstTime || !event.hasBeenDisplayed) {
val notification = notificationUtils.buildRoomInvitationNotification(event, session.myUserId) if (useCompleteNotificationFormat) {
notificationUtils.showNotificationMessage(event.roomId, ROOM_INVITATION_NOTIFICATION_ID, notification) 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 event.hasBeenDisplayed = true // we can consider it as displayed
hasNewEvent = true hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || event.noisy summaryIsNoisy = summaryIsNoisy || event.noisy
@ -396,8 +414,10 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
for (event in simpleEvents) { for (event in simpleEvents) {
// We build a simple notification // We build a simple notification
if (firstTime || !event.hasBeenDisplayed) { if (firstTime || !event.hasBeenDisplayed) {
val notification = notificationUtils.buildSimpleEventNotification(event, session.myUserId) if (useCompleteNotificationFormat) {
notificationUtils.showNotificationMessage(event.eventId, ROOM_EVENT_NOTIFICATION_ID, notification) 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 event.hasBeenDisplayed = true // we can consider it as displayed
hasNewEvent = true hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || event.noisy summaryIsNoisy = summaryIsNoisy || event.noisy
@ -421,19 +441,76 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
if (eventList.isEmpty() || eventList.all { it.isRedacted }) { if (eventList.isEmpty() || eventList.all { it.isRedacted }) {
notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID) notificationUtils.cancelNotificationMessage(null, SUMMARY_NOTIFICATION_ID)
} else { } else {
// FIXME roomIdToEventMap.size is not correct, this is the number of rooms
val nbEvents = roomIdToEventMap.size + simpleEvents.size val nbEvents = roomIdToEventMap.size + simpleEvents.size
val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle) summaryInboxStyle.setBigContentTitle(sumTitle)
// TODO get latest event? // TODO get latest event?
.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents))
val notification = notificationUtils.buildSummaryListNotification( if (useCompleteNotificationFormat) {
summaryInboxStyle, val notification = notificationUtils.buildSummaryListNotification(
sumTitle, summaryInboxStyle,
noisy = hasNewEvent && summaryIsNoisy, sumTitle,
lastMessageTimestamp = globalLastMessageTimestamp) 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) { if (hasNewEvent && summaryIsNoisy) {
try { try {

View file

@ -772,7 +772,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
/** /**
* Build the summary notification * Build the summary notification
*/ */
fun buildSummaryListNotification(style: NotificationCompat.InboxStyle, fun buildSummaryListNotification(style: NotificationCompat.InboxStyle?,
compatSummary: String, compatSummary: String,
noisy: Boolean, noisy: Boolean,
lastMessageTimestamp: Long): Notification { lastMessageTimestamp: Long): Notification {

View file

@ -28,7 +28,6 @@ import im.vector.app.core.platform.VectorBaseActivity
class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity { class PinActivity : VectorBaseActivity(), ToolbarConfigurable, UnlockedActivity {
companion object { companion object {
const val PIN_REQUEST_CODE = 17890 const val PIN_REQUEST_CODE = 17890
fun newIntent(context: Context, args: PinArgs): Intent { fun newIntent(context: Context, args: PinArgs): Intent {

View file

@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.settings.VectorPreferences
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -42,7 +43,8 @@ data class PinArgs(
) : Parcelable ) : Parcelable
class PinFragment @Inject constructor( class PinFragment @Inject constructor(
private val pinCodeStore: PinCodeStore private val pinCodeStore: PinCodeStore,
private val vectorPreferences: VectorPreferences
) : VectorBaseFragment() { ) : VectorBaseFragment() {
private val fragmentArgs: PinArgs by args() private val fragmentArgs: PinArgs by args()
@ -53,54 +55,10 @@ class PinFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
when (fragmentArgs.pinMode) { when (fragmentArgs.pinMode) {
PinMode.CREATE -> showCreateFragment() PinMode.CREATE -> showCreateFragment()
PinMode.DELETE -> showDeleteFragment()
PinMode.AUTH -> showAuthFragment() 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() { private fun showCreateFragment() {
val createFragment = PFLockScreenFragment() val createFragment = PFLockScreenFragment()
val builder = PFFLockScreenConfiguration.Builder(requireContext()) val builder = PFFLockScreenConfiguration.Builder(requireContext())
@ -131,9 +89,8 @@ class PinFragment @Inject constructor(
val authFragment = PFLockScreenFragment() val authFragment = PFLockScreenFragment()
val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0 val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0
val builder = PFFLockScreenConfiguration.Builder(requireContext()) val builder = PFFLockScreenConfiguration.Builder(requireContext())
.setUseBiometric(true)
.setAutoShowBiometric(true) .setAutoShowBiometric(true)
.setUseBiometric(canUseBiometrics) .setUseBiometric(vectorPreferences.useBiometricsToUnlock() && canUseBiometrics)
.setAutoShowBiometric(canUseBiometrics) .setAutoShowBiometric(canUseBiometrics)
.setTitle(getString(R.string.auth_pin_title)) .setTitle(getString(R.string.auth_pin_title))
.setLeftButton(getString(R.string.auth_pin_forgot)) .setLeftButton(getString(R.string.auth_pin_forgot))

View file

@ -22,12 +22,14 @@ import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
// 2 minutes, when enabled
private const val PERIOD_OF_GRACE_IN_MS = 2 * 60 * 1000L 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. * 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. * You can force to unlock with unlock method, use it whenever the pin code has been validated.
*/ */
@Singleton @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 { enum class State {
// App is locked, can be unlock // App is locked, can be unlock
LOCKED, LOCKED,
// App is blocked and can't be unlocked as long as the app is in foreground // App is unlocked, the app can be used
BLOCKED,
// is unlocked, the app can be used
UNLOCKED UNLOCKED
} }
private val liveState = MutableLiveData<State>() private val liveState = MutableLiveData<State>()
private var isBlocked = false
private var shouldBeLocked = true private var shouldBeLocked = true
private var entersBackgroundTs = 0L private var entersBackgroundTs = 0L
@ -62,13 +62,13 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li
private fun computeState() { private fun computeState() {
GlobalScope.launch { GlobalScope.launch {
val state = if (isBlocked) { val state = if (shouldBeLocked && pinCodeStore.hasEncodedPin()) {
State.BLOCKED
} else if (shouldBeLocked && pinCodeStore.hasEncodedPin()) {
State.LOCKED State.LOCKED
} else { } else {
State.UNLOCKED State.UNLOCKED
} }
.also { Timber.v("New state: $it") }
if (liveState.value != state) { if (liveState.value != state) {
liveState.postValue(state) liveState.postValue(state)
} }
@ -81,23 +81,25 @@ class PinLocker @Inject constructor(private val pinCodeStore: PinCodeStore) : Li
computeState() computeState()
} }
fun block() {
Timber.v("Block app")
isBlocked = true
computeState()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs val timeElapsedSinceBackground = SystemClock.elapsedRealtime() - entersBackgroundTs
shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= PERIOD_OF_GRACE_IN_MS shouldBeLocked = shouldBeLocked || timeElapsedSinceBackground >= getGracePeriod()
Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background") Timber.v("App enters foreground after $timeElapsedSinceBackground ms spent in background shouldBeLocked: $shouldBeLocked")
computeState() computeState()
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun entersBackground() { fun entersBackground() {
isBlocked = false Timber.v("App enters background")
entersBackgroundTs = SystemClock.elapsedRealtime() entersBackgroundTs = SystemClock.elapsedRealtime()
} }
private fun getGracePeriod(): Long {
return if (vectorPreferences.useGracePeriod()) {
PERIOD_OF_GRACE_IN_MS
} else {
0L
}
}
} }

View file

@ -18,6 +18,5 @@ package im.vector.app.features.pin
enum class PinMode { enum class PinMode {
CREATE, CREATE,
DELETE,
AUTH AUTH
} }

View file

@ -16,12 +16,13 @@
package im.vector.app.features.raw.wellknown 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.api.raw.RawService
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? { suspend fun RawService.getElementWellknown(userId: String): ElementWellKnown? {
return awaitCallback<String> { getWellknown(userId, it) } return tryOrNull { awaitCallback<String> { getWellknown(userId, it) } }
.let { ElementWellKnownMapper.from(it) } ?.let { ElementWellKnownMapper.from(it) }
} }
fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true

View file

@ -17,6 +17,7 @@
package im.vector.app.features.raw.wellknown package im.vector.app.features.raw.wellknown
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
object ElementWellKnownMapper { object ElementWellKnownMapper {
@ -24,6 +25,6 @@ object ElementWellKnownMapper {
val adapter: JsonAdapter<ElementWellKnown> = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java) val adapter: JsonAdapter<ElementWellKnown> = MoshiProvider.providesMoshi().adapter(ElementWellKnown::class.java)
fun from(value: String): ElementWellKnown? { fun from(value: String): ElementWellKnown? {
return adapter.fromJson(value) return tryOrNull("Unable to parse well-known data") { adapter.fromJson(value) }
} }
} }

View file

@ -34,7 +34,7 @@ import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback 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.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
@ -59,7 +59,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun initAdminE2eByDefault() { private fun initAdminE2eByDefault() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
adminE2EByDefault = tryThis { adminE2EByDefault = tryOrNull {
rawService.getElementWellknown(session.myUserId) rawService.getElementWellknown(session.myUserId)
?.isE2EByDefault() ?.isE2EByDefault()
?: true ?: true

View file

@ -28,7 +28,7 @@ import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.features.homeserver.ServerUrlsRepository import im.vector.app.features.homeserver.ServerUrlsRepository
import im.vector.app.features.themes.ThemeUtils 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -166,6 +166,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
// Security // Security
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE" 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" 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 // other
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" 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<String> { fun getUnknownDeviceDismissedList(): List<String> {
return tryThis { return tryOrNull {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
}.orEmpty() }.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 { fun useFlagPinCode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false) 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 { fun backgroundSyncTimeOut(): Int {
return tryThis { return tryOrNull {
// The xml pref is saved as a string so use getString and parse // The xml pref is saved as a string so use getString and parse
defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt() defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt()
} ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS
@ -860,7 +880,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
fun backgroundSyncDelay(): Int { fun backgroundSyncDelay(): Int {
return tryThis { return tryOrNull {
// The xml pref is saved as a string so use getString and parse // The xml pref is saved as a string so use getString and parse
defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt() defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt()
} ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS

View file

@ -17,6 +17,7 @@ package im.vector.app.features.settings
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
@ -134,6 +135,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
} }
} }
fun <T: Fragment> navigateTo(fragmentClass: Class<T>) {
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 { companion object {
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }

View file

@ -37,7 +37,7 @@ import im.vector.app.core.utils.requestDisablingBatteryOptimization
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import org.matrix.android.sdk.api.MatrixCallback 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.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind import org.matrix.android.sdk.api.pushrules.RuleKind
import javax.inject.Inject import javax.inject.Inject
@ -88,7 +88,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut()) it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut())
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) { 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)) vectorPreferences.setBackgroundSyncTimeout(maxOf(0, syncTimeout))
refreshBackgroundSyncPrefs() refreshBackgroundSyncPrefs()
} }
@ -101,7 +101,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
it.summary = secondsToText(vectorPreferences.backgroundSyncDelay()) it.summary = secondsToText(vectorPreferences.backgroundSyncDelay())
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) { 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)) vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay))
refreshBackgroundSyncPrefs() refreshBackgroundSyncPrefs()
} }

Some files were not shown because too many files have changed in this diff Show more