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

View file

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

View file

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

View file

@ -17,13 +17,11 @@
package org.matrix.android.sdk.api.auth.data
// Either a list of supported login types, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String
val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
}

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.extensions
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 {
operation()
} catch (any: Throwable) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
* Must be call on the crypto coroutine thread
*
* @param megolmSessionsData megolm sessions.
* @param backUpKeys true to back up them to the homeserver.
* @param fromBackup true if the imported keys are already backed up on the server.
* @param progressListener the progress listener
* @return import room keys result
*/

View file

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

View file

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
@ -398,7 +398,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity ->
tryThis {
tryOrNull {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
}
}

View file

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

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

View file

@ -17,17 +17,17 @@
package org.matrix.android.sdk.internal.crypto.verification
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
@ -37,56 +37,56 @@ import javax.inject.Inject
*/
internal class SendVerificationMessageWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
: SessionSafeCoroutineWorker<SendVerificationMessageWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
override val sessionId: String,
val event: Event,
val eventId: String,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject
lateinit var sendVerificationMessageTask: SendVerificationMessageTask
@Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject
lateinit var cryptoService: CryptoService
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doWork(): Result {
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build()
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success(errorOutputData)
override suspend fun doSafeWork(params: Params): Result {
val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found")
val localEventId = localEvent.eventId ?: ""
val roomId = localEvent.roomId ?: ""
if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(localEventId, roomId)
Timber.e("## SendEvent: Event sending has been cancelled $localEventId")
}
}
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
val localId = params.event.eventId ?: ""
return try {
val eventId = sendVerificationMessageTask.execute(
val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params(
event = params.event,
event = localEvent,
cryptoService = cryptoService
)
)
Result.success(Data.Builder().putString(localId, eventId).build())
} catch (exception: Throwable) {
if (exception.shouldBeRetried()) {
Result.success(Data.Builder().putString(localEventId, resultEventId).build())
} catch (throwable: Throwable) {
if (throwable.shouldBeRetried()) {
Result.retry()
} else {
Result.success(errorOutputData)
buildErrorResult(params, throwable.localizedMessage ?: "error")
}
}
}
companion object {
private const val OUTPUT_KEY_FAILED = "failed"
fun hasFailed(outputData: Data): Boolean {
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
}
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
}

View file

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

View file

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

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.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
internal object ContentMapper {
private val moshi = MoshiProvider.providesMoshi()
private val adapter = moshi.adapter<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 {
adapter.fromJson(it)
if (castJsonNumbers) {
castJsonNumberMoshi
} else {
moshi
}.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).fromJson(it)
}
}
fun map(content: Content?): String? {
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
}
fun map(eventEntity: EventEntity): Event {
fun map(eventEntity: EventEntity, castJsonNumbers: Boolean = false): Event {
val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() }
?.let {
@ -69,8 +69,8 @@ internal object EventMapper {
return Event(
type = eventEntity.type,
eventId = eventEntity.eventId,
content = ContentMapper.map(eventEntity.content),
prevContent = ContentMapper.map(eventEntity.prevContent),
content = ContentMapper.map(eventEntity.content, castJsonNumbers),
prevContent = ContentMapper.map(eventEntity.prevContent, castJsonNumbers),
originServerTs = eventEntity.originServerTs,
senderId = eventEntity.sender,
stateKey = eventEntity.stateKey,
@ -96,8 +96,8 @@ internal object EventMapper {
}
}
internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this)
internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
return EventMapper.map(this, castJsonNumbers)
}
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -17,24 +17,24 @@
package org.matrix.android.sdk.internal.session.room.send
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
internal class RedactEventWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<RedactEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true)
internal data class Params(
@ -49,20 +49,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
@Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
val eventId = params.eventId
return runCatching {
executeRequest<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 {
// Same parameter
val params = EncryptEventWorker.Params(sessionId, event)
val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -68,7 +68,7 @@ internal class RoomEventSender @Inject constructor(
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)

View file

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

View file

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

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.
You can also force to refetch group data using [Group] API.
* You can also force to refetch group data using [Group] API.
*/
private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) {
val groupIds = ArrayList<String>()

View file

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

View file

@ -202,6 +202,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
stateKey = QueryStringValue.NoCondition
)
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
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.internal.database.awaitTransaction
import io.realm.Realm
import io.realm.RealmModel
import org.matrix.android.sdk.internal.database.awaitTransaction
import java.util.concurrent.atomic.AtomicReference
internal suspend fun <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
import androidx.work.Data
import com.squareup.moshi.Moshi
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
internal object WorkerParamsFactory {
val moshi by lazy {
private val moshi: Moshi by lazy {
// We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row
// and we lost typing information doing so.
// We don't want this check to be done on all adapters, so we just add it here.
@ -33,20 +35,24 @@ internal object WorkerParamsFactory {
.build()
}
const val KEY = "WORKER_PARAMS_JSON"
private const val KEY = "WORKER_PARAMS_JSON"
inline fun <reified T> toData(params: T): Data {
val adapter = moshi.adapter(T::class.java)
inline fun <reified T> toData(params: T) = toData(T::class.java, params)
fun <T> toData(clazz: Class<T>, params: T): Data {
val adapter = moshi.adapter(clazz)
val json = adapter.toJson(params)
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)
return if (json == null) {
null
} else {
val adapter = moshi.adapter(T::class.java)
val adapter = moshi.adapter(clazz)
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_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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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_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_avatar_url_changed">%1$s ändrade sin avatar</string>
<string name="notice_avatar_url_changed_by_you">Du ändrade din avatar</string>
<string name="notice_avatar_url_changed">%1$s bytte sin 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_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>

View file

@ -27,7 +27,7 @@
<string name="verification_emoji_banana">Banan</string>
<string name="verification_emoji_apple">Äpple</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_cake">Tå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_hourglass">Timglas</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_book">Bok</string>
<string name="verification_emoji_pencil">Penna</string>
@ -52,7 +52,7 @@
<string name="verification_emoji_hammer">Hammare</string>
<string name="verification_emoji_telephone">Telefon</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_aeroplane">Flygplan</string>
<string name="verification_emoji_rocket">Raket</string>

View file

@ -7,4 +7,62 @@
<string name="verification_emoji_horse"></string>
<string name="verification_emoji_unicorn">独角兽</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>

View file

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

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

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

View file

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

View file

@ -18,14 +18,14 @@ package im.vector.app.core.extensions
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.matrix.android.sdk.api.extensions.ensurePrefix
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.identity.ThreePid
fun ThreePid.getFormattedValue(): String {
return when (this) {
is ThreePid.Email -> email
is ThreePid.Msisdn -> {
tryThis(message = "Unable to parse the phone number") {
tryOrNull(message = "Unable to parse the phone number") {
PhoneNumberUtil.getInstance().parse(msisdn.ensurePrefix("+"), null)
}
?.let {

View file

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

View file

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

View file

@ -40,7 +40,7 @@ import androidx.fragment.app.Fragment
import im.vector.app.BuildConfig
import im.vector.app.R
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.GlobalScope
import kotlinx.coroutines.launch
@ -530,7 +530,7 @@ fun saveFileIntoLegacy(sourceFile: File, dstDirPath: File, outputFilename: Strin
return null
} finally {
// Close resources
tryThis { inputStream?.close() }
tryThis { outputStream?.close() }
tryOrNull { inputStream?.close() }
tryOrNull { outputStream?.close() }
}
}

View file

@ -30,8 +30,6 @@ import androidx.fragment.app.Fragment
import im.vector.app.R
import timber.log.Timber
private const val LOG_TAG = "PermissionUtils"
// Android M permission request code management
private const val PERMISSIONS_GRANTED = true
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_RECORD_AUDIO = 0x1 shl 2
private const val PERMISSION_READ_CONTACTS = 0x1 shl 3
private const val PERMISSION_READ_EXTERNAL_STORAGE = 0x1 shl 4
// Permissions sets
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_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_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
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_READING_FILES = PERMISSION_READ_EXTERNAL_STORAGE
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
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_PICK_ATTACHMENT = 576
const val PERMISSION_REQUEST_CODE_INCOMING_URI = 577
const val PERMISSION_REQUEST_CODE_PREVIEW_FRAGMENT = 578
const val PERMISSION_REQUEST_CODE_READ_CONTACTS = 579
/**
@ -79,6 +78,7 @@ fun logPermissionStatuses(context: Context) {
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_CONTACTS)
Timber.v("## logPermissionStatuses() : log the permissions status used by the app")
@ -145,7 +145,7 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
fragment: Fragment?,
requestCode: Int,
@StringRes rationaleMessage: Int
): Boolean {
): Boolean {
var isPermissionGranted = false
// sanity check
@ -161,7 +161,8 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
&& PERMISSIONS_FOR_MEMBER_DETAILS != permissionsToBeGrantedBitMap
&& PERMISSIONS_FOR_ROOM_AVATAR != 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")
isPermissionGranted = false
} else {
@ -188,6 +189,12 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
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
// for android M, we use the system preferences
// 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_FOR_PICKING_CONTACT
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.app.features.attachments.AttachmentTypeSelectorView.Callback
import kotlin.math.max
@ -215,10 +214,10 @@ class AttachmentTypeSelectorView(context: Context,
*/
enum class Type(val permissionsBit: Int) {
CAMERA(PERMISSIONS_FOR_TAKING_PHOTO),
GALLERY(PERMISSIONS_FOR_WRITING_FILES),
FILE(PERMISSIONS_FOR_WRITING_FILES),
GALLERY(PERMISSIONS_EMPTY),
FILE(PERMISSIONS_EMPTY),
STICKER(PERMISSIONS_EMPTY),
AUDIO(PERMISSIONS_FOR_WRITING_FILES),
AUDIO(PERMISSIONS_EMPTY),
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
}
}

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

View file

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

View file

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

View file

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

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

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.utils.ensureProtocol
import im.vector.app.features.discovery.change.SetIdentityServerFragment
import im.vector.app.features.settings.VectorSettingsActivity
import im.vector.app.features.terms.ReviewTermsActivity
import org.matrix.android.sdk.api.session.identity.SharedState
import org.matrix.android.sdk.api.session.identity.ThreePid
@ -178,10 +179,6 @@ class DiscoverySettingsFragment @Inject constructor(
}
private fun navigateToChangeIdentityServerFragment() {
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom, R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
.replace(R.id.vector_settings_page, SetIdentityServerFragment::class.java, null)
.addToBackStack(null)
.commit()
(vectorBaseActivity as? VectorSettingsActivity)?.navigateTo(SetIdentityServerFragment::class.java)
}
}

View file

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

View file

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

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.createJSonViewerStyleProvider
import im.vector.app.core.utils.createUIHandler
import im.vector.app.core.utils.getColorFromUserId
import im.vector.app.core.utils.isValidUrl
import im.vector.app.core.utils.onPermissionResultAudioIpCall
import im.vector.app.core.utils.onPermissionResultVideoIpCall
@ -127,6 +126,7 @@ import im.vector.app.features.home.room.detail.timeline.action.EventSharedAction
import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBottomSheet
import im.vector.app.features.home.room.detail.timeline.action.MessageSharedActionViewModel
import im.vector.app.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
@ -217,7 +217,9 @@ class RoomDetailFragment @Inject constructor(
private val vectorPreferences: VectorPreferences,
private val colorProvider: ColorProvider,
private val notificationUtils: NotificationUtils,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) :
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider
) :
VectorBaseFragment(),
TimelineEventController.Callback,
VectorInviteView.Callback,
@ -610,6 +612,16 @@ class RoomDetailFragment @Inject constructor(
it.isVisible = roomDetailViewModel.isMenuItemVisible(it.itemId)
}
withState(roomDetailViewModel) { state ->
// Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions
val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
1 -> false
2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets
}
setOf(R.id.voice_call, R.id.video_call).forEach {
menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40
}
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
if (widgetsCount > 0) {
@ -687,6 +699,8 @@ class RoomDetailFragment @Inject constructor(
// webRtcPeerConnectionManager.endCall()
// safeStartCall(it, isVideoCall)
// }
} else if (!state.isAllowedToStartWebRTCCall) {
showDialogWithMessage(getString(R.string.no_permissions_to_start_webrtc_call))
} else {
safeStartCall(isVideoCall)
}
@ -778,7 +792,7 @@ class RoomDetailFragment @Inject constructor(
// switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply {
text = event.senderInfo.disambiguatedDisplayName
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
setTextColor(matrixItemColorProvider.getColor(MatrixItem.UserItem(event.root.senderId ?: "@")))
}
val messageContent: MessageContent? = event.getLastMessageContent()

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

View file

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

View file

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

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

View file

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

View file

@ -32,6 +32,7 @@ data class MessageInformationData(
val avatarUrl: String?,
val memberName: CharSequence? = null,
val showInformation: Boolean = true,
val forceShowTimestamp: Boolean = false,
/*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = 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.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.widgets.model.Widget
import java.net.URL
@ -42,7 +42,7 @@ abstract class RoomWidgetItem : EpoxyModelWithHolder<RoomWidgetItem.Holder>() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.widgetName.text = widget.name
holder.widgetUrl.text = tryThis { URL(widget.computedUrl) }?.host ?: widget.computedUrl
holder.widgetUrl.text = tryOrNull { URL(widget.computedUrl) }?.host ?: widget.computedUrl
if (iconRes != null) {
holder.iconImage.isVisible = true
holder.iconImage.setImageResource(iconRes!!)

View file

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

View file

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

View file

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

View file

@ -748,34 +748,21 @@ class LoginViewModel @AssistedInject constructor(
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
notSupported()
} else {
// FIXME We should post a view event here normally?
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
// FIXME We should post a view event here normally?
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized,
homeServerUrl = data.homeServerUrl,
loginMode = loginMode,
loginModeSupportedTypes = data.supportedLoginTypes.toList()
)
}
if ((loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported)
|| data.isOutdatedHomeserver) {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
}
}
is LoginFlowResult.OutdatedHomeserver -> {
notSupported()
}
}
}
private fun notSupported() {
// Notify the UI
_viewEvents.post(LoginViewEvents.OutdatedHomeserver)
setState {
copy(
asyncHomeServerLoginFlowRequest = Uninitialized
)
}
}
})

View file

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

View file

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

View file

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

View file

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

View file

@ -32,6 +32,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.toast
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.settings.VectorPreferences
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -42,7 +43,8 @@ data class PinArgs(
) : Parcelable
class PinFragment @Inject constructor(
private val pinCodeStore: PinCodeStore
private val pinCodeStore: PinCodeStore,
private val vectorPreferences: VectorPreferences
) : VectorBaseFragment() {
private val fragmentArgs: PinArgs by args()
@ -53,54 +55,10 @@ class PinFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState)
when (fragmentArgs.pinMode) {
PinMode.CREATE -> showCreateFragment()
PinMode.DELETE -> showDeleteFragment()
PinMode.AUTH -> showAuthFragment()
}
}
private fun showDeleteFragment() {
val encodedPin = pinCodeStore.getEncodedPin() ?: return
val authFragment = PFLockScreenFragment()
val builder = PFFLockScreenConfiguration.Builder(requireContext())
.setUseBiometric(pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0)
.setTitle(getString(R.string.auth_pin_confirm_to_disable_title))
.setClearCodeOnError(true)
.setMode(PFFLockScreenConfiguration.MODE_AUTH)
authFragment.setConfiguration(builder.build())
authFragment.setEncodedPinCode(encodedPin)
authFragment.setLoginListener(object : PFLockScreenFragment.OnPFLockScreenLoginListener {
override fun onPinLoginFailed() {
onWrongPin()
}
override fun onBiometricAuthSuccessful() {
lifecycleScope.launch {
pinCodeStore.deleteEncodedPin()
vectorBaseActivity.setResult(Activity.RESULT_OK)
vectorBaseActivity.finish()
}
}
override fun onBiometricAuthLoginFailed() {
val remainingAttempts = pinCodeStore.onWrongBiometrics()
if (remainingAttempts <= 0) {
// Disable Biometrics
builder.setUseBiometric(false)
authFragment.setConfiguration(builder.build())
}
}
override fun onCodeInputSuccessful() {
lifecycleScope.launch {
pinCodeStore.deleteEncodedPin()
vectorBaseActivity.setResult(Activity.RESULT_OK)
vectorBaseActivity.finish()
}
}
})
replaceFragment(R.id.pinFragmentContainer, authFragment)
}
private fun showCreateFragment() {
val createFragment = PFLockScreenFragment()
val builder = PFFLockScreenConfiguration.Builder(requireContext())
@ -131,9 +89,8 @@ class PinFragment @Inject constructor(
val authFragment = PFLockScreenFragment()
val canUseBiometrics = pinCodeStore.getRemainingBiometricsAttemptsNumber() > 0
val builder = PFFLockScreenConfiguration.Builder(requireContext())
.setUseBiometric(true)
.setAutoShowBiometric(true)
.setUseBiometric(canUseBiometrics)
.setUseBiometric(vectorPreferences.useBiometricsToUnlock() && canUseBiometrics)
.setAutoShowBiometric(canUseBiometrics)
.setTitle(getString(R.string.auth_pin_title))
.setLeftButton(getString(R.string.auth_pin_forgot))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ import im.vector.app.R
import im.vector.app.core.di.DefaultSharedPreferences
import im.vector.app.features.homeserver.ServerUrlsRepository
import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.extensions.tryOrNull
import timber.log.Timber
import javax.inject.Inject
@ -166,6 +166,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
// Security
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
const val SETTINGS_SECURITY_USE_PIN_CODE_FLAG = "SETTINGS_SECURITY_USE_PIN_CODE_FLAG"
private const val SETTINGS_SECURITY_USE_BIOMETRICS_FLAG = "SETTINGS_SECURITY_USE_BIOMETRICS_FLAG"
private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG"
const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG"
// other
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@ -424,7 +427,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
fun getUnknownDeviceDismissedList(): List<String> {
return tryThis {
return tryOrNull {
defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList()
}.orEmpty()
}
@ -839,14 +842,31 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
/**
* The user enable protecting app access with pin code
* The user enable protecting app access with pin code.
* Currently we use the pin code store to know if the pin is enabled, so this is not used
*/
fun useFlagPinCode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_PIN_CODE_FLAG, false)
}
fun useBiometricsToUnlock(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_BIOMETRICS_FLAG, true)
}
fun useGracePeriod(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG, true)
}
/**
* Return true if Pin code is disabled, or if user set the settings to see full notification content
*/
fun useCompleteNotificationFormat(): Boolean {
return !useFlagPinCode()
|| defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true)
}
fun backgroundSyncTimeOut(): Int {
return tryThis {
return tryOrNull {
// The xml pref is saved as a string so use getString and parse
defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt()
} ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS
@ -860,7 +880,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
fun backgroundSyncDelay(): Int {
return tryThis {
return tryOrNull {
// The xml pref is saved as a string so use getString and parse
defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt()
} ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS

View file

@ -17,6 +17,7 @@ package im.vector.app.features.settings
import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
@ -134,6 +135,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
}
}
fun <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 {
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.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.push.fcm.FcmHelper
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleKind
import javax.inject.Inject
@ -88,7 +88,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
it.summary = secondsToText(vectorPreferences.backgroundSyncTimeOut())
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) {
val syncTimeout = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS
val syncTimeout = tryOrNull { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS
vectorPreferences.setBackgroundSyncTimeout(maxOf(0, syncTimeout))
refreshBackgroundSyncPrefs()
}
@ -101,7 +101,7 @@ class VectorSettingsNotificationPreferenceFragment @Inject constructor(
it.summary = secondsToText(vectorPreferences.backgroundSyncDelay())
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue is String) {
val syncDelay = tryThis { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
val syncDelay = tryOrNull { Integer.parseInt(newValue) } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS
vectorPreferences.setBackgroundSyncDelay(maxOf(0, syncDelay))
refreshBackgroundSyncPrefs()
}

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