Sqlite: make it work with latest develop version

This commit is contained in:
ganfra 2020-04-23 20:34:26 +02:00
parent 9a4cad1e45
commit 9903a299b9
52 changed files with 849 additions and 2577 deletions

View file

@ -53,7 +53,7 @@ allprojects {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them
kotlinOptions.allWarningsAsErrors = true
kotlinOptions.allWarningsAsErrors = false
}
}

View file

@ -38,6 +38,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.3'
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0"

View file

@ -27,11 +27,9 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.rx2.asObservable
import timber.log.Timber
class RxRoom(private val room: Room) {

View file

@ -16,8 +16,8 @@
package im.vector.matrix.android.api.session.room.timeline
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.util.Optional
import kotlinx.coroutines.flow.Flow
/**
* This interface defines methods to interact with the timeline. It's implemented at the room level.
@ -38,5 +38,5 @@ interface TimelineService {
fun getTimeLineEvent(eventId: String): TimelineEvent?
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>>
}

View file

@ -1,84 +0,0 @@
/*
* Copyright 2018 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.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
internal object AuthRealmMigration : RealmMigration {
// Current schema version
const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3")
Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
realm.schema.get("SessionParamsEntity")
?.removePrimaryKey()
?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
?.transform {
val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
val credentials = MoshiProvider.providesMoshi()
.adapter(Credentials::class.java)
.fromJson(credentialsJson)
it.set(SessionParamsEntityFields.SESSION_ID, credentials!!.sessionId())
}
?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
}
}

View file

@ -1,29 +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.matrix.android.internal.auth.db
import io.realm.annotations.RealmModule
/**
* Realm module for authentication classes
*/
@RealmModule(library = true,
classes = [
SessionParamsEntity::class,
PendingSessionEntity::class
])
internal class AuthRealmModule

View file

@ -1,50 +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.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import java.util.UUID
/**
* This class holds all pending data when creating a session, either by login or by register
*/
internal data class PendingSessionData(
val homeServerConnectionConfig: HomeServerConnectionConfig,
/* ==========================================================================================
* Common
* ========================================================================================== */
val clientSecret: String = UUID.randomUUID().toString(),
val sendAttempt: Int = 0,
/* ==========================================================================================
* For login
* ========================================================================================== */
val resetPasswordData: ResetPasswordData? = null,
/* ==========================================================================================
* For register
* ========================================================================================== */
val currentSession: String? = null,
val isRegistrationStarted: Boolean = false,
val currentThreePidData: ThreePidData? = null
)

View file

@ -1,29 +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.matrix.android.internal.auth.db
import io.realm.RealmObject
internal open class PendingSessionEntity(
var homeServerConnectionConfigJson: String = "",
var clientSecret: String = "",
var sendAttempt: Int = 0,
var resetPasswordDataJson: String? = null,
var currentSession: String? = null,
var isRegistrationStarted: Boolean = false,
var currentThreePidDataJson: String? = null
) : RealmObject()

View file

@ -1,69 +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.matrix.android.internal.auth.db
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import javax.inject.Inject
internal class PendingSessionMapper @Inject constructor(moshi: Moshi) {
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
fun map(entity: PendingSessionEntity?): PendingSessionData? {
if (entity == null) {
return null
}
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!!
val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) }
val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) }
return PendingSessionData(
homeServerConnectionConfig = homeServerConnectionConfig,
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
resetPasswordData = resetPasswordData,
currentSession = entity.currentSession,
isRegistrationStarted = entity.isRegistrationStarted,
currentThreePidData = threePidData)
}
fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
if (sessionData == null) {
return null
}
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
return PendingSessionEntity(
homeServerConnectionConfigJson = homeServerConnectionConfigJson,
clientSecret = sessionData.clientSecret,
sendAttempt = sessionData.sendAttempt,
resetPasswordDataJson = resetPasswordDataJson,
currentSession = sessionData.currentSession,
isRegistrationStarted = sessionData.isRegistrationStarted,
currentThreePidDataJson = currentThreePidDataJson
)
}
}

View file

@ -1,61 +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.matrix.android.internal.auth.db
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
@AuthDatabase
private val realmConfiguration: RealmConfiguration
) : PendingSessionStore {
override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) {
awaitTransaction(realmConfiguration) { realm ->
val entity = mapper.map(pendingSessionData)
if (entity != null) {
realm.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
realm.insert(entity)
}
}
}
override fun getPendingSessionData(): PendingSessionData? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(PendingSessionEntity::class.java)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
}
}
override suspend fun delete() {
awaitTransaction(realmConfiguration) {
it.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
}
}
}

View file

@ -1,143 +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.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.exceptions.RealmPrimaryKeyConstraintException
import timber.log.Timber
import javax.inject.Inject
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
@AuthDatabase
private val realmConfiguration: RealmConfiguration
) : SessionParamsStore {
override fun getLast(): SessionParams? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.map { mapper.map(it) }
.lastOrNull()
}
}
override fun get(sessionId: String): SessionParams? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
}
}
override fun getAll(): List<SessionParams> {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.mapNotNull { mapper.map(it) }
}
}
override suspend fun save(sessionParams: SessionParams) {
awaitTransaction(realmConfiguration) {
val entity = mapper.map(sessionParams)
if (entity != null) {
try {
it.insert(entity)
} catch (e: RealmPrimaryKeyConstraintException) {
Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials")
it.insertOrUpdate(entity)
}
}
}
}
override suspend fun setTokenInvalid(sessionId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for id $sessionId"
.let { Timber.w(it) }
.also { error(it) }
} else {
currentSessionParams.isTokenValid = false
}
}
}
override suspend fun updateCredentials(newCredentials: Credentials) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.SESSION_ID, newCredentials.sessionId())
.findAll()
.map { mapper.map(it) }
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for id ${newCredentials.sessionId()}"
.let { Timber.w(it) }
.also { error(it) }
} else {
val newSessionParams = currentSessionParams.copy(
credentials = newCredentials,
isTokenValid = true
)
val entity = mapper.map(newSessionParams)
if (entity != null) {
realm.insertOrUpdate(entity)
}
}
}
}
override suspend fun delete(sessionId: String) {
awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.SESSION_ID, sessionId)
.findAll()
.deleteAllFromRealm()
}
}
override suspend fun deleteAll() {
awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java)
.findAll()
.deleteAllFromRealm()
}
}
}

View file

@ -1,30 +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.matrix.android.internal.auth.db
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
@PrimaryKey var sessionId: String = "",
var userId: String = "",
var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
) : RealmObject()

View file

@ -1,59 +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.matrix.android.internal.auth.db
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.data.sessionId
import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
private val credentialsAdapter = moshi.adapter(Credentials::class.java)
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
fun map(entity: SessionParamsEntity?): SessionParams? {
if (entity == null) {
return null
}
val credentials = credentialsAdapter.fromJson(entity.credentialsJson)
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)
if (credentials == null || homeServerConnectionConfig == null) {
return null
}
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
}
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
if (sessionParams == null) {
return null
}
val credentialsJson = credentialsAdapter.toJson(sessionParams.credentials)
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionParams.homeServerConnectionConfig)
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null
}
return SessionParamsEntity(
sessionParams.credentials.sessionId(),
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)
}
}

View file

@ -92,7 +92,8 @@ import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionScope
@ -115,15 +116,15 @@ internal abstract class CryptoModule {
@JvmStatic
@Provides
@im.vector.matrix.android.internal.di.CryptoDatabase
@RealmCryptoDatabase
@SessionScope
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
databaseKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
@ -166,8 +167,8 @@ internal abstract class CryptoModule {
@JvmStatic
@Provides
@im.vector.matrix.android.internal.di.CryptoDatabase
fun providesClearCacheTask(@im.vector.matrix.android.internal.di.CryptoDatabase
@RealmCryptoDatabase
fun providesClearCacheTask(@RealmCryptoDatabase
realmConfiguration: RealmConfiguration): ClearCacheTask {
return RealmClearCacheTask(realmConfiguration)
}

View file

@ -23,7 +23,6 @@ import android.os.Handler
import android.os.Looper
import androidx.lifecycle.LiveData
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
@ -33,7 +32,6 @@ import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@ -53,11 +51,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFa
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.*
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
@ -65,19 +59,11 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toRest
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.database.repository.CurrentStateEventDataSource
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
@ -89,13 +75,8 @@ import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.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 im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
@ -159,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope
@ -178,16 +159,16 @@ internal class DefaultCryptoService @Inject constructor(
fun onStateEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
fun onLiveEvent(roomId: String, event: Event) {
when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
}
}
@ -412,7 +393,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList()
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
}
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
@ -508,7 +489,7 @@ internal class DefaultCryptoService @Inject constructor(
val alg: IMXEncrypting = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
else -> olmEncryptionFactory.create(roomId)
else -> olmEncryptionFactory.create(roomId)
}
synchronized(roomEncryptors) {
@ -542,12 +523,10 @@ internal class DefaultCryptoService @Inject constructor(
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/
override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
}
return encryptionEvent != null
return sessionDatabase.eventQueries
.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.executeAsList()
.firstOrNull() != null
}
/**
@ -706,17 +685,17 @@ internal class DefaultCryptoService @Inject constructor(
onRoomKeyEvent(event)
}
EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> {
EventType.ROOM_KEY_REQUEST -> {
// save audit trail
cryptoStore.saveGossipingEvent(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event)
}
EventType.SEND_SECRET -> {
EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event)
}
else -> {
else -> {
// ignore
}
}
@ -767,30 +746,19 @@ internal class DefaultCryptoService @Inject constructor(
return
}
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
// TODO Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
}
}
/**
* Returns true if handled by SDK, otherwise should be sent to application layer
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
when (existingRequest.secretName) {
SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretValue)
true
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
return
}
USER_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretUSKGossip(secretValue)
true
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
return
}
KEYBACKUP_SECRET_SSSS_NAME -> {
keysBackupService.onSecretKeyGossip(secretValue)
true
else -> {
// Ask to application layer?
Timber.v("## onSecretSend() : secret not handled by SDK")
}
else -> false
}
}
@ -804,29 +772,24 @@ internal class DefaultCryptoService @Inject constructor(
val params = LoadRoomMembersTask.Params(roomId)
try {
loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
} catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
}
}
}
private fun getRoomUserIds(roomId: String): List<String> {
var userIds: List<String> = emptyList()
monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
// Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
userIds = if (encryptForInvitedMembers) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
}
return if (encryptForInvitedMembers) {
RoomMemberHelper(sessionDatabase, roomId).getActiveRoomMemberIds()
} else {
RoomMemberHelper(sessionDatabase, roomId).getJoinedRoomMemberIds()
}
return userIds
}
/**

View file

@ -75,8 +75,8 @@ import im.vector.matrix.android.internal.crypto.store.db.query.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
@ -91,7 +91,7 @@ import kotlin.collections.set
@SessionScope
internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
@RealmCryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ==========================================================================================

View file

@ -19,35 +19,33 @@ package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.sqldelight.session.SessionDatabase
import timber.log.Timber
import java.util.ArrayList
import java.util.*
import javax.inject.Inject
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
data class Params(
val events: List<Event>,
val verificationService: DefaultVerificationService,
val cryptoService: CryptoService
val eventInsertNotifications: List<EventInsertNotification>
)
}
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val sessionDatabase: SessionDatabase,
private val verificationService: DefaultVerificationService,
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
companion object {
@ -57,8 +55,15 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
// TODO ignore initial sync or back pagination?
params.eventInsertNotifications.forEach { eventInsertNotification ->
val eventId = eventInsertNotification.event_id
val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
if (isLocalEcho) {
return@forEach
}
val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
?: return@forEach
params.events.forEach { event ->
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
@ -74,7 +79,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync
try {
val result = cryptoService.decryptEvent(event, "")
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@ -83,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
)
} catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
@ -112,7 +117,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
@ -121,13 +126,13 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
relatesToEventId?.let {
transactionsHandledByOtherDevice.remove(it)
params.verificationService.onRoomRequestHandledByOtherDevice(event)
verificationService.onRoomRequestHandledByOtherDevice(event)
}
}
@ -148,11 +153,11 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> {
params.verificationService.onRoomEvent(event)
verificationService.onRoomEvent(event)
}
EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
params.verificationService.onRoomRequestReceived(event)
verificationService.onRoomRequestReceived(event)
}
}
}

View file

@ -15,59 +15,37 @@
*/
package im.vector.matrix.android.internal.crypto.verification
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.whereTypes
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.sqldelight.session.SessionDatabase
import javax.inject.Inject
internal class VerificationMessageLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
private val cryptoService: CryptoService,
private val verificationService: DefaultVerificationService,
private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
sessionDatabase: SessionDatabase,
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask
) : SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
override val query = Monarchy.Query {
EventEntity.whereTypes(it, listOf(
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_READY,
EventType.MESSAGE,
EventType.ENCRYPTED)
)
override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
types = listOf(
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_READY,
EventType.MESSAGE,
EventType.ENCRYPTED)
)
override suspend fun handleChanges(results: List<EventInsertNotification>) {
val params = RoomVerificationUpdateTask.Params(results)
roomVerificationUpdateTask.execute(params)
val notificationIds = results.map { it.event_id }
sessionDatabase.observerTriggerQueries.deleteEventInsertNotifications(notificationIds)
}
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// Should we ignore when it's an initial sync?
val events = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.filterNot {
// ignore local echos
LocalEcho.isLocalEchoId(it.eventId ?: "")
}
.toList()
roomVerificationUpdateTask.configureWith(
RoomVerificationUpdateTask.Params(events, verificationService, cryptoService)
).executeBy(taskExecutor)
}
}

View file

@ -1,120 +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.matrix.android.internal.database
import android.content.Context
import android.util.Base64
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
import io.realm.RealmConfiguration
import timber.log.Timber
import java.security.SecureRandom
import javax.inject.Inject
/**
* On creation a random key is generated, this key is then encrypted using the system KeyStore.
* The encrypted key is stored in shared preferences.
* When the database is opened again, the encrypted key is taken from the shared pref,
* then the Keystore is used to decrypt the key. The decrypted key is passed to the RealConfiguration.
*
* On android >=M, the KeyStore generates an AES key to encrypt/decrypt the database key,
* and the encrypted key is stored with the initialization vector in base64 in the shared pref.
* On android <M, the KeyStore cannot create AES keys, so a public/private key pair is generated,
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
*/
internal class RealmKeysUtils @Inject constructor(context: Context,
private val secretStoringUtils: SecretStoringUtils) {
private val rng = SecureRandom()
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
private fun generateKeyForRealm(): ByteArray {
val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH)
rng.nextBytes(keyForRealm)
return keyForRealm
}
/**
* Check if there is already a key for this alias
*/
private fun hasKeyForDatabase(alias: String): Boolean {
return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias")
}
/**
* Creates a new secure random key for this database.
* The random key is then encrypted by the keystore, and the encrypted key is stored
* in shared preferences.
*
* @return the generated key (can be passed to Realm Configuration)
*/
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
val key = generateKeyForRealm()
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
sharedPreferences
.edit()
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
.apply()
return key
}
/**
* Retrieves the key for this database
* throws if something goes wrong
*/
private fun extractKeyForDatabase(alias: String): ByteArray {
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
return Base64.decode(b64!!, Base64.NO_PADDING)
}
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
val key = if (hasKeyForDatabase(alias)) {
Timber.i("Found key for alias:$alias")
extractKeyForDatabase(alias)
} else {
Timber.i("Create key for DB alias:$alias")
createAndSaveKeyForDatabase(alias)
}
if (BuildConfig.LOG_PRIVATE_DATA) {
val log = key.joinToString("") { "%02x".format(it) }
Timber.w("Database key for alias `$alias`: $log")
}
realmConfigurationBuilder.encryptionKey(key)
}
// Delete elements related to the alias
fun clear(alias: String) {
if (hasKeyForDatabase(alias)) {
secretStoringUtils.safeDeleteKey(alias)
sharedPreferences
.edit()
.remove("${ENCRYPTED_KEY_PREFIX}_$alias")
.apply()
}
}
companion object {
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
}
}

View file

@ -1,83 +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.matrix.android.internal.database
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
import io.realm.RealmResults
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
fun dispose()
fun cancelProcess()
fun isStarted(): Boolean
}
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
}
protected val observerScope = CoroutineScope(SupervisorJob())
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val backgroundRealm = AtomicReference<Realm>()
private lateinit var results: AtomicReference<RealmResults<T>>
override fun start() {
if (isStarted.compareAndSet(false, true)) {
BACKGROUND_HANDLER.post {
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
val queryResults = query.createQuery(realm).findAll()
queryResults.addChangeListener(this)
results = AtomicReference(queryResults)
}
}
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
BACKGROUND_HANDLER.post {
results.getAndSet(null).removeAllChangeListeners()
backgroundRealm.getAndSet(null).also {
it.close()
}
observerScope.coroutineContext.cancelChildren()
}
}
}
override fun cancelProcess() {
observerScope.coroutineContext.cancelChildren()
}
override fun isStarted(): Boolean {
return isStarted.get()
}
}

View file

@ -1,60 +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.matrix.android.internal.database
import io.realm.Realm
import io.realm.RealmChangeListener
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
internal suspend fun <T> awaitNotEmptyResult(realmConfiguration: RealmConfiguration,
timeoutMillis: Long,
builder: (Realm) -> RealmQuery<T>) {
withTimeout(timeoutMillis) {
// Confine Realm interaction to a single thread with Looper.
withContext(Dispatchers.Main) {
val latch = CompletableDeferred<Unit>()
Realm.getInstance(realmConfiguration).use { realm ->
val result = builder(realm).findAllAsync()
val listener = object : RealmChangeListener<RealmResults<T>> {
override fun onChange(it: RealmResults<T>) {
if (it.isNotEmpty()) {
result.removeChangeListener(this)
latch.complete(Unit)
}
}
}
result.addChangeListener(listener)
try {
latch.await()
} catch (e: CancellationException) {
result.removeChangeListener(listener)
throw e
}
}
}
}
}

View file

@ -33,7 +33,8 @@ internal object EventMapper {
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
val eventEntity = EventEntity()
// TODO change this as we shouldn't use event everywhere
eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
eventEntity.eventId = event.eventId
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
eventEntity.roomId = event.roomId ?: roomId
eventEntity.content = ContentMapper.map(event.content)
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
@ -48,6 +49,34 @@ internal object EventMapper {
return eventEntity
}
fun map(event: Event, roomId: String, sendState: SendState, ageLocalTs: Long?): im.vector.matrix.sqldelight.session.EventEntity {
val uds = if (event.unsignedData == null) {
null
} else {
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
}
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
return im.vector.matrix.sqldelight.session.EventEntity.Impl(
// TODO change this as we shouldn't use event everywhere
event_id = event.eventId
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}",
room_id = event.roomId ?: roomId,
content = ContentMapper.map(event.content),
prev_content = ContentMapper.map(resolvedPrevContent),
state_key = event.stateKey,
type = event.type,
sender_id = event.senderId,
origin_server_ts = event.originServerTs,
redacts = event.redacts,
age = event.unsignedData?.age ?: event.originServerTs,
unsigned_data = uds,
age_local_ts = ageLocalTs,
send_state = sendState.name,
decryption_error_code = null,
decryption_result_json = null
)
}
fun map(eventEntity: EventEntity): Event {
val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() }
@ -87,12 +116,56 @@ internal object EventMapper {
}
}
}
fun map(eventEntity: im.vector.matrix.sqldelight.session.EventEntity): Event {
return Event(
type = eventEntity.type,
eventId = eventEntity.event_id,
content = ContentMapper.map(eventEntity.content),
prevContent = ContentMapper.map(eventEntity.prev_content),
originServerTs = eventEntity.origin_server_ts,
senderId = eventEntity.sender_id,
stateKey = eventEntity.state_key,
roomId = eventEntity.room_id,
unsignedData = UnsignedDataMapper.mapFromString(eventEntity.unsigned_data),
redacts = eventEntity.redacts
).also {
it.ageLocalTs = eventEntity.age_local_ts
it.sendState = SendState.valueOf(eventEntity.send_state)
it.setDecryptionValues(eventEntity.decryption_result_json, eventEntity.decryption_error_code)
}
}
}
internal fun Event.setDecryptionValues(decryptionResultJson: String?, decryptionErrorCode: String?): Event {
return apply {
decryptionResultJson?.let { json ->
try {
mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
} catch (t: JsonDataException) {
Timber.e(t, "Failed to parse decryption result")
}
}
// TODO get the full crypto error object
mCryptoError = decryptionErrorCode?.let { errorCode ->
MXCryptoError.ErrorType.valueOf(errorCode)
}
}
}
internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this)
}
internal fun im.vector.matrix.sqldelight.session.EventEntity.asDomain(): Event {
return EventMapper.map(this)
}
internal fun Event.toSQLEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): im.vector.matrix.sqldelight.session.EventEntity {
return EventMapper.map(this, roomId, sendState, ageLocalTs)
}
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
return EventMapper.map(this, roomId).apply {
this.sendState = sendState

View file

@ -17,14 +17,16 @@ package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Types
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.RuleKind
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.database.model.PushRuleEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.RealmList
import im.vector.matrix.sqldelight.session.GetPushConditions
import im.vector.matrix.sqldelight.session.PushRuleEntity
import timber.log.Timber
import javax.inject.Inject
internal object PushRulesMapper {
internal class PushRulesMapper @Inject constructor(private val pushConditionMapper: PushConditionMapper) {
private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java))
@ -33,10 +35,10 @@ internal object PushRulesMapper {
fun mapContentRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
)
@ -44,58 +46,60 @@ internal object PushRulesMapper {
}
private fun fromActionStr(actionsStr: String?): List<Any> {
try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
return try {
actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
} catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList()
emptyList()
}
}
fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.rule_id)
)
)
}
fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.rule_id)
)
)
}
fun map(pushrule: PushRuleEntity): PushRule {
fun map(pushrule: PushRuleEntity, conditions: List<GetPushConditions>): PushRule {
return PushRule(
actions = fromActionStr(pushrule.actionsStr),
default = pushrule.default,
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) }
actions = fromActionStr(pushrule.action_str),
default = pushrule.is_default,
enabled = pushrule.is_enabled,
ruleId = pushrule.rule_id,
conditions = conditions.map {
pushConditionMapper.map(it)
}
)
}
fun map(pushRule: PushRule): PushRuleEntity {
return PushRuleEntity(
actionsStr = moshiActionsAdapter.toJson(pushRule.actions),
default = pushRule.default ?: false,
enabled = pushRule.enabled,
ruleId = pushRule.ruleId,
fun map(scope: String, kind: RuleKind, pushRule: PushRule): PushRuleEntity {
return PushRuleEntity.Impl(
action_str = moshiActionsAdapter.toJson(pushRule.actions),
is_default = pushRule.default ?: false,
is_enabled = pushRule.enabled,
rule_id = pushRule.ruleId,
pattern = pushRule.pattern,
conditions = pushRule.conditions?.let {
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray())
} ?: RealmList()
scope = scope,
kind = kind.name
)
}
}

View file

@ -16,40 +16,87 @@
package im.vector.matrix.android.internal.database.mapper
import com.squareup.sqldelight.db.SqlCursor
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
internal class TimelineEventMapper @Inject constructor() {
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
val readReceipts = if (buildReadReceipts) {
correctedReadReceipts ?: timelineEventEntity.readReceipts
?.let {
readReceiptsSummaryMapper.map(it)
}
} else {
null
fun map(cursor: SqlCursor): TimelineEvent = map(
cursor.getLong(0)!!,
cursor.getLong(1)!!,
cursor.getLong(2)!!.toInt(),
cursor.getString(3),
cursor.getString(4),
cursor.getLong(5)!! == 1L,
cursor.getString(6)!!,
cursor.getString(7)!!,
cursor.getString(8),
cursor.getString(9),
cursor.getString(10),
cursor.getString(11)!!,
cursor.getString(12)!!,
cursor.getLong(13),
cursor.getString(14),
cursor.getString(15),
cursor.getString(16),
cursor.getLong(17),
cursor.getLong(18),
cursor.getString(19),
cursor.getString(20)
)
fun map(local_id: Long,
chunk_id: Long,
display_index: Int,
sender_name: String?,
sender_avatar: String?,
is_unique_display_name: Boolean,
event_id: String,
room_id: String,
content: String?,
prev_content: String?,
state_key: String?,
send_state: String,
type: String,
origin_server_ts: Long?,
sender_id: String?,
unsigned_data: String?,
redacts: String?,
age: Long?,
age_local_ts: Long?,
decryption_result_json: String?,
decryption_error_code: String?): TimelineEvent {
val event = Event(
type = type,
roomId = room_id,
eventId = event_id,
content = ContentMapper.map(content),
prevContent = ContentMapper.map(prev_content),
originServerTs = origin_server_ts,
senderId = sender_id,
redacts = redacts,
stateKey = state_key,
unsignedData = UnsignedDataMapper.mapFromString(unsigned_data)
).also {
it.ageLocalTs = age_local_ts
it.sendState = SendState.valueOf(send_state)
it.setDecryptionValues(decryption_result_json, decryption_error_code)
}
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId),
eventId = timelineEventEntity.eventId,
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
root = event,
eventId = event_id,
annotations = null,
displayIndex = display_index,
isUniqueDisplayName = is_unique_display_name,
localId = local_id,
readReceipts = emptyList(),
senderAvatar = sender_avatar,
senderName = sender_name
)
}
}

View file

@ -20,6 +20,7 @@ import android.content.Context
import android.content.res.Resources
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.internal.concurrency.newNamedSingleThreadExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler
import kotlinx.coroutines.Dispatchers
@ -36,11 +37,14 @@ internal object MatrixModule {
@Provides
@MatrixScope
fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers {
return MatrixCoroutineDispatchers(io = Dispatchers.IO,
return MatrixCoroutineDispatchers(
dbTransaction = newNamedSingleThreadExecutor("db_transaction").asCoroutineDispatcher(),
dbQuery = newNamedSingleThreadExecutor("db_query").asCoroutineDispatcher(),
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
dmVerif = newNamedSingleThreadExecutor("dm_verif").asCoroutineDispatcher()
)
}

View file

@ -17,51 +17,45 @@
package im.vector.matrix.android.internal.session.group
import androidx.work.ExistingWorkPolicy
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import com.squareup.sqldelight.Query
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmResults
import kotlinx.coroutines.launch
import im.vector.matrix.sqldelight.session.Memberships
import im.vector.matrix.sqldelight.session.SessionDatabase
import javax.inject.Inject
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
internal class GroupSummaryUpdater @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
@SessionId private val sessionId: String,
private val monarchy: Monarchy)
: RealmLiveEntityObserver<GroupEntity>(monarchy.realmConfiguration) {
sessionDatabase: SessionDatabase)
: SqlLiveEntityObserver<String>(sessionDatabase) {
override val query = Monarchy.Query { GroupEntity.where(it) }
override val query: Query<String>
get() = sessionDatabase.observerTriggerQueries.getAllGroupNotifications()
override fun onChange(results: RealmResults<GroupEntity>, changeSet: OrderedCollectionChangeSet) {
// `insertions` for new groups and `changes` to handle left groups
val modifiedGroupEntity = (changeSet.insertions + changeSet.changes)
.asSequence()
.mapNotNull { results[it] }
override suspend fun handleChanges(results: List<String>) {
val groupIdsToFetchData = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
groupIds = results,
memberships = listOf(Memberships.JOIN, Memberships.INVITE)
).executeAsList()
fetchGroupsData(groupIdsToFetchData)
fetchGroupsData(modifiedGroupEntity
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
.map { it.groupId }
.toList())
val groupIdsToDelete = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
groupIds = results,
memberships = listOf(Memberships.LEAVE)
).executeAsList()
modifiedGroupEntity
.filter { it.membership == Membership.LEAVE }
.map { it.groupId }
.toList()
.also {
observerScope.launch {
deleteGroups(it)
}
}
sessionDatabase.awaitTransaction(coroutineDispatchers) {
sessionDatabase.groupSummaryQueries.deleteGroupSummaries(groupIdsToDelete)
sessionDatabase.observerTriggerQueries.deleteGroupNotifications(results)
}
}
private fun fetchGroupsData(groupIds: List<String>) {
@ -79,12 +73,4 @@ internal class GroupSummaryUpdater @Inject constructor(
.enqueue()
}
/**
* Delete the GroupSummaryEntity of left groups
*/
private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
GroupSummaryEntity.where(realm, groupIds)
.findAll()
.deleteAllFromRealm()
}
}

View file

@ -16,62 +16,44 @@
package im.vector.matrix.android.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.sqldelight.session.SessionDatabase
import org.greenrobot.eventbus.EventBus
import java.util.Date
import java.util.*
import javax.inject.Inject
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI,
private val monarchy: Monarchy,
private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val eventBus: EventBus
) : GetHomeServerCapabilitiesTask {
override suspend fun execute(params: Unit) {
var doRequest = false
monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
}
val lastUpdatedTs = sessionDatabase.homeServerCapabilitiesQueries.selectLastUpdatedTimetamp().executeAsOneOrNull()
val doRequest = lastUpdatedTs == null || lastUpdatedTs + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
if (!doRequest) {
return
}
val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult>(eventBus) {
apiCall = capabilitiesAPI.getUploadCapabilities()
}
val capabilities = runCatching {
executeRequest<GetCapabilitiesResult>(eventBus) {
apiCall = capabilitiesAPI.getCapabilities()
}
}.getOrNull()
// TODO Add other call here (get version, etc.)
insertInDb(capabilities, uploadCapabilities)
insertInDb(uploadCapabilities)
}
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
private suspend fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
sessionDatabase.awaitTransaction(coroutineDispatchers) {
val maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
val lastUpdatedTimestamp = Date().time
it.homeServerCapabilitiesQueries.insert(maxUploadFileSize, lastUpdatedTimestamp)
}
}

View file

@ -25,7 +25,8 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor(private val
fun getHomeServerCapabilities(): HomeServerCapabilities {
return sessionDatabase.homeServerCapabilitiesQueries.selectMaxUploadFileSize().executeAsOneOrNull()
?.let {
HomeServerCapabilities(it)
//TODO: handle canChangePassword
HomeServerCapabilities(false, it)
}
?: HomeServerCapabilities()
}

View file

@ -15,22 +15,18 @@
*/
package im.vector.matrix.android.internal.session.notification
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.RuleKind
import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.pushrules.getActions
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.RuleSet
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
import im.vector.matrix.android.internal.session.pushers.PushRuleDataSource
import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleActionsTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask
@ -44,10 +40,10 @@ internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
private val pushRuleDataSource: PushRuleDataSource,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask
) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()
@ -59,47 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
}
override fun getPushRules(scope: String): RuleSet {
var contentRules: List<PushRule> = emptyList()
var overrideRules: List<PushRule> = emptyList()
var roomRules: List<PushRule> = emptyList()
var senderRules: List<PushRule> = emptyList()
var underrideRules: List<PushRule> = emptyList()
monarchy.doWithRealm { realm ->
PushRulesEntity.where(realm, scope, RuleSetKey.CONTENT)
.findFirst()
?.let { pushRulesEntity ->
contentRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapContentRule(it) }
}
PushRulesEntity.where(realm, scope, RuleSetKey.OVERRIDE)
.findFirst()
?.let { pushRulesEntity ->
overrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) }
}
PushRulesEntity.where(realm, scope, RuleSetKey.ROOM)
.findFirst()
?.let { pushRulesEntity ->
roomRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapRoomRule(it) }
}
PushRulesEntity.where(realm, scope, RuleSetKey.SENDER)
.findFirst()
?.let { pushRulesEntity ->
senderRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapSenderRule(it) }
}
PushRulesEntity.where(realm, scope, RuleSetKey.UNDERRIDE)
.findFirst()
?.let { pushRulesEntity ->
underrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) }
}
}
return RuleSet(
content = contentRules,
override = overrideRules,
room = roomRules,
sender = senderRules,
underride = underrideRules
)
return pushRuleDataSource.getPushRules(scope)
}
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {

View file

@ -19,19 +19,19 @@ import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.pushers.PusherState
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.PushersMapper
import im.vector.matrix.android.internal.database.model.PusherEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.awaitTransaction
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.worker.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import im.vector.matrix.sqldelight.session.SessionDatabase
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
@ -44,14 +44,20 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var pushersAPI: PushersAPI
@Inject lateinit var monarchy: Monarchy
@Inject lateinit var eventBus: EventBus
@Inject
lateinit var pushersAPI: PushersAPI
@Inject
lateinit var sessionDatabase: SessionDatabase
@Inject
lateinit var pushersMapper: PushersMapper
@Inject
lateinit var eventBus: EventBus
@Inject
lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
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)
@ -67,15 +73,12 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
} catch (exception: Throwable) {
when (exception) {
is Failure.NetworkConnection -> Result.retry()
else -> {
monarchy.awaitTransaction { realm ->
PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
// update it
it.state = PusherState.FAILED_TO_REGISTER
}
else -> {
sessionDatabase.awaitTransaction(coroutineDispatchers) {
sessionDatabase.pushersQueries.updateState(PusherState.FAILED_TO_REGISTER.name, pusher.pushKey)
}
// always return success, or the chain will be stuck for ever!
Result.failure()
Result.success()
}
}
}
@ -85,24 +88,9 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
executeRequest<Unit>(eventBus) {
apiCall = pushersAPI.setPusher(pusher)
}
monarchy.awaitTransaction { realm ->
val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
if (echo != null) {
// update it
echo.appDisplayName = pusher.appDisplayName
echo.appId = pusher.appId
echo.kind = pusher.kind
echo.lang = pusher.lang
echo.profileTag = pusher.profileTag
echo.data?.format = pusher.data?.format
echo.data?.url = pusher.data?.url
echo.state = PusherState.REGISTERED
} else {
pusher.toEntity().also {
it.state = PusherState.REGISTERED
realm.insertOrUpdate(it)
}
}
sessionDatabase.awaitTransaction(coroutineDispatchers) {
val pusherEntity = pushersMapper.map(pusher, PusherState.REGISTERED)
sessionDatabase.pushersQueries.insertOrReplace(pusherEntity)
}
}
}

View file

@ -60,9 +60,6 @@ internal abstract class PushersModule {
@Binds
abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask
@Binds
abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask
@Binds
abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask

View file

@ -1,80 +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.matrix.android.internal.session.pushers
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.pushrules.rest.GetPushRulesResponse
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import javax.inject.Inject
/**
* Save the push rules in DB
*/
internal interface SavePushRulesTask : Task<SavePushRulesTask.Params, Unit> {
data class Params(val pushRules: GetPushRulesResponse)
}
internal class DefaultSavePushRulesTask @Inject constructor(private val monarchy: Monarchy) : SavePushRulesTask {
override suspend fun execute(params: SavePushRulesTask.Params) {
monarchy.awaitTransaction { realm ->
// clear current push rules
realm.where(PushRulesEntity::class.java)
.findAll()
.deleteAllFromRealm()
// Save only global rules for the moment
val globalRules = params.pushRules.global
val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT }
globalRules.content?.forEach { rule ->
content.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(content)
val override = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.OVERRIDE }
globalRules.override?.forEach { rule ->
PushRulesMapper.map(rule).also {
override.pushRules.add(it)
}
}
realm.insertOrUpdate(override)
val rooms = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.ROOM }
globalRules.room?.forEach { rule ->
rooms.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(rooms)
val senders = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.SENDER }
globalRules.sender?.forEach { rule ->
senders.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(senders)
val underrides = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.UNDERRIDE }
globalRules.underride?.forEach { rule ->
underrides.pushRules.add(PushRulesMapper.map(rule))
}
realm.insertOrUpdate(underrides)
}
}
}

View file

@ -15,16 +15,9 @@
*/
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.AggregatedAnnotation
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.events.model.*
import im.vector.matrix.android.api.session.room.model.PollSummaryContent
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.model.VoteInfo
@ -34,21 +27,18 @@ import im.vector.matrix.android.api.session.room.model.message.MessageRelationCo
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.create
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.sqldelight.session.*
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@ -56,8 +46,7 @@ import javax.inject.Inject
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
data class Params(
val events: List<Event>,
val userId: String
val eventInsertNotifications: List<EventInsertNotification>
)
}
@ -92,59 +81,52 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/
internal class DefaultEventRelationsAggregationTask @Inject constructor(
private val monarchy: Monarchy,
private val sessionDatabase: SessionDatabase,
@UserId private val userId: String,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoService: CryptoService) : EventRelationsAggregationTask {
// OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
override suspend fun execute(params: EventRelationsAggregationTask.Params) {
val events = params.events
val userId = params.userId
monarchy.awaitTransaction { realm ->
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events")
update(realm, events, userId)
val eventInsertNotifications = params.eventInsertNotifications
sessionDatabase.awaitTransaction(coroutineDispatchers) {
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${eventInsertNotifications.size} events")
update(eventInsertNotifications, userId)
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
}
}
private fun update(realm: Realm, events: List<Event>, userId: String) {
events.forEach { event ->
private fun update(eventInsertNotifications: List<EventInsertNotification>, userId: String) {
eventInsertNotifications.forEach { eventInsertNotification ->
try { // Temporary catch, should be removed
val roomId = event.roomId
if (roomId == null) {
Timber.w("Event has no room id ${event.eventId}")
return@forEach
}
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
when (event.type) {
EventType.REACTION -> {
val roomId = eventInsertNotification.room_id
val eventId = eventInsertNotification.event_id
val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
?: return@forEach
when (eventInsertNotification.type) {
EventType.REACTION -> {
// we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
handleReaction(event, roomId, realm, userId, isLocalEcho)
Timber.v("###REACTION in room $roomId , reaction eventID ${eventId}")
handleReaction(event, roomId, userId, isLocalEcho)
}
EventType.MESSAGE -> {
EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
?: "").findFirst()?.let { tet ->
tet.annotations = it
}
}
//handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations)
}
val content: MessageContent? = event.content.toModel()
if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace!
handleReplace(realm, event, content, roomId, isLocalEcho)
handleReplace(event, content, roomId, isLocalEcho)
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
handleResponse(userId, event, content, roomId, isLocalEcho)
}
}
@ -158,12 +140,12 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
if (it.type == RelationType.REFERENCE && it.eventId != null) {
handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId)
handleVerification(event, roomId, isLocalEcho, it.eventId, userId)
}
}
}
EventType.ENCRYPTED -> {
EventType.ENCRYPTED -> {
// Relation type is in clear
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
@ -175,10 +157,10 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
handleReplace(event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
//handleResponse( realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
}
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
@ -193,33 +175,33 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
EventType.KEY_VERIFICATION_KEY -> {
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
encryptedEventContent.relatesTo.eventId?.let {
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
handleVerification(event, roomId, isLocalEcho, it, userId)
}
}
}
}
}
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { sessionDatabase.eventQueries.select(event.redacts).executeAsOneOrNull() }
?: return@forEach
when (eventToPrune.type) {
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}")
EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.event_id}")
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
// ?: UnsignedData(null, null)
// was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, roomId)
}
}
EventType.REACTION -> {
handleReactionRedact(eventToPrune, realm, userId)
handleReactionRedact(eventToPrune, roomId)
}
}
}
else -> Timber.v("UnHandled event ${event.eventId}")
else -> Timber.v("UnHandled event ${eventInsertNotification.event_id}")
}
} catch (t: Throwable) {
Timber.e(t, "## Should not happen ")
@ -230,7 +212,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
private fun decryptIfNeeded(event: Event) {
if (event.mxDecryptionResult == null) {
try {
val result = cryptoService.decryptEvent(event, "")
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
@ -244,69 +226,85 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
private fun handleReplace(event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
val eventId = event.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return
// ok, this is a replace
val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId)
// we have it
val existingSummary = existing.editSummary
val existingSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(targetEventId).executeAsOneOrNull()
if (existingSummary == null) {
Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)")
// create the edit summary
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
editSummary.aggregatedContent = ContentMapper.map(newContent)
val lastEditTs: Long
val sourceLocalEchoEvents: List<String>
val sourceEvents: List<String>
if (isLocalEcho) {
editSummary.lastEditTs = 0
editSummary.sourceLocalEchoEvents.add(eventId)
lastEditTs = 0
sourceLocalEchoEvents = listOf(eventId)
sourceEvents = emptyList()
} else {
editSummary.lastEditTs = event.originServerTs ?: 0
editSummary.sourceEvents.add(eventId)
lastEditTs = event.originServerTs ?: 0
sourceLocalEchoEvents = emptyList()
sourceEvents = listOf(eventId)
}
existing.editSummary = editSummary
val newEditSummary = EditAggregatedSummary.Impl(
event_id = targetEventId,
room_id = roomId,
aggregated_content = ContentMapper.map(newContent),
last_edit_ts = lastEditTs,
source_local_echo_ids = sourceLocalEchoEvents,
source_event_ids = sourceEvents
)
sessionDatabase.eventAnnotationsQueries.insertNewEdit(newEditSummary)
} else {
if (existingSummary.sourceEvents.contains(eventId)) {
val sourceEvents = existingSummary.source_event_ids.toMutableList()
if (sourceEvents.contains(eventId)) {
// ignore this event, we already know it (??)
Timber.v("###REPLACE ignoring event for summary, it's known $eventId")
return
}
val txId = event.unsignedData?.transactionId
// is it a remote echo?
if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) {
val sourceLocalEchoEvents = existingSummary.source_local_echo_ids.toMutableList()
if (!isLocalEcho && sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
existingSummary.sourceLocalEchoEvents.remove(txId)
existingSummary.sourceEvents.add(event.eventId)
sourceLocalEchoEvents.remove(txId)
sourceEvents.add(event.eventId)
} else if (
isLocalEcho // do not rely on ts for local echo, take it
|| event.originServerTs ?: 0 >= existingSummary.lastEditTs
|| event.originServerTs ?: 0 >= existingSummary.last_edit_ts
) {
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
if (!isLocalEcho) {
// Do not take local echo originServerTs here, could mess up ordering (keep old ts)
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
val newLastEditTs = event.originServerTs ?: System.currentTimeMillis()
sessionDatabase.eventAnnotationsQueries.updateEditLastTs(newLastEditTs, targetEventId)
}
existingSummary.aggregatedContent = ContentMapper.map(newContent)
val newAggregatedContent = ContentMapper.map(newContent)
sessionDatabase.eventAnnotationsQueries.updateEditContent(newAggregatedContent, targetEventId)
if (isLocalEcho) {
existingSummary.sourceLocalEchoEvents.add(eventId)
sourceLocalEchoEvents.add(eventId)
} else {
existingSummary.sourceEvents.add(eventId)
sourceEvents.add(eventId)
}
} else {
// ignore this event for the summary (back paginate)
if (!isLocalEcho) {
existingSummary.sourceEvents.add(eventId)
sourceEvents.add(eventId)
}
Timber.v("###REPLACE ignoring event for summary, it's to old $eventId")
}
sessionDatabase.eventAnnotationsQueries.updateEditSources(
sourceEventIds = sourceEvents,
sourceLocalEchoIds = sourceLocalEchoEvents,
eventId = targetEventId
)
}
}
private fun handleResponse(realm: Realm,
userId: String,
private fun handleResponse(userId: String,
event: Event,
content: MessageContent,
roomId: String,
@ -317,45 +315,38 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val eventTimestamp = event.originServerTs ?: return
// ok, this is a poll response
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
if (existing == null) {
Timber.v("## POLL creating new relation summary for $targetEventId")
existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
}
// we have it
val existingPollSummary = existing.pollResponseSummary
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
existing.pollResponseSummary = it
}
val closedTime = existingPollSummary?.closedTime
val existingPollSummary = sessionDatabase.eventAnnotationsQueries.selectPollForEvent(targetEventId).executeAsOneOrNull()
val closedTime = existingPollSummary?.closed_time
if (closedTime != null && eventTimestamp > closedTime) {
Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
return
}
val sumModel = ContentMapper.map(existingPollSummary?.content).toModel<PollSummaryContent>()
?: PollSummaryContent()
val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent()
if (existingPollSummary!!.sourceEvents.contains(eventId)) {
val sourceEvents = existingPollSummary?.source_event_ids?.toMutableList() ?: ArrayList()
if (sourceEvents.contains(eventId)) {
// ignore this event, we already know it (??)
Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId")
return
}
val txId = event.unsignedData?.transactionId
// is it a remote echo?
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
val sourceLocalEchoEvents = existingPollSummary?.source_local_echo_ids?.toMutableList()
?: ArrayList()
if (!isLocalEcho && sourceLocalEchoEvents.contains(txId)) {
// ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$eventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId)
sourceLocalEchoEvents.remove(txId)
sourceEvents.add(event.eventId)
sessionDatabase.eventAnnotationsQueries.updatePollSources(sourceEvents, sourceLocalEchoEvents, targetEventId)
return
}
val responseContent = event.content.toModel<MessagePollResponseContent>() ?: return Unit.also {
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
}
val responseContent = event.content.toModel<MessagePollResponseContent>()
?: return Unit.also {
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
}
val optionIndex = responseContent.relatesTo?.option ?: return Unit.also {
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
@ -385,12 +376,13 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
sumModel.votes = votes
if (isLocalEcho) {
existingPollSummary.sourceLocalEchoEvents.add(eventId)
sourceLocalEchoEvents.add(eventId)
} else {
existingPollSummary.sourceEvents.add(eventId)
sourceEvents.add(eventId)
}
existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
val newContent = ContentMapper.map(sumModel.toContent())
sessionDatabase.eventAnnotationsQueries.updatePollSources(sourceEvents, sourceLocalEchoEvents, targetEventId)
sessionDatabase.eventAnnotationsQueries.updatePollContent(newContent, targetEventId)
}
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
@ -415,7 +407,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
private fun handleReaction(event: Event, roomId: String, userId: String, isLocalEcho: Boolean) {
val content = event.content.toModel<ReactionContent>()
if (content == null) {
Timber.e("Malformed reaction content ${event.content}")
@ -425,50 +417,70 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
if (RelationType.ANNOTATION == content.relatesTo?.type) {
val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId
val reactionEventId = event.eventId
val reactionEventId = event.eventId ?: return
Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID)
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(relatedEventID, reaction).executeAsOneOrNull()
val txId = event.unsignedData?.transactionId
if (isLocalEcho && txId.isNullOrBlank()) {
Timber.w("Received a local echo with no transaction ID")
return
}
if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction
sum.firstTimestamp = event.originServerTs ?: 0
if (isLocalEcho) {
if (reactionSummary == null) {
Timber.v("$reaction is a new reaction")
val (sourceEventIds, sourceLocalEchoIds) = if (isLocalEcho && txId != null) {
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
sum.count = 1
Pair(emptyList(), listOf(txId))
} else {
Timber.v("Adding synced reaction $reaction")
sum.count = 1
sum.sourceEvents.add(reactionEventId)
Pair(listOf(reactionEventId), emptyList<String>())
}
sum.addedByMe = sum.addedByMe || (userId == event.senderId)
eventSummary.reactionsSummary.add(sum)
val newReactionSummary = ReactionAggregatedSummary.Impl(
event_id = relatedEventID,
room_id = roomId,
key = reaction,
count = 1,
added_by_me = userId == event.senderId,
first_timestamp = event.originServerTs ?: 0,
source_event_ids = sourceEventIds,
source_local_echo_ids = sourceLocalEchoIds
)
sessionDatabase.eventAnnotationsQueries.insertNewReaction(newReactionSummary)
} else {
Timber.v("$reaction is an already known reaction")
// is this a known event (is possible? pagination?)
if (!sum.sourceEvents.contains(reactionEventId)) {
val sourceEvents = reactionSummary.source_event_ids.toMutableList()
if (!sourceEvents.contains(reactionEventId)) {
// check if it's not the sync of a local echo
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
val sourceLocalEcho = reactionSummary.source_local_echo_ids.toMutableList()
if (!isLocalEcho && sourceLocalEcho.contains(txId)) {
// ok it has already been counted, just sync the list, do not touch count
Timber.v("Ignoring synced of local echo for reaction $reaction")
sum.sourceLocalEcho.remove(txId)
sum.sourceEvents.add(reactionEventId)
sourceLocalEcho.remove(txId)
sourceEvents.add(reactionEventId)
sessionDatabase.eventAnnotationsQueries.updateLocalReaction(
sourceEventIds = sourceEvents,
sourceLocalEchoIds = sourceLocalEcho,
eventId = relatedEventID,
key = reaction
)
} else {
sum.count += 1
if (isLocalEcho) {
val newCount = reactionSummary.count + 1
val newAddedByMe = reactionSummary.added_by_me || (userId == event.senderId)
if (isLocalEcho && txId != null) {
Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId)
sourceLocalEcho.add(txId)
} else {
Timber.v("Adding synced reaction $reaction")
sum.sourceEvents.add(reactionEventId)
sourceEvents.add(reactionEventId)
}
sum.addedByMe = sum.addedByMe || (userId == event.senderId)
sessionDatabase.eventAnnotationsQueries.updateReaction(
count = newCount,
addedByMe = newAddedByMe,
sourceEventIds = sourceEvents,
sourceLocalEchoIds = sourceLocalEcho,
eventId = relatedEventID,
key = reaction
)
}
}
}
@ -480,31 +492,34 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
/**
* Called when an event is deleted
*/
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, roomId: String) {
Timber.d("Handle redaction of m.replace")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
if (eventSummary == null) {
val editSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(relatedEventId).executeAsOneOrNull()
if (editSummary == null) {
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
return
}
val sourceEvents = eventSummary.editSummary?.sourceEvents
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
if (sourceToDiscard == null) {
val sourceEvents = editSummary.source_event_ids.toMutableList()
val sourceToDiscard = sourceEvents.indexOf(redacted.event_id)
if (sourceToDiscard == -1) {
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
return
}
// Need to remove this event from the redaction list and compute new aggregation state
sourceEvents.removeAt(sourceToDiscard)
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
val previousEdit = sourceEvents.mapNotNull { sessionDatabase.eventQueries.select(it).executeAsOneOrNull() }.sortedBy { it.origin_server_ts }.lastOrNull()
if (previousEdit == null) {
// revert to original
eventSummary.editSummary?.deleteFromRealm()
sessionDatabase.eventAnnotationsQueries.deleteEdit(relatedEventId, roomId)
} else {
// I have the last event
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
val newLastEditTs = previousEdit.origin_server_ts
?: System.currentTimeMillis()
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
val newAggregatedContent = ContentMapper.map(newContent)
sessionDatabase.eventAnnotationsQueries.updateEditSources(sourceEvents, editSummary.source_local_echo_ids, relatedEventId)
sessionDatabase.eventAnnotationsQueries.updateEditLastTs(newLastEditTs, relatedEventId)
sessionDatabase.eventAnnotationsQueries.updateEditContent(newAggregatedContent, relatedEventId)
} ?: run {
Timber.e("Failed to udate edited summary")
// TODO how to reccover that
@ -512,8 +527,8 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
Timber.v("REDACTION of reaction ${eventToPrune.eventId}")
fun handleReactionRedact(eventToPrune: EventEntity, roomId: String) {
Timber.v("REDACTION of reaction ${eventToPrune.event_id}")
// delete a reaction, need to update the annotation summary if any
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
?: return
@ -521,78 +536,110 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val reactionKey = reactionContent.relatesTo.key
Timber.v("REMOVE reaction for key $reactionKey")
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
if (summary != null) {
summary.reactionsSummary.where()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)
.findFirst()?.let { aggregation ->
Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})")
Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}")
if (aggregation.sourceEvents.contains(eventToPrune.eventId)) {
Timber.v("REMOVE reaction for key $reactionKey")
aggregation.sourceEvents.remove(eventToPrune.eventId)
Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}")
aggregation.count = aggregation.count - 1
if (eventToPrune.sender == userId) {
// Was it a redact on my reaction?
aggregation.addedByMe = false
}
if (aggregation.count == 0) {
// delete!
aggregation.deleteFromRealm()
}
} else {
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
}
}
val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(eventThatWasReacted, reactionKey).executeAsOneOrNull()
if (reactionSummary != null) {
val sourceEvents = reactionSummary.source_event_ids.toMutableList()
Timber.v("Find summary for key with ${sourceEvents.size} known reactions (count:${reactionSummary.count})")
if (sourceEvents.contains(eventToPrune.event_id)) {
Timber.v("REMOVE reaction for key $reactionKey")
sourceEvents.remove(eventToPrune.event_id)
val newCount = reactionSummary.count - 1
val addedByMe = if (eventToPrune.sender_id == userId) {
// Was it a redact on my reaction?
false
} else {
reactionSummary.added_by_me
}
if (newCount == 0L) {
sessionDatabase.eventAnnotationsQueries.deleteReaction(eventThatWasReacted, roomId, reactionKey)
} else {
sessionDatabase.eventAnnotationsQueries.updateReaction(
count = newCount,
addedByMe = addedByMe,
sourceEventIds = sourceEvents,
sourceLocalEchoIds = reactionSummary.source_local_echo_ids,
eventId = eventThatWasReacted,
key = reactionKey
)
}
} else {
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.event_id} is not known")
}
} else {
Timber.e("## Cannot find summary for key $reactionKey")
}
}
private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
val verifSummary = eventSummary.referencesSummaryEntity
?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also {
eventSummary.referencesSummaryEntity = it
}
val txId = event.unsignedData?.transactionId
if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) {
// ok it has already been handled
} else {
ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
?: ReferencesAggregatedContent(VerificationState.REQUEST)
// TODO ignore invalid messages? e.g a START after a CANCEL?
// i.e. never change state if already canceled/done
val currentState = data.verificationState
val newState = when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING)
EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) {
VerificationState.CANCELED_BY_ME
} else {
VerificationState.CANCELED_BY_OTHER
})
EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE)
else -> VerificationState.REQUEST
private fun handleVerification(event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
val eventId = event.eventId ?: return
val verifSummary = sessionDatabase.eventAnnotationsQueries.selectReferenceForEvent(relatedEventId).executeAsOneOrNull()
if (verifSummary == null) {
val state = VerificationState.REQUEST.computeNewVerificationState(event)
val data = ReferencesAggregatedContent(state)
val sourceLocalEchoEvents: List<String>
val sourceEvents: List<String>
if (isLocalEcho) {
sourceLocalEchoEvents = listOf(eventId)
sourceEvents = emptyList()
} else {
sourceLocalEchoEvents = emptyList()
sourceEvents = listOf(eventId)
}
val newVerifSummary = ReferencesAggregatedSummary.Impl(
event_id = relatedEventId,
room_id = roomId,
content = ContentMapper.map(data.toContent()),
source_local_echo_ids = sourceLocalEchoEvents,
source_event_ids = sourceEvents
)
sessionDatabase.eventAnnotationsQueries.insertNewReference(newVerifSummary)
data = data.copy(verificationState = newState)
verifSummary.content = ContentMapper.map(data.toContent())
}
if (isLocalEcho) {
verifSummary.sourceLocalEcho.add(event.eventId)
} else {
verifSummary.sourceLocalEcho.remove(txId)
verifSummary.sourceEvents.add(event.eventId)
val txId = event.unsignedData?.transactionId
val sourceEvents = verifSummary.source_event_ids.toMutableList()
val sourceLocalEcho = verifSummary.source_local_echo_ids.toMutableList()
if (!isLocalEcho && sourceLocalEcho.contains(txId)) {
// ok it has already been handled
} else {
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
?: ReferencesAggregatedContent(VerificationState.REQUEST)
// TODO ignore invalid messages? e.g a START after a CANCEL?
// i.e. never change state if already canceled/done
val currentState = data.verificationState
val newState = currentState.computeNewVerificationState(event)
data = data.copy(verificationState = newState)
val newContent = ContentMapper.map(data.toContent())
sessionDatabase.eventAnnotationsQueries.updateReferenceContent(newContent, relatedEventId)
}
if (isLocalEcho) {
sourceLocalEcho.add(eventId)
} else {
sourceLocalEcho.remove(txId)
sourceEvents.add(event.eventId)
}
sessionDatabase.eventAnnotationsQueries.updateReferenceSources(
sourceEventIds = sourceEvents,
sourceLocalEchoIds = sourceLocalEcho,
eventId = relatedEventId
)
}
}
private fun VerificationState.computeNewVerificationState(event: Event): VerificationState {
return when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> toState(VerificationState.WAITING)
EventType.KEY_VERIFICATION_CANCEL -> toState(if (event.senderId == userId) {
VerificationState.CANCELED_BY_ME
} else {
VerificationState.CANCELED_BY_OTHER
})
EventType.KEY_VERIFICATION_DONE -> toState(VerificationState.DONE)
else -> VerificationState.REQUEST
}
}
}

View file

@ -15,19 +15,10 @@
*/
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.whereTypes
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.sqldelight.session.SessionDatabase
import javax.inject.Inject
/**
@ -36,41 +27,32 @@ import javax.inject.Inject
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
*/
internal class EventRelationsAggregationUpdater @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String,
sessionDatabase: SessionDatabase,
private val task: EventRelationsAggregationTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
override val query = Monarchy.Query<EventEntity> {
EventEntity.whereTypes(it, listOf(
EventType.MESSAGE,
EventType.REDACTION,
EventType.REACTION,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC,
// TODO Add ?
// EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED)
)
override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
types = listOf(
EventType.MESSAGE,
EventType.REDACTION,
EventType.REACTION,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC,
// TODO Add ?
// EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED)
)
override suspend fun handleChanges(results: List<EventInsertNotification>) {
val params = EventRelationsAggregationTask.Params(results)
task.execute(params)
val notificationIds = results.map { it.event_id }
sessionDatabase.observerTriggerQueries.deleteEventInsertNotifications(notificationIds)
}
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
val insertedDomains = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.toList()
val params = EventRelationsAggregationTask.Params(
insertedDomains,
userId
)
observerScope.launch {
task.execute(params)
}
}
}

View file

@ -16,48 +16,34 @@
package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
import im.vector.matrix.android.internal.database.helper.isEventRead
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.getOrNull
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.database.mapper.map
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.sync.RoomSyncHandler
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import im.vector.matrix.sqldelight.session.RoomSummaryHeroes
import im.vector.matrix.sqldelight.session.SessionDatabase
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String,
private val sessionDatabase: SessionDatabase,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
private val eventBus: EventBus,
private val monarchy: Monarchy) {
private val eventBus: EventBus) {
companion object {
// TODO: maybe allow user of SDK to give that list
@ -79,107 +65,139 @@ internal class RoomSummaryUpdater @Inject constructor(
)
}
fun update(realm: Realm,
roomId: String,
membership: Membership? = null,
fun update(roomId: String,
newMembership: Membership? = null,
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false,
ephemeralResult: RoomSyncHandler.EphemeralResult? = null,
inviterId: String? = null) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear()
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
}
if (roomSummary.invitedMembersCount != null) {
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
}
if (roomSummary.joinedMembersCount != null) {
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
}
}
roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0
roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0
if (membership != null) {
roomSummaryEntity.membership = membership
val currentRoomSummary = sessionDatabase.roomSummaryQueries.get(roomId).executeAsOneOrNull()
val heroes = if (roomSummary != null && roomSummary.heroes.isNotEmpty()) {
roomSummary.heroes
} else {
emptyList()
}
sessionDatabase.roomSummaryQueries.deleteHeroes(roomId)
heroes.forEach {
sessionDatabase.roomSummaryQueries.setHeroes(RoomSummaryHeroes.Impl(it, roomId))
}
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
val invitedMemberCount = if (roomSummary?.invitedMembersCount != null) {
roomSummary.invitedMembersCount
} else {
currentRoomSummary?.invited_members_count ?: 0
}
val joinedMemberCount = if (roomSummary?.joinedMembersCount != null) {
roomSummary.joinedMembersCount
} else {
currentRoomSummary?.joined_members_count ?: 0
}
val highlightCount = unreadNotifications?.highlightCount ?: 0
val notificationCount = unreadNotifications?.notificationCount ?: 0
val membership = newMembership ?: currentRoomSummary?.membership?.map() ?: Membership.NONE
val latestPreviewableEventId = getLastestKnownEventId(roomId = roomId)
val lastTopicEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "").executeAsOneOrNull()
val lastCanonicalAliasEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "").executeAsOneOrNull()
val lastAliasesEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "").executeAsOneOrNull()
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
val encryptionEvent = sessionDatabase.eventQueries.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"").executeAsList().firstOrNull()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
val hasUnreadMessages = notificationCount > 0
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|| !sessionDatabase.isEventRead(userId, roomId, latestPreviewableEventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
val displayName = roomDisplayNameResolver.resolve(roomId, membership).toString()
val avatarUrl = roomAvatarResolver.resolve(roomId)
val topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
val canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.typingUserIds.clear()
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {
roomSummaryEntity.inviterId = inviterId
} else if (roomSummaryEntity.membership != Membership.INVITE) {
roomSummaryEntity.inviterId = null
sessionDatabase.roomAliasesQueries.deleteAllForRoom(roomId)
roomAliases.forEach { alias ->
sessionDatabase.roomAliasesQueries.insert(roomId, alias)
}
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
val isEncrypted = encryptionEvent != null
val directUserId = currentRoomSummary?.direct_user_id
sessionDatabase.userQueries.deleteAllTypingUsers()
ephemeralResult?.typingUserIds?.forEach { typingId ->
sessionDatabase.userQueries.insertTyping(roomId, typingId)
}
val isDirect = currentRoomSummary?.is_direct ?: false
if (updateMembers) {
val otherRoomMembers = RoomMemberHelper(realm, roomId)
.queryRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll()
.asSequence()
.map { it.userId }
roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) {
// The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val listToCheck = if (roomSummaryEntity.isDirect) {
roomSummaryEntity.otherMemberIds.toList()
} else {
roomSummaryEntity.otherMemberIds.toList() + userId
}
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
if (latestPreviewableEventId != null && latestPreviewableEventId.isNotEmpty()) {
if (selectEventType(latestPreviewableEventId) == EventType.ENCRYPTED
&& selectDecryptionResult(latestPreviewableEventId) == null) {
Timber.v("Should decrypt $latestPreviewableEventId for room: $displayName")
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEventId, ""))
}
}
}
fun updateShieldTrust(realm: Realm,
roomId: String,
trust: RoomEncryptionTrustLevel?) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummaryEntity.isEncrypted) {
roomSummaryEntity.roomEncryptionTrustLevel = trust
val newInviterId = if (membership == Membership.INVITE && inviterId != null) {
inviterId
} else if (membership != Membership.INVITE) {
null
} else {
currentRoomSummary?.inviter_id
}
val newRoomSummaryEntity = im.vector.matrix.sqldelight.session.RoomSummaryEntity.Impl(
room_id = roomId,
membership = membership.map(),
avatar_url = avatarUrl,
display_name = displayName,
invited_members_count = invitedMemberCount,
topic = topic,
joined_members_count = joinedMemberCount,
latest_previewable_event = latestPreviewableEventId,
is_direct = isDirect,
notification_count = notificationCount,
highlight_count = highlightCount,
canonical_alias = canonicalAlias,
is_encrypted = isEncrypted,
has_unread = hasUnreadMessages,
direct_user_id = directUserId,
versioning_state = currentRoomSummary?.versioning_state
?: VersioningState.NONE.name,
room_encryption_trust_level = currentRoomSummary?.room_encryption_trust_level,
inviter_id = newInviterId
)
sessionDatabase.roomSummaryQueries.insertOrUpdate(newRoomSummaryEntity)
if (isEncrypted && updateMembers) {
// The set of “all users” depends on the type of room:
// For regular / topic rooms, all users including yourself, are considered when decorating a room
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
val excludedIds = if (isDirect) {
listOf(userId)
} else {
emptyList()
}
val listToCheck = sessionDatabase.roomMemberSummaryQueries.getAllUserIdFromRoom(
memberships = Membership.activeMemberships().map(),
excludedIds = excludedIds,
roomId = roomId
).executeAsList()
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
}
}
private fun getLastestKnownEventId(roomId: String): String? {
return sessionDatabase.timelineEventQueries.getLatestKnownEventId(roomId = roomId, types = PREVIEWABLE_TYPES).executeAsOneOrNull()
?.takeIf { it.isNotBlank() }
}
private fun selectEventType(eventId: String): String? {
return sessionDatabase.eventQueries.selectType(eventId).executeAsOneOrNull()
}
private fun selectDecryptionResult(eventId: String): String? {
return sessionDatabase.eventQueries.selectDecryptionResult(eventId).executeAsOneOrNull()?.decryption_result_json
}
}

View file

@ -16,34 +16,14 @@
package im.vector.matrix.android.internal.session.room.notification
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.Condition
import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.pushrules.getActions
import im.vector.matrix.android.api.pushrules.*
import im.vector.matrix.android.api.pushrules.rest.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.toJson
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRuleEntity
internal fun PushRuleEntity.toRoomPushRule(): RoomPushRule? {
val kind = parent?.firstOrNull()?.kind
val pushRule = when (kind) {
RuleSetKey.OVERRIDE -> {
PushRulesMapper.map(this)
}
RuleSetKey.ROOM -> {
PushRulesMapper.mapRoomRule(this)
}
else -> null
}
return if (pushRule == null || kind == null) {
null
} else {
RoomPushRule(kind, pushRule)
}
}
internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? {
return when {

View file

@ -16,32 +16,26 @@
package im.vector.matrix.android.internal.session.room.read
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.read.ReadService
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.query.isEventRead
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.helper.isEventRead
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.flow.Flow
internal class DefaultReadService @AssistedInject constructor(
@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val sessionDatabase: SessionDatabase,
private val readMarkerDataSource: ReadMarkerDataSource,
private val readReceiptDataSource: ReadReceiptDataSource,
@UserId private val userId: String
) : ReadService {
@ -82,37 +76,19 @@ internal class DefaultReadService @AssistedInject constructor(
}
override fun isEventRead(eventId: String): Boolean {
return isEventRead(monarchy, userId, roomId, eventId)
return sessionDatabase.isEventRead(userId, roomId, eventId)
}
override fun getReadMarkerLive(): LiveData<Optional<String>> {
val liveRealmData = monarchy.findAllMappedWithChanges(
{ ReadMarkerEntity.where(it, roomId) },
{ it.eventId }
)
return Transformations.map(liveRealmData) {
it.firstOrNull().toOptional()
}
override fun getReadMarkerLive(): Flow<Optional<String>> {
return readMarkerDataSource.getReadMarkerLive(roomId)
}
override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
val liveRealmData = monarchy.findAllMappedWithChanges(
{ ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
{ it.eventId }
)
return Transformations.map(liveRealmData) {
it.firstOrNull().toOptional()
}
override fun getMyReadReceiptLive(): Flow<Optional<String>> {
return readReceiptDataSource.getReadReceiptLive(roomId, userId)
}
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveRealmData = monarchy.findAllMappedWithChanges(
{ ReadReceiptsSummaryEntity.where(it, eventId) },
{ readReceiptsSummaryMapper.map(it) }
)
return Transformations.map(liveRealmData) {
it.firstOrNull() ?: emptyList()
}
override fun getEventReadReceiptsLive(eventId: String): Flow<List<ReadReceipt>> {
return readReceiptDataSource.getEventReadReceiptsLive(eventId)
}
private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean {

View file

@ -168,7 +168,7 @@ internal class DefaultSendService @AssistedInject constructor(
override fun deleteFailedEcho(localEcho: TimelineEvent) {
taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, localEcho)
localEchoRepository.deleteFailedEcho(localEcho)
}
}
@ -197,7 +197,7 @@ internal class DefaultSendService @AssistedInject constructor(
eventsToResend.forEach {
sendEvent(it)
}
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
localEchoRepository.updateSendState(eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
}
}

View file

@ -116,7 +116,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint()
)
localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
//TODO
//localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
}
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)

View file

@ -43,9 +43,7 @@ import javax.inject.Inject
internal class LocalEchoRepository @Inject constructor(private val sessionDatabase: SessionDatabase,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val timelineEventMapper: TimelineEventMapper,
private val roomMemberSummaryDataSource: RoomMemberSummaryDataSource) {
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
suspend fun createLocalEcho(event: Event) {
val roomId = event.roomId

View file

@ -1,756 +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.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.database.query.whereInRoom
import im.vector.matrix.android.internal.database.query.whereRoomId
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.Debouncer
import im.vector.matrix.android.internal.util.createBackgroundHandler
import im.vector.matrix.android.internal.util.createUIHandler
import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
import java.util.Collections
import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.max
private const val MIN_FETCHING_COUNT = 30
internal class DefaultTimeline(
private val roomId: String,
private var initialEventId: String? = null,
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val eventBus: EventBus,
private val eventDecryptor: TimelineEventDecryptor
) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
data class OnLocalEchoCreated(val roomId: String, val timelineEvent: TimelineEvent)
companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
}
private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler()
private val backgroundRealm = AtomicReference<Realm>()
private val cancelableBag = CancelableBag()
private val debouncer = Debouncer(mainHandler)
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
private lateinit var eventRelations: RealmResults<EventAnnotationsSummaryEntity>
private var roomEntity: RoomEntity? = null
private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int? = null
private val inMemorySendingEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsState = AtomicReference(State())
private val forwardsState = AtomicReference(State())
override val timelineID = UUID.randomUUID().toString()
override val isLive
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<TimelineEventEntity>> { results, changeSet ->
if (!results.isLoaded || !results.isValid) {
return@OrderedRealmCollectionChangeListener
}
handleUpdates(results, changeSet)
}
private val relationsListener = OrderedRealmCollectionChangeListener<RealmResults<EventAnnotationsSummaryEntity>> { collection, changeSet ->
var hasChange = false
(changeSet.insertions + changeSet.changes).forEach {
val eventRelations = collection[it]
if (eventRelations != null) {
hasChange = rebuildEvent(eventRelations.eventId) { te ->
te.copy(annotations = eventRelations.asDomain())
} || hasChange
}
}
if (hasChange) postSnapshot()
}
// Public methods ******************************************************************************
override fun paginate(direction: Timeline.Direction, count: Int) {
BACKGROUND_HANDLER.post {
if (!canPaginate(direction)) {
return@post
}
Timber.v("Paginate $direction of $count items")
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
if (shouldPostSnapshot) {
postSnapshot()
}
}
}
override fun pendingEventCount(): Int {
return Realm.getInstance(realmConfiguration).use {
RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
}
}
override fun failedToDeliverEventCount(): Int {
return Realm.getInstance(realmConfiguration).use {
TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count()
}
}
override fun start() {
if (isStarted.compareAndSet(false, true)) {
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
eventBus.register(this)
BACKGROUND_HANDLER.post {
eventDecryptor.start()
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
roomEntity?.sendingTimelineEvents?.addChangeListener { events ->
// Remove in memory as soon as they are known by database
events.forEach { te ->
inMemorySendingEvents.removeAll { te.eventId == it.eventId }
}
postSnapshot()
}
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where()
.filterEventsWithSettings()
.findAll()
handleInitialLoad()
nonFilteredEvents.addChangeListener(eventsChangeListener)
eventRelations = EventAnnotationsSummaryEntity.whereInRoom(realm, roomId)
.findAllAsync()
.also { it.addChangeListener(relationsListener) }
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
}
isReady.set(true)
}
}
}
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filterEdits || filterTypes)
}
override fun dispose() {
if (isStarted.compareAndSet(true, false)) {
isReady.set(false)
eventBus.unregister(this)
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
cancelableBag.cancel()
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
BACKGROUND_HANDLER.post {
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
if (this::eventRelations.isInitialized) {
eventRelations.removeAllChangeListeners()
}
if (this::nonFilteredEvents.isInitialized) {
nonFilteredEvents.removeAllChangeListeners()
}
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.dispose()
}
clearAllValues()
backgroundRealm.getAndSet(null).also {
it?.close()
}
eventDecryptor.destroy()
}
}
}
override fun restartWithEventId(eventId: String?) {
dispose()
initialEventId = eventId
start()
postSnapshot()
}
override fun getTimelineEventAtIndex(index: Int): TimelineEvent? {
return builtEvents.getOrNull(index)
}
override fun getIndexOfEvent(eventId: String?): Int? {
return builtEventsIdMap[eventId]
}
override fun getTimelineEventWithId(eventId: String?): TimelineEvent? {
return builtEventsIdMap[eventId]?.let {
getTimelineEventAtIndex(it)
}
}
override fun getFirstDisplayableEventId(eventId: String): String? {
// If the item is built, the id is obviously displayable
val builtIndex = builtEventsIdMap[eventId]
if (builtIndex != null) {
return eventId
}
// Otherwise, we should check if the event is in the db, but is hidden because of filters
return Realm.getInstance(realmConfiguration).use { localRealm ->
val nonFilteredEvents = buildEventQuery(localRealm)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()
val nonFilteredEvent = nonFilteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst()
val filteredEvents = nonFilteredEvents.where().filterEventsWithSettings().findAll()
val isEventInDb = nonFilteredEvent != null
val isHidden = isEventInDb && filteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
.findFirst() == null
if (isHidden) {
val displayIndex = nonFilteredEvent?.displayIndex
if (displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = filteredEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
.findFirst()
firstDisplayedEvent?.eventId
} else {
null
}
} else {
null
}
}
}
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
override fun addListener(listener: Timeline.Listener): Boolean {
if (listeners.contains(listener)) {
return false
}
return listeners.add(listener).also {
postSnapshot()
}
}
override fun removeListener(listener: Timeline.Listener): Boolean {
return listeners.remove(listener)
}
override fun removeAllListeners() {
listeners.clear()
}
// TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(readReceipts = readReceipts)
}
}
override fun onReadReceiptsUpdated() {
postSnapshot()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onNewTimelineEvents(onNewTimelineEvents: OnNewTimelineEvents) {
if (isLive && onNewTimelineEvents.roomId == roomId) {
listeners.forEach {
it.onNewTimelineEvents(onNewTimelineEvents.eventIds)
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
if (isLive && onLocalEchoCreated.roomId == roomId) {
listeners.forEach {
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
}
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot()
}
}
// 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
}
} ?: false
}
private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache
private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd
private fun updateLoadingStates(results: RealmResults<TimelineEventEntity>) {
val lastCacheEvent = results.lastOrNull()
val lastBuiltEvent = builtEvents.lastOrNull()
val firstCacheEvent = results.firstOrNull()
val firstBuiltEvent = builtEvents.firstOrNull()
val chunkEntity = getLiveChunk()
updateState(Timeline.Direction.FORWARDS) {
it.copy(
hasMoreInCache = firstBuiltEvent == null || firstBuiltEvent.displayIndex < firstCacheEvent?.displayIndex ?: Int.MIN_VALUE,
hasReachedEnd = chunkEntity?.isLastForward ?: false
)
}
updateState(Timeline.Direction.BACKWARDS) {
it.copy(
hasMoreInCache = lastBuiltEvent == null || lastBuiltEvent.displayIndex > lastCacheEvent?.displayIndex ?: Int.MAX_VALUE,
hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
)
}
}
/**
* This has to be called on TimelineThread as it access realm live results
* @return true if createSnapshot should be posted
*/
private fun paginateInternal(startDisplayIndex: Int?,
direction: Timeline.Direction,
count: Int): Boolean {
updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
if (shouldFetchMore) {
val newRequestedCount = count - builtCount
updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) }
val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount)
executePaginationTask(direction, fetchingCount)
} else {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
}
return !shouldFetchMore
}
private fun createSnapshot(): List<TimelineEvent> {
return buildSendingEvents() + builtEvents.toList()
}
private fun buildSendingEvents(): List<TimelineEvent> {
val sendingEvents = ArrayList<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
sendingEvents.addAll(inMemorySendingEvents.filterEventsWithSettings())
roomEntity?.sendingTimelineEvents
?.where()
?.filterEventsWithSettings()
?.findAll()
?.forEach { timelineEventEntity ->
if (sendingEvents.find { it.eventId == timelineEventEntity.eventId } == null) {
sendingEvents.add(timelineEventMapper.map(timelineEventEntity))
}
}
}
return sendingEvents
}
private fun canPaginate(direction: Timeline.Direction): Boolean {
return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction)
}
private fun getState(direction: Timeline.Direction): State {
return when (direction) {
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.BACKWARDS -> backwardsState
}
val currentValue = stateReference.get()
val newValue = update(currentValue)
stateReference.set(newValue)
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun handleInitialLoad() {
var shouldFetchInitialEvent = false
val currentInitialEventId = initialEventId
val initialDisplayIndex = if (currentInitialEventId == null) {
nonFilteredEvents.firstOrNull()?.displayIndex
} else {
val initialEvent = nonFilteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
.findFirst()
shouldFetchInitialEvent = initialEvent == null
initialEvent?.displayIndex
}
prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex
if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId)
} else {
val count = filteredEvents.size.coerceAtMost(settings.initialSize)
if (initialEventId == null) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else {
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1))
paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1))
}
}
postSnapshot()
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
// If changeSet has deletion we are having a gap, so we clear everything
if (changeSet.deletionRanges.isNotEmpty()) {
clearAllValues()
}
var postSnapshot = false
changeSet.insertionRanges.forEach { range ->
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
} else {
Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
}
val state = getState(direction)
if (state.isPaginating) {
// We are getting new items from pagination
postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount)
} else {
// We are getting new items from sync
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
postSnapshot = true
}
}
changeSet.changes.forEach { index ->
val eventEntity = results[index]
eventEntity?.eventId?.let { eventId ->
postSnapshot = rebuildEvent(eventId) {
buildTimelineEvent(eventEntity)
} || postSnapshot
}
}
if (postSnapshot) {
postSnapshot()
}
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction)
if (token == null) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
return
}
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
.configureWith(params) {
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request")
}
TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot()
}
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
// Database won't be updated, so we force pagination request
BACKGROUND_HANDLER.post {
executePaginationTask(direction, limit)
}
}
}
override fun onFailure(failure: Throwable) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
postSnapshot()
Timber.v("Failure fetching $limit items $direction from pagination request")
}
}
}
.executeBy(taskExecutor)
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun getTokenLive(direction: Timeline.Direction): String? {
val chunkEntity = getLiveChunk() ?: return null
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
}
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun getLiveChunk(): ChunkEntity? {
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
}
/**
* This has to be called on TimelineThread as it access realm live results
* @return number of items who have been added
*/
private fun buildTimelineEvents(startDisplayIndex: Int?,
direction: Timeline.Direction,
count: Long): Int {
if (count < 1 || startDisplayIndex == null) {
return 0
}
val start = System.currentTimeMillis()
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
if (offsetResults.isEmpty()) {
return 0
}
val offsetIndex = offsetResults.last()!!.displayIndex
if (direction == Timeline.Direction.BACKWARDS) {
prevDisplayIndex = offsetIndex - 1
} else {
nextDisplayIndex = offsetIndex + 1
}
offsetResults.forEach { eventEntity ->
val timelineEvent = buildTimelineEvent(eventEntity)
val transactionId = timelineEvent.root.unsignedData?.transactionId
val sendingEvent = inMemorySendingEvents.find {
it.eventId == transactionId
}
inMemorySendingEvents.remove(sendingEvent)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) }
}
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
builtEvents.add(position, timelineEvent)
// Need to shift :/
builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) }
builtEventsIdMap[eventEntity.eventId] = position
}
val time = System.currentTimeMillis() - start
Timber.v("Built ${offsetResults.size} items from db in $time ms")
return offsetResults.size
}
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts,
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
)
/**
* This has to be called on TimelineThread as it access realm live results
*/
private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction,
count: Long): RealmResults<TimelineEventEntity> {
val offsetQuery = filteredEvents.where()
if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
}
return offsetQuery
.limit(count)
.findAll()
}
private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> {
return if (initialEventId == null) {
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
} else {
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.`in`("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID}", arrayOf(initialEventId))
}
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId)
cancelableBag += contextOfEventTask.configureWith(params) {
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
postSnapshot()
}
override fun onFailure(failure: Throwable) {
postFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
private fun postSnapshot() {
BACKGROUND_HANDLER.post {
if (isReady.get().not()) {
return@post
}
updateLoadingStates(filteredEvents)
val snapshot = createSnapshot()
val runnable = Runnable {
listeners.forEach {
it.onTimelineUpdated(snapshot)
}
}
debouncer.debounce("post_snapshot", runnable, 1)
}
}
private fun postFailure(throwable: Throwable) {
if (isReady.get().not()) {
return
}
val runnable = Runnable {
listeners.forEach {
it.onTimelineFailure(throwable)
}
}
mainHandler.post(runnable)
}
private fun clearAllValues() {
prevDisplayIndex = null
nextDisplayIndex = null
builtEvents.clear()
builtEventsIdMap.clear()
backwardsState.set(State())
forwardsState.set(State())
}
// Extension methods ***************************************************************************
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
}
private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> {
if (settings.filterTypes) {
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
}
if (settings.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE)
not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE)
}
return this
}
private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> {
return filter {
val filterType = if (settings.filterTypes) {
settings.allowedTypes.contains(it.root.type)
} else {
true
}
val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE
} else {
true
}
filterType && filterEdits
}
}
private data class State(
val hasReachedEnd: Boolean = false,
val hasMoreInCache: Boolean = true,
val isPaginating: Boolean = false,
val requestedPaginationCount: Int = 0
)
}

View file

@ -16,34 +16,18 @@
package im.vector.matrix.android.internal.session.room.timeline
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.fetchCopyMap
import org.greenrobot.eventbus.EventBus
import kotlinx.coroutines.flow.Flow
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy,
private val eventBus: EventBus,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
private val timelineEventDataSource: TimelineEventDataSource,
private val sqlTimelineFactory: SQLTimeline.Factory
) : TimelineService {
@AssistedInject.Factory
@ -52,37 +36,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
}
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline(
roomId = roomId,
initialEventId = eventId,
realmConfiguration = monarchy.realmConfiguration,
taskExecutor = taskExecutor,
contextOfEventTask = contextOfEventTask,
paginationTask = paginationTask,
timelineEventMapper = timelineEventMapper,
settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
eventBus = eventBus,
eventDecryptor = eventDecryptor
)
return sqlTimelineFactory.create(roomId, eventId, settings)
}
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy
.fetchCopyMap({
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
}, { entity, _ ->
timelineEventMapper.map(entity)
})
return timelineEventDataSource.getTimeLineEvent(eventId)
}
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
val liveData = monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
{ timelineEventMapper.map(it) }
)
return Transformations.map(liveData) { events ->
events.firstOrNull().toOptional()
}
override fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>> {
return timelineEventDataSource.getTimeLineEventLive(eventId)
}
}

View file

@ -20,13 +20,11 @@ import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.database.helper.setDecryptionResult
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
import im.vector.matrix.sqldelight.session.SessionDatabase
import okhttp3.internal.tryExecute
import timber.log.Timber
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -34,8 +32,7 @@ import javax.inject.Inject
@SessionScope
internal class TimelineEventDecryptor @Inject constructor(
@SessionDatabase
private val realmConfiguration: RealmConfiguration,
private val sessionDatabase: SessionDatabase,
private val cryptoService: CryptoService
) {
@ -93,31 +90,33 @@ internal class TimelineEventDecryptor @Inject constructor(
return
}
}
executor?.execute {
Realm.getInstance(realmConfiguration).use { realm ->
processDecryptRequest(request, realm)
}
executor?.tryExecute("process_decrypt_request") {
processDecryptRequest(request)
}
}
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
private fun processDecryptRequest(request: DecryptionRequest) {
val eventId = request.eventId
val timelineId = request.timelineId
Timber.v("Decryption request for event $eventId")
val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
?: return@executeTransaction Unit.also {
Timber.d("Decryption request for unknown message")
}
val eventEntity = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()
if (eventEntity == null) {
Timber.d("Decryption request for unknown message")
synchronized(existingRequests) {
existingRequests.remove(request)
}
return
}
val event = eventEntity.asDomain()
try {
val result = cryptoService.decryptEvent(event, timelineId)
Timber.v("Successfully decrypted event $eventId")
eventEntity.setDecryptionResult(result)
sessionDatabase.eventQueries.setDecryptionResult(result, eventId)
} catch (e: MXCryptoError) {
Timber.v(e, "Failed to decrypt event $eventId")
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// Keep track of unknown sessions to automatically try to decrypt on new session
eventEntity.decryptionErrorCode = e.errorType.name
sessionDatabase.eventQueries.setDecryptionError(e.errorType.name, eventId)
event.content?.toModel<EncryptedEventContent>()?.let { content ->
content.sessionId?.let { sessionId ->
synchronized(unknownSessionsFailure) {

View file

@ -249,7 +249,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val sessionD
val chunks = sessionDatabase.chunkQueriesChunkEntity.findAllIncludingEvents(realm, eventIds)
val chunksToDelete = ArrayList<ChunkEntity>()
chunks.forEach {
if (it != currentChunk) {
if (it != currentChunk) {s
currentChunk.merge(roomId, it, direction)
chunksToDelete.add(it)
}

View file

@ -16,26 +16,17 @@
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
import im.vector.matrix.android.internal.di.*
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.io.File
@ -54,13 +45,10 @@ internal class DefaultSignOutTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@RealmCryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
private val databaseKeysUtils: DatabaseKeysUtils,
@UserMd5 private val userMd5: String,
private val eventBus: EventBus
) : SignOutTask {
@ -96,9 +84,6 @@ internal class DefaultSignOutTask @Inject constructor(
Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(sessionId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit)
@ -107,17 +92,7 @@ internal class DefaultSignOutTask @Inject constructor(
sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
// Sanity check
if (BuildConfig.DEBUG) {
Realm.getGlobalInstanceCount(realmSessionConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
}
databaseKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
databaseKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
}
}

View file

@ -22,10 +22,16 @@ internal class Debouncer(private val handler: Handler) {
private val runnables = HashMap<String, Runnable>()
fun debounce(identifier: String, r: Runnable, millis: Long): Boolean {
// debounce
runnables[identifier]?.let { runnable -> handler.removeCallbacks(runnable) }
fun cancelAll() {
handler.removeCallbacksAndMessages(null)
}
fun debounce(identifier: String, r: Runnable, millis: Long): Boolean {
if (runnables.containsKey(identifier)) {
// debounce
val old = runnables[identifier]
handler.removeCallbacks(old)
}
insertRunnable(identifier, r, millis)
return true
}

View file

@ -271,6 +271,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"

View file

@ -20,8 +20,8 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import arrow.core.Option
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx
import im.vector.riotx.features.grouplist.ALL_COMMUNITIES_GROUP_ID
@ -29,11 +29,9 @@ import im.vector.riotx.features.grouplist.SelectedGroupDataSource
import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.addTo
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@ -62,32 +60,30 @@ class AppStateHandler @Inject constructor(
private fun observeRoomsAndGroup() {
Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
sessionDataSource.observe()
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
val query = roomSummaryQueryParams {}
it.orNull()?.rx()?.liveRoomSummaries(query)
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS),
.combineLatest<Option<Session>, Option<GroupSummary>, Pair<Option<Session>, Option<GroupSummary>>>(
sessionDataSource.observe(),
selectedGroupDataSource.observe(),
BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull()
val filteredRooms = rooms.filter {
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
true
} else if (it.isDirect) {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
} else {
selectedGroup.roomIds.contains(it.roomId)
}
}
filteredRooms.sortedWith(chronologicalRoomComparator)
BiFunction { sessionOption, selectedGroupOption ->
Pair(sessionOption, selectedGroupOption)
}
)
).switchMap {
val selectedGroup = it.second.orNull()
val session = it.first.orNull()
val queryParams = if (selectedGroup?.groupId == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
roomSummaryQueryParams()
} else {
roomSummaryQueryParams {
fromGroupId = selectedGroup.groupId
}
}
session
?.rx()
?.liveRoomSummaries(queryParams)
?: Observable.empty()
}
.map {
it.sortedWith(chronologicalRoomComparator)
}
.subscribe {
homeRoomListDataSource.post(it)
}

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.view.View
import androidx.lifecycle.asLiveData
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
@ -40,7 +41,7 @@ class HomeDrawerFragment @Inject constructor(
if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
}
session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
session.getUserLive(session.myUserId).asLiveData().observeK(viewLifecycleOwner) { optionalUser ->
val user = optionalUser?.getOrNull()
if (user != null) {
avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)

View file

@ -19,8 +19,8 @@ package im.vector.riotx.features.home.room.breadcrumbs
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.Breadcrumb
data class BreadcrumbsViewState(
val asyncBreadcrumbs: Async<List<RoomSummary>> = Uninitialized
val asyncBreadcrumbs: Async<List<Breadcrumb>> = Uninitialized
) : MvRxState

View file

@ -154,7 +154,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
.liveAnnotationSummary(eventId)
.map { annotations ->
EmojiDataSource.quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
ToggleState(emoji, annotations.reactionsSummary.firstOrNull { it.key == emoji }?.addedByMe ?: false)
}
}
.execute {

View file

@ -88,7 +88,6 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
private fun observeEventAnnotationSummaries() {
RxRoom(room)
.liveAnnotationSummary(eventId)
.unwrap()
.flatMapSingle { summaries ->
Observable
.fromIterable(summaries.reactionsSummary)