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 { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
// Warnings are potential errors, so stop ignoring them // 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 'androidx.appcompat:appcompat:1.1.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.3'
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.0" 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.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.Optional
import im.vector.matrix.android.api.util.toOptional
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import kotlinx.coroutines.rx2.asObservable import kotlinx.coroutines.rx2.asObservable
import timber.log.Timber
class RxRoom(private val room: Room) { class RxRoom(private val room: Room) {

View File

@ -16,8 +16,8 @@
package im.vector.matrix.android.api.session.room.timeline package im.vector.matrix.android.api.session.room.timeline
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.util.Optional 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. * 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 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.UploadKeysTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask 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.SessionFilesDirectory
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
@ -115,15 +116,15 @@ internal abstract class CryptoModule {
@JvmStatic @JvmStatic
@Provides @Provides
@im.vector.matrix.android.internal.di.CryptoDatabase @RealmCryptoDatabase
@SessionScope @SessionScope
fun providesRealmConfiguration(@SessionFilesDirectory directory: File, fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
@UserMd5 userMd5: String, @UserMd5 userMd5: String,
realmKeysUtils: RealmKeysUtils): RealmConfiguration { databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
.apply { .apply {
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5)) databaseKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
} }
.name("crypto_store.realm") .name("crypto_store.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
@ -166,8 +167,8 @@ internal abstract class CryptoModule {
@JvmStatic @JvmStatic
@Provides @Provides
@im.vector.matrix.android.internal.di.CryptoDatabase @RealmCryptoDatabase
fun providesClearCacheTask(@im.vector.matrix.android.internal.di.CryptoDatabase fun providesClearCacheTask(@RealmCryptoDatabase
realmConfiguration: RealmConfiguration): ClearCacheTask { realmConfiguration: RealmConfiguration): ClearCacheTask {
return RealmClearCacheTask(realmConfiguration) return RealmClearCacheTask(realmConfiguration)
} }

View File

@ -23,7 +23,6 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback 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.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService 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.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.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.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener 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.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel 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.keysbackup.DefaultKeysBackupService
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.*
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.event.EncryptedEventContent 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.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent 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.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse 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.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.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore 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.*
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.verification.DefaultVerificationService 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.repository.CurrentStateEventDataSource
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.whereType
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope 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.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -159,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy, private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
@ -178,16 +159,16 @@ internal class DefaultCryptoService @Inject constructor(
fun onStateEvent(roomId: String, event: Event) { fun onStateEvent(roomId: String, event: Event) {
when { when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(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_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }
fun onLiveEvent(roomId: String, event: Event) { fun onLiveEvent(roomId: String, event: Event) {
when { when {
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(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_MEMBER -> onRoomMembershipEvent(roomId, event)
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(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> { 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>> { override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
@ -508,7 +489,7 @@ internal class DefaultCryptoService @Inject constructor(
val alg: IMXEncrypting = when (algorithm) { val alg: IMXEncrypting = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
else -> olmEncryptionFactory.create(roomId) else -> olmEncryptionFactory.create(roomId)
} }
synchronized(roomEncryptors) { synchronized(roomEncryptors) {
@ -542,12 +523,10 @@ internal class DefaultCryptoService @Inject constructor(
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
*/ */
override fun isRoomEncrypted(roomId: String): Boolean { override fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm -> return sessionDatabase.eventQueries
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) .findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .executeAsList()
.findFirst() .firstOrNull() != null
}
return encryptionEvent != null
} }
/** /**
@ -706,17 +685,17 @@ internal class DefaultCryptoService @Inject constructor(
onRoomKeyEvent(event) onRoomKeyEvent(event)
} }
EventType.REQUEST_SECRET, EventType.REQUEST_SECRET,
EventType.ROOM_KEY_REQUEST -> { EventType.ROOM_KEY_REQUEST -> {
// save audit trail // save audit trail
cryptoStore.saveGossipingEvent(event) cryptoStore.saveGossipingEvent(event)
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete) // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
incomingGossipingRequestManager.onGossipingRequestEvent(event) incomingGossipingRequestManager.onGossipingRequestEvent(event)
} }
EventType.SEND_SECRET -> { EventType.SEND_SECRET -> {
cryptoStore.saveGossipingEvent(event) cryptoStore.saveGossipingEvent(event)
onSecretSendReceived(event) onSecretSendReceived(event)
} }
else -> { else -> {
// ignore // ignore
} }
} }
@ -767,30 +746,19 @@ internal class DefaultCryptoService @Inject constructor(
return return
} }
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { when (existingRequest.secretName) {
// 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) {
SELF_SIGNING_KEY_SSSS_NAME -> { SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretValue) crossSigningService.onSecretSSKGossip(secretContent.secretValue)
true return
} }
USER_SIGNING_KEY_SSSS_NAME -> { USER_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretUSKGossip(secretValue) crossSigningService.onSecretUSKGossip(secretContent.secretValue)
true return
} }
KEYBACKUP_SECRET_SSSS_NAME -> { else -> {
keysBackupService.onSecretKeyGossip(secretValue) // Ask to application layer?
true Timber.v("## onSecretSend() : secret not handled by SDK")
} }
else -> false
} }
} }
@ -804,29 +772,24 @@ internal class DefaultCryptoService @Inject constructor(
val params = LoadRoomMembersTask.Params(roomId) val params = LoadRoomMembersTask.Params(roomId)
try { try {
loadRoomMembersTask.execute(params) loadRoomMembersTask.execute(params)
} catch (throwable: Throwable) {
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
} finally {
val userIds = getRoomUserIds(roomId) val userIds = getRoomUserIds(roomId)
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds) 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> { private fun getRoomUserIds(roomId: String): List<String> {
var userIds: List<String> = emptyList() // Check whether the event content must be encrypted for the invited members.
monarchy.doWithRealm { realm -> val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
// Check whether the event content must be encrypted for the invited members. && shouldEncryptForInvitedMembers(roomId)
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId)
userIds = if (encryptForInvitedMembers) { return if (encryptForInvitedMembers) {
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() RoomMemberHelper(sessionDatabase, roomId).getActiveRoomMemberIds()
} else { } else {
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds() 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.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate 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.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.MoshiProvider
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -91,7 +91,7 @@ import kotlin.collections.set
@SessionScope @SessionScope
internal class RealmCryptoStore @Inject constructor( internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration, @RealmCryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore { 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.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError 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.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.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.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.*
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.internal.crypto.algorithms.olm.OlmDecryptionResult 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.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.DeviceId
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.Task 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 timber.log.Timber
import java.util.ArrayList import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> { internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
data class Params( data class Params(
val events: List<Event>, val eventInsertNotifications: List<EventInsertNotification>
val verificationService: DefaultVerificationService,
val cryptoService: CryptoService
) )
} }
internal class DefaultRoomVerificationUpdateTask @Inject constructor( internal class DefaultRoomVerificationUpdateTask @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
@DeviceId private val deviceId: String?, @DeviceId private val deviceId: String?,
private val sessionDatabase: SessionDatabase,
private val verificationService: DefaultVerificationService,
private val cryptoService: CryptoService) : RoomVerificationUpdateTask { private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
companion object { companion object {
@ -57,8 +55,15 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
override suspend fun execute(params: RoomVerificationUpdateTask.Params) { override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
// TODO ignore initial sync or back pagination? // 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}") 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, // 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? // TODO use a global event decryptor? attache to session and that listen to new sessionId?
// for now decrypt sync // for now decrypt sync
try { try {
val result = cryptoService.decryptEvent(event, "") val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, senderKey = result.senderCurve25519Key,
@ -83,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
) )
} catch (e: MXCryptoError) { } catch (e: MXCryptoError) {
Timber.e("## SAS Failed to decrypt event: ${event.eventId}") 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()}") 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 // The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
params.verificationService.onRoomRequestHandledByOtherDevice(event) verificationService.onRoomRequestHandledByOtherDevice(event)
} }
} }
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) { } else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
@ -121,13 +126,13 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
// The verification is started from another device // The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ") Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) } 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()) { } else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
relatesToEventId?.let { relatesToEventId?.let {
transactionsHandledByOtherDevice.remove(it) 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_CANCEL,
EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_DONE -> { EventType.KEY_VERIFICATION_DONE -> {
params.verificationService.onRoomEvent(event) verificationService.onRoomEvent(event)
} }
EventType.MESSAGE -> { EventType.MESSAGE -> {
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) { 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 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.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.DefaultRoomVerificationUpdateTask
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.sqldelight.session.SessionDatabase
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 javax.inject.Inject import javax.inject.Inject
internal class VerificationMessageLiveObserver @Inject constructor( internal class VerificationMessageLiveObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration, sessionDatabase: SessionDatabase,
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask, private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask
private val cryptoService: CryptoService, ) : SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
private val verificationService: DefaultVerificationService,
private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query { override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
EventEntity.whereTypes(it, listOf( types = listOf(
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_READY, EventType.KEY_VERIFICATION_READY,
EventType.MESSAGE, EventType.MESSAGE,
EventType.ENCRYPTED) 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) else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
val eventEntity = EventEntity() val eventEntity = EventEntity()
// TODO change this as we shouldn't use event everywhere // 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.roomId = event.roomId ?: roomId
eventEntity.content = ContentMapper.map(event.content) eventEntity.content = ContentMapper.map(event.content)
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
@ -48,6 +49,34 @@ internal object EventMapper {
return eventEntity 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 { fun map(eventEntity: EventEntity): Event {
val ud = eventEntity.unsignedData val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() } ?.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 { internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this) 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 { internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
return EventMapper.map(this, roomId).apply { return EventMapper.map(this, roomId).apply {
this.sendState = sendState this.sendState = sendState

View File

@ -17,14 +17,16 @@ package im.vector.matrix.android.internal.database.mapper
import com.squareup.moshi.Types import com.squareup.moshi.Types
import im.vector.matrix.android.api.pushrules.Condition 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.PushCondition
import im.vector.matrix.android.api.pushrules.rest.PushRule 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 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 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)) 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 { fun mapContentRule(pushrule: PushRuleEntity): PushRule {
return PushRule( return PushRule(
actions = fromActionStr(pushrule.actionsStr), actions = fromActionStr(pushrule.action_str),
default = pushrule.default, default = pushrule.is_default,
enabled = pushrule.enabled, enabled = pushrule.is_enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.rule_id,
conditions = listOf( conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern) PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
) )
@ -44,58 +46,60 @@ internal object PushRulesMapper {
} }
private fun fromActionStr(actionsStr: String?): List<Any> { private fun fromActionStr(actionsStr: String?): List<Any> {
try { return try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList() actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>") Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList() emptyList()
} }
} }
fun mapRoomRule(pushrule: PushRuleEntity): PushRule { fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
return PushRule( return PushRule(
actions = fromActionStr(pushrule.actionsStr), actions = fromActionStr(pushrule.action_str),
default = pushrule.default, default = pushrule.is_default,
enabled = pushrule.enabled, enabled = pushrule.is_enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.rule_id,
conditions = listOf( 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 { fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
return PushRule( return PushRule(
actions = fromActionStr(pushrule.actionsStr), actions = fromActionStr(pushrule.action_str),
default = pushrule.default, default = pushrule.is_default,
enabled = pushrule.enabled, enabled = pushrule.is_enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.rule_id,
conditions = listOf( 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( return PushRule(
actions = fromActionStr(pushrule.actionsStr), actions = fromActionStr(pushrule.action_str),
default = pushrule.default, default = pushrule.is_default,
enabled = pushrule.enabled, enabled = pushrule.is_enabled,
ruleId = pushrule.ruleId, ruleId = pushrule.rule_id,
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) } conditions = conditions.map {
pushConditionMapper.map(it)
}
) )
} }
fun map(pushRule: PushRule): PushRuleEntity { fun map(scope: String, kind: RuleKind, pushRule: PushRule): PushRuleEntity {
return PushRuleEntity( return PushRuleEntity.Impl(
actionsStr = moshiActionsAdapter.toJson(pushRule.actions), action_str = moshiActionsAdapter.toJson(pushRule.actions),
default = pushRule.default ?: false, is_default = pushRule.default ?: false,
enabled = pushRule.enabled, is_enabled = pushRule.enabled,
ruleId = pushRule.ruleId, rule_id = pushRule.ruleId,
pattern = pushRule.pattern, pattern = pushRule.pattern,
conditions = pushRule.conditions?.let { scope = scope,
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray()) kind = kind.name
} ?: RealmList()
) )
} }
} }

View File

@ -16,40 +16,87 @@
package im.vector.matrix.android.internal.database.mapper 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.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.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject 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 { fun map(cursor: SqlCursor): TimelineEvent = map(
val readReceipts = if (buildReadReceipts) { cursor.getLong(0)!!,
correctedReadReceipts ?: timelineEventEntity.readReceipts cursor.getLong(1)!!,
?.let { cursor.getLong(2)!!.toInt(),
readReceiptsSummaryMapper.map(it) cursor.getString(3),
} cursor.getString(4),
} else { cursor.getLong(5)!! == 1L,
null 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( return TimelineEvent(
root = timelineEventEntity.root?.asDomain() root = event,
?: Event("", timelineEventEntity.eventId), eventId = event_id,
eventId = timelineEventEntity.eventId, annotations = null,
annotations = timelineEventEntity.annotations?.asDomain(), displayIndex = display_index,
localId = timelineEventEntity.localId, isUniqueDisplayName = is_unique_display_name,
displayIndex = timelineEventEntity.displayIndex, localId = local_id,
senderName = timelineEventEntity.senderName, readReceipts = emptyList(),
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, senderAvatar = sender_avatar,
senderAvatar = timelineEventEntity.senderAvatar, senderName = sender_name
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
) )
} }
} }

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import dagger.Module import dagger.Module
import dagger.Provides 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.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.createBackgroundHandler import im.vector.matrix.android.internal.util.createBackgroundHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,11 +37,14 @@ internal object MatrixModule {
@Provides @Provides
@MatrixScope @MatrixScope
fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers { 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, computation = Dispatchers.Default,
main = Dispatchers.Main, main = Dispatchers.Main,
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(), 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 package im.vector.matrix.android.internal.session.group
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import com.zhuinden.monarchy.Monarchy import com.squareup.sqldelight.Query
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.awaitTransaction 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.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider 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 im.vector.matrix.android.internal.worker.WorkerParamsFactory
import io.realm.OrderedCollectionChangeSet import im.vector.matrix.sqldelight.session.Memberships
import io.realm.RealmResults import im.vector.matrix.sqldelight.session.SessionDatabase
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
internal class GroupSummaryUpdater @Inject constructor( internal class GroupSummaryUpdater @Inject constructor(
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
@SessionId private val sessionId: String, @SessionId private val sessionId: String,
private val monarchy: Monarchy) sessionDatabase: SessionDatabase)
: RealmLiveEntityObserver<GroupEntity>(monarchy.realmConfiguration) { : 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) { override suspend fun handleChanges(results: List<String>) {
// `insertions` for new groups and `changes` to handle left groups val groupIdsToFetchData = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
val modifiedGroupEntity = (changeSet.insertions + changeSet.changes) groupIds = results,
.asSequence() memberships = listOf(Memberships.JOIN, Memberships.INVITE)
.mapNotNull { results[it] } ).executeAsList()
fetchGroupsData(groupIdsToFetchData)
fetchGroupsData(modifiedGroupEntity val groupIdsToDelete = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE } groupIds = results,
.map { it.groupId } memberships = listOf(Memberships.LEAVE)
.toList()) ).executeAsList()
modifiedGroupEntity sessionDatabase.awaitTransaction(coroutineDispatchers) {
.filter { it.membership == Membership.LEAVE } sessionDatabase.groupSummaryQueries.deleteGroupSummaries(groupIdsToDelete)
.map { it.groupId } sessionDatabase.observerTriggerQueries.deleteGroupNotifications(results)
.toList() }
.also {
observerScope.launch {
deleteGroups(it)
}
}
} }
private fun fetchGroupsData(groupIds: List<String>) { private fun fetchGroupsData(groupIds: List<String>) {
@ -79,12 +73,4 @@ internal class GroupSummaryUpdater @Inject constructor(
.enqueue() .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 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.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task 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 org.greenrobot.eventbus.EventBus
import java.util.Date import java.util.*
import javax.inject.Inject import javax.inject.Inject
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit> internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI, private val capabilitiesAPI: CapabilitiesAPI,
private val monarchy: Monarchy, private val sessionDatabase: SessionDatabase,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val eventBus: EventBus private val eventBus: EventBus
) : GetHomeServerCapabilitiesTask { ) : GetHomeServerCapabilitiesTask {
override suspend fun execute(params: Unit) { override suspend fun execute(params: Unit) {
var doRequest = false val lastUpdatedTs = sessionDatabase.homeServerCapabilitiesQueries.selectLastUpdatedTimetamp().executeAsOneOrNull()
monarchy.awaitTransaction { realm -> val doRequest = lastUpdatedTs == null || lastUpdatedTs + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
}
if (!doRequest) { if (!doRequest) {
return return
} }
val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult>(eventBus) { val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult>(eventBus) {
apiCall = capabilitiesAPI.getUploadCapabilities() apiCall = capabilitiesAPI.getUploadCapabilities()
} }
val capabilities = runCatching {
executeRequest<GetCapabilitiesResult>(eventBus) {
apiCall = capabilitiesAPI.getCapabilities()
}
}.getOrNull()
// TODO Add other call here (get version, etc.) // TODO Add other call here (get version, etc.)
insertInDb(uploadCapabilities)
insertInDb(capabilities, uploadCapabilities)
} }
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) { private suspend fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
monarchy.awaitTransaction { realm -> sessionDatabase.awaitTransaction(coroutineDispatchers) {
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm) val maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN ?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
val lastUpdatedTimestamp = Date().time
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time it.homeServerCapabilitiesQueries.insert(maxUploadFileSize, lastUpdatedTimestamp)
} }
} }

View File

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

View File

@ -15,22 +15,18 @@
*/ */
package im.vector.matrix.android.internal.session.notification 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.MatrixCallback
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.pushrules.RuleKind 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.getActions
import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.RuleSet 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.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable 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.SessionScope
import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask 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.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.RemovePushRuleTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleActionsTask import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleActionsTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask
@ -44,10 +40,10 @@ internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask, private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask, private val addPushRuleTask: AddPushRuleTask,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask,
private val removePushRuleTask: RemovePushRuleTask, private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy private val pushRuleDataSource: PushRuleDataSource,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask
) : PushRuleService { ) : PushRuleService {
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>() private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()
@ -59,47 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
} }
override fun getPushRules(scope: String): RuleSet { override fun getPushRules(scope: String): RuleSet {
var contentRules: List<PushRule> = emptyList() return pushRuleDataSource.getPushRules(scope)
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
)
} }
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable { 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.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.pushers.PusherState 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.model.PusherEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest 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.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent import im.vector.matrix.android.internal.worker.getSessionComponent
import im.vector.matrix.sqldelight.session.SessionDatabase
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
@ -44,14 +44,20 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var pushersAPI: PushersAPI @Inject
@Inject lateinit var monarchy: Monarchy lateinit var pushersAPI: PushersAPI
@Inject lateinit var eventBus: EventBus @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 { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this) sessionComponent.inject(this)
@ -67,15 +73,12 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
} catch (exception: Throwable) { } catch (exception: Throwable) {
when (exception) { when (exception) {
is Failure.NetworkConnection -> Result.retry() is Failure.NetworkConnection -> Result.retry()
else -> { else -> {
monarchy.awaitTransaction { realm -> sessionDatabase.awaitTransaction(coroutineDispatchers) {
PusherEntity.where(realm, pusher.pushKey).findFirst()?.let { sessionDatabase.pushersQueries.updateState(PusherState.FAILED_TO_REGISTER.name, pusher.pushKey)
// update it
it.state = PusherState.FAILED_TO_REGISTER
}
} }
// always return success, or the chain will be stuck for ever! // 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) { executeRequest<Unit>(eventBus) {
apiCall = pushersAPI.setPusher(pusher) apiCall = pushersAPI.setPusher(pusher)
} }
monarchy.awaitTransaction { realm -> sessionDatabase.awaitTransaction(coroutineDispatchers) {
val echo = PusherEntity.where(realm, pusher.pushKey).findFirst() val pusherEntity = pushersMapper.map(pusher, PusherState.REGISTERED)
if (echo != null) { sessionDatabase.pushersQueries.insertOrReplace(pusherEntity)
// 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)
}
}
} }
} }
} }

View File

@ -60,9 +60,6 @@ internal abstract class PushersModule {
@Binds @Binds
abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask
@Binds
abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask
@Binds @Binds
abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask 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 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.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError 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.*
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.room.model.PollSummaryContent 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.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.model.VoteInfo 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.api.session.room.model.relation.ReactionContent
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult 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.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.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper 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.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.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.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.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.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 io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -56,8 +46,7 @@ import javax.inject.Inject
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> { internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
data class Params( data class Params(
val events: List<Event>, val eventInsertNotifications: List<EventInsertNotification>
val userId: String
) )
} }
@ -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. * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/ */
internal class DefaultEventRelationsAggregationTask @Inject constructor( 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 { private val cryptoService: CryptoService) : EventRelationsAggregationTask {
// OPT OUT serer aggregation until API mature enough // OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false private val SHOULD_HANDLE_SERVER_AGREGGATION = false
override suspend fun execute(params: EventRelationsAggregationTask.Params) { override suspend fun execute(params: EventRelationsAggregationTask.Params) {
val events = params.events val eventInsertNotifications = params.eventInsertNotifications
val userId = params.userId sessionDatabase.awaitTransaction(coroutineDispatchers) {
monarchy.awaitTransaction { realm -> Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${eventInsertNotifications.size} events")
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events") update(eventInsertNotifications, userId)
update(realm, events, userId)
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished") Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
} }
} }
private fun update(realm: Realm, events: List<Event>, userId: String) { private fun update(eventInsertNotifications: List<EventInsertNotification>, userId: String) {
events.forEach { event -> eventInsertNotifications.forEach { eventInsertNotification ->
try { // Temporary catch, should be removed try { // Temporary catch, should be removed
val roomId = event.roomId val roomId = eventInsertNotification.room_id
if (roomId == null) { val eventId = eventInsertNotification.event_id
Timber.w("Event has no room id ${event.eventId}") val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
return@forEach val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
} ?: return@forEach
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") when (eventInsertNotification.type) {
when (event.type) { EventType.REACTION -> {
EventType.REACTION -> {
// we got a reaction!! // we got a reaction!!
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") Timber.v("###REACTION in room $roomId , reaction eventID ${eventId}")
handleReaction(event, roomId, realm, userId, isLocalEcho) handleReaction(event, roomId, userId, isLocalEcho)
} }
EventType.MESSAGE -> {
EventType.MESSAGE -> {
if (event.unsignedData?.relations?.annotations != null) { if (event.unsignedData?.relations?.annotations != null) {
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}") Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm) //handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations)
EventAnnotationsSummaryEntity.where(realm, event.eventId
?: "").findFirst()?.let {
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
?: "").findFirst()?.let { tet ->
tet.annotations = it
}
}
} }
val content: MessageContent? = event.content.toModel() val content: MessageContent? = event.content.toModel()
if (content?.relatesTo?.type == RelationType.REPLACE) { if (content?.relatesTo?.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // A replace!
handleReplace(realm, event, content, roomId, isLocalEcho) handleReplace(event, content, roomId, isLocalEcho)
} else if (content?.relatesTo?.type == RelationType.RESPONSE) { } else if (content?.relatesTo?.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") 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}") Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
event.content.toModel<MessageRelationContent>()?.relatesTo?.let { event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
if (it.type == RelationType.REFERENCE && it.eventId != null) { 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 // Relation type is in clear
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
@ -175,10 +157,10 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}") Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
// A replace! // 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) { } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") 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) { } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
@ -193,33 +175,33 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
EventType.KEY_VERIFICATION_KEY -> { EventType.KEY_VERIFICATION_KEY -> {
Timber.v("## SAS REF in room $roomId for event ${event.eventId}") Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
encryptedEventContent.relatesTo.eventId?.let { encryptedEventContent.relatesTo.eventId?.let {
handleVerification(realm, event, roomId, isLocalEcho, it, userId) handleVerification(event, roomId, isLocalEcho, it, userId)
} }
} }
} }
} }
} }
EventType.REDACTION -> { EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } val eventToPrune = event.redacts?.let { sessionDatabase.eventQueries.select(event.redacts).executeAsOneOrNull() }
?: return@forEach ?: return@forEach
when (eventToPrune.type) { when (eventToPrune.type) {
EventType.MESSAGE -> { EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.event_id}")
// val unsignedData = EventMapper.map(eventToPrune).unsignedData // val unsignedData = EventMapper.map(eventToPrune).unsignedData
// ?: UnsignedData(null, null) // ?: UnsignedData(null, null)
// was this event a m.replace // was this event a m.replace
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) { if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm) handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, roomId)
} }
} }
EventType.REACTION -> { 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) { } catch (t: Throwable) {
Timber.e(t, "## Should not happen ") Timber.e(t, "## Should not happen ")
@ -230,7 +212,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
private fun decryptIfNeeded(event: Event) { private fun decryptIfNeeded(event: Event) {
if (event.mxDecryptionResult == null) { if (event.mxDecryptionResult == null) {
try { try {
val result = cryptoService.decryptEvent(event, "") val result = cryptoService.decryptEvent(event, event.roomId ?: "")
event.mxDecryptionResult = OlmDecryptionResult( event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent, payload = result.clearEvent,
senderKey = result.senderCurve25519Key, 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 eventId = event.eventId ?: return
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return val newContent = content.newContent ?: return
// ok, this is a replace
val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId)
// we have it // we have it
val existingSummary = existing.editSummary val existingSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(targetEventId).executeAsOneOrNull()
if (existingSummary == null) { if (existingSummary == null) {
Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)") Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)")
// create the edit summary // create the edit summary
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java) val lastEditTs: Long
editSummary.aggregatedContent = ContentMapper.map(newContent) val sourceLocalEchoEvents: List<String>
val sourceEvents: List<String>
if (isLocalEcho) { if (isLocalEcho) {
editSummary.lastEditTs = 0 lastEditTs = 0
editSummary.sourceLocalEchoEvents.add(eventId) sourceLocalEchoEvents = listOf(eventId)
sourceEvents = emptyList()
} else { } else {
editSummary.lastEditTs = event.originServerTs ?: 0 lastEditTs = event.originServerTs ?: 0
editSummary.sourceEvents.add(eventId) sourceLocalEchoEvents = emptyList()
sourceEvents = listOf(eventId)
} }
val newEditSummary = EditAggregatedSummary.Impl(
existing.editSummary = editSummary 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 { } else {
if (existingSummary.sourceEvents.contains(eventId)) { val sourceEvents = existingSummary.source_event_ids.toMutableList()
if (sourceEvents.contains(eventId)) {
// ignore this event, we already know it (??) // ignore this event, we already know it (??)
Timber.v("###REPLACE ignoring event for summary, it's known $eventId") Timber.v("###REPLACE ignoring event for summary, it's known $eventId")
return return
} }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
// is it a remote echo? // 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 // ok it has already been managed
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)") Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
existingSummary.sourceLocalEchoEvents.remove(txId) sourceLocalEchoEvents.remove(txId)
existingSummary.sourceEvents.add(event.eventId) sourceEvents.add(event.eventId)
} else if ( } else if (
isLocalEcho // do not rely on ts for local echo, take it 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)") Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
if (!isLocalEcho) { if (!isLocalEcho) {
// Do not take local echo originServerTs here, could mess up ordering (keep old ts) // 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) { if (isLocalEcho) {
existingSummary.sourceLocalEchoEvents.add(eventId) sourceLocalEchoEvents.add(eventId)
} else { } else {
existingSummary.sourceEvents.add(eventId) sourceEvents.add(eventId)
} }
} else { } else {
// ignore this event for the summary (back paginate) // ignore this event for the summary (back paginate)
if (!isLocalEcho) { if (!isLocalEcho) {
existingSummary.sourceEvents.add(eventId) sourceEvents.add(eventId)
} }
Timber.v("###REPLACE ignoring event for summary, it's to old $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, private fun handleResponse(userId: String,
userId: String,
event: Event, event: Event,
content: MessageContent, content: MessageContent,
roomId: String, roomId: String,
@ -317,45 +315,38 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val eventTimestamp = event.originServerTs ?: return val eventTimestamp = event.originServerTs ?: return
// ok, this is a poll response val existingPollSummary = sessionDatabase.eventAnnotationsQueries.selectPollForEvent(targetEventId).executeAsOneOrNull()
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() val closedTime = existingPollSummary?.closed_time
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
if (closedTime != null && eventTimestamp > closedTime) { if (closedTime != null && eventTimestamp > closedTime) {
Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
return return
} }
val sumModel = ContentMapper.map(existingPollSummary?.content).toModel<PollSummaryContent>()
?: PollSummaryContent()
val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent() val sourceEvents = existingPollSummary?.source_event_ids?.toMutableList() ?: ArrayList()
if (sourceEvents.contains(eventId)) {
if (existingPollSummary!!.sourceEvents.contains(eventId)) {
// ignore this event, we already know it (??) // ignore this event, we already know it (??)
Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId")
return return
} }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
// is it a remote echo? // 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 // ok it has already been managed
Timber.v("## POLL Receiving remote echo of response eventId:$eventId") Timber.v("## POLL Receiving remote echo of response eventId:$eventId")
existingPollSummary.sourceLocalEchoEvents.remove(txId) sourceLocalEchoEvents.remove(txId)
existingPollSummary.sourceEvents.add(event.eventId) sourceEvents.add(event.eventId)
sessionDatabase.eventAnnotationsQueries.updatePollSources(sourceEvents, sourceLocalEchoEvents, targetEventId)
return return
} }
val responseContent = event.content.toModel<MessagePollResponseContent>() ?: return Unit.also { val responseContent = event.content.toModel<MessagePollResponseContent>()
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}") ?: return Unit.also {
} Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
}
val optionIndex = responseContent.relatesTo?.option ?: return Unit.also { val optionIndex = responseContent.relatesTo?.option ?: return Unit.also {
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") 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 sumModel.votes = votes
if (isLocalEcho) { if (isLocalEcho) {
existingPollSummary.sourceLocalEchoEvents.add(eventId) sourceLocalEchoEvents.add(eventId)
} else { } else {
existingPollSummary.sourceEvents.add(eventId) sourceEvents.add(eventId)
} }
val newContent = ContentMapper.map(sumModel.toContent())
existingPollSummary.aggregatedContent = 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) { 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>() val content = event.content.toModel<ReactionContent>()
if (content == null) { if (content == null) {
Timber.e("Malformed reaction content ${event.content}") Timber.e("Malformed reaction content ${event.content}")
@ -425,50 +417,70 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
if (RelationType.ANNOTATION == content.relatesTo?.type) { if (RelationType.ANNOTATION == content.relatesTo?.type) {
val reaction = content.relatesTo.key val reaction = content.relatesTo.key
val relatedEventID = content.relatesTo.eventId val relatedEventID = content.relatesTo.eventId
val reactionEventId = event.eventId val reactionEventId = event.eventId ?: return
Timber.v("Reaction $reactionEventId relates to $relatedEventID") Timber.v("Reaction $reactionEventId relates to $relatedEventID")
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID) val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(relatedEventID, reaction).executeAsOneOrNull()
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
val txId = event.unsignedData?.transactionId val txId = event.unsignedData?.transactionId
if (isLocalEcho && txId.isNullOrBlank()) { if (isLocalEcho && txId.isNullOrBlank()) {
Timber.w("Received a local echo with no transaction ID") Timber.w("Received a local echo with no transaction ID")
return
} }
if (sum == null) { if (reactionSummary == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java) Timber.v("$reaction is a new reaction")
sum.key = reaction val (sourceEventIds, sourceLocalEchoIds) = if (isLocalEcho && txId != null) {
sum.firstTimestamp = event.originServerTs ?: 0
if (isLocalEcho) {
Timber.v("Adding local echo reaction $reaction") Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId) Pair(emptyList(), listOf(txId))
sum.count = 1
} else { } else {
Timber.v("Adding synced reaction $reaction") Timber.v("Adding synced reaction $reaction")
sum.count = 1 Pair(listOf(reactionEventId), emptyList<String>())
sum.sourceEvents.add(reactionEventId)
} }
sum.addedByMe = sum.addedByMe || (userId == event.senderId) val newReactionSummary = ReactionAggregatedSummary.Impl(
eventSummary.reactionsSummary.add(sum) 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 { } else {
Timber.v("$reaction is an already known reaction")
// is this a known event (is possible? pagination?) // 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 // 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 // ok it has already been counted, just sync the list, do not touch count
Timber.v("Ignoring synced of local echo for reaction $reaction") Timber.v("Ignoring synced of local echo for reaction $reaction")
sum.sourceLocalEcho.remove(txId) sourceLocalEcho.remove(txId)
sum.sourceEvents.add(reactionEventId) sourceEvents.add(reactionEventId)
sessionDatabase.eventAnnotationsQueries.updateLocalReaction(
sourceEventIds = sourceEvents,
sourceLocalEchoIds = sourceLocalEcho,
eventId = relatedEventID,
key = reaction
)
} else { } else {
sum.count += 1 val newCount = reactionSummary.count + 1
if (isLocalEcho) { val newAddedByMe = reactionSummary.added_by_me || (userId == event.senderId)
if (isLocalEcho && txId != null) {
Timber.v("Adding local echo reaction $reaction") Timber.v("Adding local echo reaction $reaction")
sum.sourceLocalEcho.add(txId) sourceLocalEcho.add(txId)
} else { } else {
Timber.v("Adding synced reaction $reaction") Timber.v("Adding synced reaction $reaction")
sum.sourceEvents.add(reactionEventId) sourceEvents.add(reactionEventId)
} }
sessionDatabase.eventAnnotationsQueries.updateReaction(
sum.addedByMe = sum.addedByMe || (userId == event.senderId) 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 * 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") Timber.d("Handle redaction of m.replace")
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst() val editSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(relatedEventId).executeAsOneOrNull()
if (eventSummary == null) { if (editSummary == null) {
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId") Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
return return
} }
val sourceEvents = eventSummary.editSummary?.sourceEvents val sourceEvents = editSummary.source_event_ids.toMutableList()
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId) val sourceToDiscard = sourceEvents.indexOf(redacted.event_id)
if (sourceToDiscard == null) { if (sourceToDiscard == -1) {
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard") Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
return return
} }
// Need to remove this event from the redaction list and compute new aggregation state // Need to remove this event from the redaction list and compute new aggregation state
sourceEvents.removeAt(sourceToDiscard) 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) { if (previousEdit == null) {
// revert to original // revert to original
eventSummary.editSummary?.deleteFromRealm() sessionDatabase.eventAnnotationsQueries.deleteEdit(relatedEventId, roomId)
} else { } else {
// I have the last event // I have the last event
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent -> ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs val newLastEditTs = previousEdit.origin_server_ts
?: System.currentTimeMillis() ?: 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 { } ?: run {
Timber.e("Failed to udate edited summary") Timber.e("Failed to udate edited summary")
// TODO how to reccover that // TODO how to reccover that
@ -512,8 +527,8 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
} }
} }
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) { fun handleReactionRedact(eventToPrune: EventEntity, roomId: String) {
Timber.v("REDACTION of reaction ${eventToPrune.eventId}") Timber.v("REDACTION of reaction ${eventToPrune.event_id}")
// delete a reaction, need to update the annotation summary if any // delete a reaction, need to update the annotation summary if any
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
?: return ?: return
@ -521,78 +536,110 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
val reactionKey = reactionContent.relatesTo.key val reactionKey = reactionContent.relatesTo.key
Timber.v("REMOVE reaction for key $reactionKey") Timber.v("REMOVE reaction for key $reactionKey")
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(eventThatWasReacted, reactionKey).executeAsOneOrNull()
if (summary != null) { if (reactionSummary != null) {
summary.reactionsSummary.where() val sourceEvents = reactionSummary.source_event_ids.toMutableList()
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey) Timber.v("Find summary for key with ${sourceEvents.size} known reactions (count:${reactionSummary.count})")
.findFirst()?.let { aggregation -> if (sourceEvents.contains(eventToPrune.event_id)) {
Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})") Timber.v("REMOVE reaction for key $reactionKey")
Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}") sourceEvents.remove(eventToPrune.event_id)
if (aggregation.sourceEvents.contains(eventToPrune.eventId)) { val newCount = reactionSummary.count - 1
Timber.v("REMOVE reaction for key $reactionKey") val addedByMe = if (eventToPrune.sender_id == userId) {
aggregation.sourceEvents.remove(eventToPrune.eventId) // Was it a redact on my reaction?
Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}") false
aggregation.count = aggregation.count - 1 } else {
if (eventToPrune.sender == userId) { reactionSummary.added_by_me
// Was it a redact on my reaction? }
aggregation.addedByMe = false if (newCount == 0L) {
} sessionDatabase.eventAnnotationsQueries.deleteReaction(eventThatWasReacted, roomId, reactionKey)
if (aggregation.count == 0) { } else {
// delete! sessionDatabase.eventAnnotationsQueries.updateReaction(
aggregation.deleteFromRealm() count = newCount,
} addedByMe = addedByMe,
} else { sourceEventIds = sourceEvents,
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") 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 { } else {
Timber.e("## Cannot find summary for key $reactionKey") Timber.e("## Cannot find summary for key $reactionKey")
} }
} }
private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) { private fun handleVerification(event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId) val eventId = event.eventId ?: return
val verifSummary = sessionDatabase.eventAnnotationsQueries.selectReferenceForEvent(relatedEventId).executeAsOneOrNull()
val verifSummary = eventSummary.referencesSummaryEntity if (verifSummary == null) {
?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also { val state = VerificationState.REQUEST.computeNewVerificationState(event)
eventSummary.referencesSummaryEntity = it val data = ReferencesAggregatedContent(state)
} val sourceLocalEchoEvents: List<String>
val sourceEvents: List<String>
val txId = event.unsignedData?.transactionId if (isLocalEcho) {
sourceLocalEchoEvents = listOf(eventId)
if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) { sourceEvents = emptyList()
// ok it has already been handled } else {
} else { sourceLocalEchoEvents = emptyList()
ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>() sourceEvents = listOf(eventId)
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
} }
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 { } else {
verifSummary.sourceLocalEcho.remove(txId) val txId = event.unsignedData?.transactionId
verifSummary.sourceEvents.add(event.eventId) 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 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.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.sqldelight.session.EventInsertNotification
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.sqldelight.session.SessionDatabase
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 javax.inject.Inject 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. * The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
*/ */
internal class EventRelationsAggregationUpdater @Inject constructor( internal class EventRelationsAggregationUpdater @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration, sessionDatabase: SessionDatabase,
@UserId private val userId: String,
private val task: EventRelationsAggregationTask) : private val task: EventRelationsAggregationTask) :
RealmLiveEntityObserver<EventEntity>(realmConfiguration) { SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
override val query = Monarchy.Query<EventEntity> { override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
EventEntity.whereTypes(it, listOf( types = listOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.REDACTION, EventType.REDACTION,
EventType.REACTION, EventType.REACTION,
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_MAC, EventType.KEY_VERIFICATION_MAC,
// TODO Add ? // TODO Add ?
// EventType.KEY_VERIFICATION_READY, // EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.ENCRYPTED) 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 package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy 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.EventType
import im.vector.matrix.android.api.session.events.model.toModel 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.*
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.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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.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.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.mapper.map
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.di.UserId 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.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.room.timeline.TimelineEventDecryptor
import im.vector.matrix.android.internal.session.sync.RoomSyncHandler 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.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications 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 org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor( internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val sessionDatabase: SessionDatabase,
private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver, private val roomAvatarResolver: RoomAvatarResolver,
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>, private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
private val eventBus: EventBus, private val eventBus: EventBus) {
private val monarchy: Monarchy) {
companion object { companion object {
// TODO: maybe allow user of SDK to give that list // TODO: maybe allow user of SDK to give that list
@ -79,107 +65,139 @@ internal class RoomSummaryUpdater @Inject constructor(
) )
} }
fun update(realm: Realm, fun update(roomId: String,
roomId: String, newMembership: Membership? = null,
membership: Membership? = null,
roomSummary: RoomSyncSummary? = null, roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null, unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false, updateMembers: Boolean = false,
ephemeralResult: RoomSyncHandler.EphemeralResult? = null, ephemeralResult: RoomSyncHandler.EphemeralResult? = null,
inviterId: String? = 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) { val currentRoomSummary = sessionDatabase.roomSummaryQueries.get(roomId).executeAsOneOrNull()
roomSummaryEntity.membership = membership 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, val invitedMemberCount = if (roomSummary?.invitedMembersCount != null) {
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) 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 // 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) val encryptionEvent = sessionDatabase.eventQueries.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"").executeAsList().firstOrNull()
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 val hasUnreadMessages = notificationCount > 0
// avoid this call if we are sure there are unread events // 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() val displayName = roomDisplayNameResolver.resolve(roomId, membership).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) val avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic val topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent val canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias ?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList() ?: 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) { sessionDatabase.roomAliasesQueries.deleteAllForRoom(roomId)
roomSummaryEntity.inviterId = inviterId roomAliases.forEach { alias ->
} else if (roomSummaryEntity.membership != Membership.INVITE) { sessionDatabase.roomAliasesQueries.insert(roomId, alias)
roomSummaryEntity.inviterId = null
} }
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) { val isEncrypted = encryptionEvent != null
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}") val directUserId = currentRoomSummary?.direct_user_id
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, "")) sessionDatabase.userQueries.deleteAllTypingUsers()
ephemeralResult?.typingUserIds?.forEach { typingId ->
sessionDatabase.userQueries.insertTyping(roomId, typingId)
} }
val isDirect = currentRoomSummary?.is_direct ?: false
if (updateMembers) { if (latestPreviewableEventId != null && latestPreviewableEventId.isNotEmpty()) {
val otherRoomMembers = RoomMemberHelper(realm, roomId) if (selectEventType(latestPreviewableEventId) == EventType.ENCRYPTED
.queryRoomMembersEvent() && selectDecryptionResult(latestPreviewableEventId) == null) {
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) Timber.v("Should decrypt $latestPreviewableEventId for room: $displayName")
.findAll() timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEventId, ""))
.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))
} }
} }
} val newInviterId = if (membership == Membership.INVITE && inviterId != null) {
inviterId
fun updateShieldTrust(realm: Realm, } else if (membership != Membership.INVITE) {
roomId: String, null
trust: RoomEncryptionTrustLevel?) { } else {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) currentRoomSummary?.inviter_id
if (roomSummaryEntity.isEncrypted) { }
roomSummaryEntity.roomEncryptionTrustLevel = trust 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 package im.vector.matrix.android.internal.session.room.notification
import im.vector.matrix.android.api.pushrules.Action import im.vector.matrix.android.api.pushrules.*
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.rest.PushCondition 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.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.api.session.room.notification.RoomNotificationState
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.model.PushRuleEntity 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? { internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? {
return when { return when {

View File

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

View File

@ -168,7 +168,7 @@ internal class DefaultSendService @AssistedInject constructor(
override fun deleteFailedEcho(localEcho: TimelineEvent) { override fun deleteFailedEcho(localEcho: TimelineEvent) {
taskExecutor.executorScope.launch { taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, localEcho) localEchoRepository.deleteFailedEcho(localEcho)
} }
} }
@ -197,7 +197,7 @@ internal class DefaultSendService @AssistedInject constructor(
eventsToResend.forEach { eventsToResend.forEach {
sendEvent(it) 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, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint() 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) 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, internal class LocalEchoRepository @Inject constructor(private val sessionDatabase: SessionDatabase,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus, private val eventBus: EventBus,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers) {
private val timelineEventMapper: TimelineEventMapper,
private val roomMemberSummaryDataSource: RoomMemberSummaryDataSource) {
suspend fun createLocalEcho(event: Event) { suspend fun createLocalEcho(event: Event) {
val roomId = event.roomId 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 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.Assisted
import com.squareup.inject.assisted.AssistedInject 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.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.TimelineService
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings 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.Optional
import im.vector.matrix.android.api.util.toOptional import kotlinx.coroutines.flow.Flow
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
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
private val monarchy: Monarchy, private val timelineEventDataSource: TimelineEventDataSource,
private val eventBus: EventBus, private val sqlTimelineFactory: SQLTimeline.Factory
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
) : TimelineService { ) : TimelineService {
@AssistedInject.Factory @AssistedInject.Factory
@ -52,37 +36,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
} }
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline { override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline( return sqlTimelineFactory.create(roomId, eventId, settings)
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
)
} }
override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy return timelineEventDataSource.getTimeLineEvent(eventId)
.fetchCopyMap({
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
}, { entity, _ ->
timelineEventMapper.map(entity)
})
} }
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>> {
val liveData = monarchy.findAllMappedWithChanges( return timelineEventDataSource.getTimeLineEventLive(eventId)
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
{ timelineEventMapper.map(it) }
)
return Transformations.map(liveData) { events ->
events.firstOrNull().toOptional()
}
} }
} }

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

View File

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

View File

@ -16,26 +16,17 @@
package im.vector.matrix.android.internal.session.signout 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.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils import im.vector.matrix.android.internal.database.DatabaseKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase import im.vector.matrix.android.internal.di.*
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.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -54,13 +45,10 @@ internal class DefaultSignOutTask @Inject constructor(
private val signOutAPI: SignOutAPI, private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask, @RealmCryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@SessionFilesDirectory private val sessionFiles: File, @SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File, @SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils, private val databaseKeysUtils: DatabaseKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String, @UserMd5 private val userMd5: String,
private val eventBus: EventBus private val eventBus: EventBus
) : SignOutTask { ) : SignOutTask {
@ -96,9 +84,6 @@ internal class DefaultSignOutTask @Inject constructor(
Timber.d("SignOut: delete session params...") Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(sessionId) sessionParamsStore.delete(sessionId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...") Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit) clearCryptoDataTask.execute(Unit)
@ -107,17 +92,7 @@ internal class DefaultSignOutTask @Inject constructor(
sessionCache.deleteRecursively() sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys") Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) databaseKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) databaseKeysUtils.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)") }
}
} }
} }

View File

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

View File

@ -271,6 +271,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.core:core-ktx:1.1.0' 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 "org.threeten:threetenbp:1.4.0:no-tzdb"
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" 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.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import arrow.core.Option 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.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.android.api.session.room.roomSummaryQueryParams
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.features.grouplist.ALL_COMMUNITIES_GROUP_ID 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.HomeRoomListDataSource
import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.addTo import io.reactivex.rxkotlin.addTo
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -62,32 +60,30 @@ class AppStateHandler @Inject constructor(
private fun observeRoomsAndGroup() { private fun observeRoomsAndGroup() {
Observable Observable
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>( .combineLatest<Option<Session>, Option<GroupSummary>, Pair<Option<Session>, Option<GroupSummary>>>(
sessionDataSource.observe() sessionDataSource.observe(),
.observeOn(AndroidSchedulers.mainThread())
.switchMap {
val query = roomSummaryQueryParams {}
it.orNull()?.rx()?.liveRoomSummaries(query)
?: Observable.just(emptyList())
}
.throttleLast(300, TimeUnit.MILLISECONDS),
selectedGroupDataSource.observe(), selectedGroupDataSource.observe(),
BiFunction { rooms, selectedGroupOption -> BiFunction { sessionOption, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull() Pair(sessionOption, selectedGroupOption)
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)
} }
) ).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 { .subscribe {
homeRoomListDataSource.post(it) homeRoomListDataSource.post(it)
} }

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.lifecycle.asLiveData
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
@ -40,7 +41,7 @@ class HomeDrawerFragment @Inject constructor(
if (savedInstanceState == null) { if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) 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() val user = optionalUser?.getOrNull()
if (user != null) { if (user != null) {
avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView) 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.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized 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( data class BreadcrumbsViewState(
val asyncBreadcrumbs: Async<List<RoomSummary>> = Uninitialized val asyncBreadcrumbs: Async<List<Breadcrumb>> = Uninitialized
) : MvRxState ) : MvRxState

View File

@ -154,7 +154,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
EmojiDataSource.quickEmojis.map { emoji -> 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 { .execute {

View File

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