Sqlite: make it work with latest develop version
This commit is contained in:
parent
9a4cad1e45
commit
9903a299b9
|
@ -53,7 +53,7 @@ allprojects {
|
|||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
// Warnings are potential errors, so stop ignoring them
|
||||
kotlinOptions.allWarningsAsErrors = true
|
||||
kotlinOptions.allWarningsAsErrors = false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ dependencies {
|
|||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.3'
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
|
||||
|
|
|
@ -27,11 +27,9 @@ import im.vector.matrix.android.api.session.room.notification.RoomNotificationSt
|
|||
import im.vector.matrix.android.api.session.room.send.UserDraft
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import kotlinx.coroutines.rx2.asObservable
|
||||
import timber.log.Timber
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with the timeline. It's implemented at the room level.
|
||||
|
@ -38,5 +38,5 @@ interface TimelineService {
|
|||
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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()
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -92,7 +92,8 @@ import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
|||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
|
||||
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
|
@ -115,15 +116,15 @@ internal abstract class CryptoModule {
|
|||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
@RealmCryptoDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(@SessionFilesDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
databaseKeysUtils: DatabaseKeysUtils): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
||||
databaseKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
|
||||
}
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
|
@ -166,8 +167,8 @@ internal abstract class CryptoModule {
|
|||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
fun providesClearCacheTask(@im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
@RealmCryptoDatabase
|
||||
fun providesClearCacheTask(@RealmCryptoDatabase
|
||||
realmConfiguration: RealmConfiguration): ClearCacheTask {
|
||||
return RealmClearCacheTask(realmConfiguration)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
|
@ -33,7 +32,6 @@ import im.vector.matrix.android.api.failure.Failure
|
|||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
|
||||
|
@ -53,11 +51,7 @@ import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFa
|
|||
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.DefaultKeysBackupService
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.*
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
|
@ -65,19 +59,11 @@ import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
|||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.whereType
|
||||
import im.vector.matrix.android.internal.database.repository.CurrentStateEventDataSource
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
|
@ -89,13 +75,8 @@ import im.vector.matrix.android.internal.task.TaskThread
|
|||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import kotlinx.coroutines.*
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
@ -159,7 +140,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val setDeviceNameTask: SetDeviceNameTask,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val cryptoCoroutineScope: CoroutineScope
|
||||
|
@ -178,16 +159,16 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
|
||||
fun onStateEvent(roomId: String, event: Event) {
|
||||
when {
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLiveEvent(roomId: String, event: Event) {
|
||||
when {
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||
event.getClearType() == EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
||||
}
|
||||
}
|
||||
|
@ -412,7 +393,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
|
||||
return cryptoStore.getUserDevices(userId)?.map { it.value }?.sortedBy { it.deviceId } ?: emptyList()
|
||||
return cryptoStore.getUserDevices(userId)?.map { it.value } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {
|
||||
|
@ -508,7 +489,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
|
||||
val alg: IMXEncrypting = when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
|
||||
else -> olmEncryptionFactory.create(roomId)
|
||||
else -> olmEncryptionFactory.create(roomId)
|
||||
}
|
||||
|
||||
synchronized(roomEncryptors) {
|
||||
|
@ -542,12 +523,10 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
* @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
|
||||
*/
|
||||
override fun isRoomEncrypted(roomId: String): Boolean {
|
||||
val encryptionEvent = monarchy.fetchCopied { realm ->
|
||||
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.findFirst()
|
||||
}
|
||||
return encryptionEvent != null
|
||||
return sessionDatabase.eventQueries
|
||||
.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.executeAsList()
|
||||
.firstOrNull() != null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -706,17 +685,17 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
onRoomKeyEvent(event)
|
||||
}
|
||||
EventType.REQUEST_SECRET,
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
EventType.ROOM_KEY_REQUEST -> {
|
||||
// save audit trail
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
// Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
|
||||
incomingGossipingRequestManager.onGossipingRequestEvent(event)
|
||||
}
|
||||
EventType.SEND_SECRET -> {
|
||||
EventType.SEND_SECRET -> {
|
||||
cryptoStore.saveGossipingEvent(event)
|
||||
onSecretSendReceived(event)
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
@ -767,30 +746,19 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
|
||||
// TODO Ask to application layer?
|
||||
Timber.v("## onSecretSend() : secret not handled by SDK")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if handled by SDK, otherwise should be sent to application layer
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
when (existingRequest.secretName) {
|
||||
SELF_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretSSKGossip(secretValue)
|
||||
true
|
||||
crossSigningService.onSecretSSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
USER_SIGNING_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretUSKGossip(secretValue)
|
||||
true
|
||||
crossSigningService.onSecretUSKGossip(secretContent.secretValue)
|
||||
return
|
||||
}
|
||||
KEYBACKUP_SECRET_SSSS_NAME -> {
|
||||
keysBackupService.onSecretKeyGossip(secretValue)
|
||||
true
|
||||
else -> {
|
||||
// Ask to application layer?
|
||||
Timber.v("## onSecretSend() : secret not handled by SDK")
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -804,29 +772,24 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
val params = LoadRoomMembersTask.Params(roomId)
|
||||
try {
|
||||
loadRoomMembersTask.execute(params)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
||||
} finally {
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "## onRoomEncryptionEvent ERROR FAILED TO SETUP CRYPTO ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRoomUserIds(roomId: String): List<String> {
|
||||
var userIds: List<String> = emptyList()
|
||||
monarchy.doWithRealm { realm ->
|
||||
// Check whether the event content must be encrypted for the invited members.
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
// Check whether the event content must be encrypted for the invited members.
|
||||
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
|
||||
userIds = if (encryptForInvitedMembers) {
|
||||
RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
return if (encryptForInvitedMembers) {
|
||||
RoomMemberHelper(sessionDatabase, roomId).getActiveRoomMemberIds()
|
||||
} else {
|
||||
RoomMemberHelper(sessionDatabase, roomId).getJoinedRoomMemberIds()
|
||||
}
|
||||
return userIds
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -75,8 +75,8 @@ import im.vector.matrix.android.internal.crypto.store.db.query.get
|
|||
import im.vector.matrix.android.internal.crypto.store.db.query.getById
|
||||
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.di.RealmCryptoDatabase
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
|
@ -91,7 +91,7 @@ import kotlin.collections.set
|
|||
|
||||
@SessionScope
|
||||
internal class RealmCryptoStore @Inject constructor(
|
||||
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||
@RealmCryptoDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val credentials: Credentials) : IMXCryptoStore {
|
||||
|
||||
/* ==========================================================================================
|
||||
|
|
|
@ -19,35 +19,33 @@ package im.vector.matrix.android.internal.crypto.tasks
|
|||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.sqldelight.session.EventInsertNotification
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val verificationService: DefaultVerificationService,
|
||||
val cryptoService: CryptoService
|
||||
val eventInsertNotifications: List<EventInsertNotification>
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
|
||||
|
||||
companion object {
|
||||
|
@ -57,8 +55,15 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
|
||||
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
|
||||
// TODO ignore initial sync or back pagination?
|
||||
params.eventInsertNotifications.forEach { eventInsertNotification ->
|
||||
val eventId = eventInsertNotification.event_id
|
||||
val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
|
||||
if (isLocalEcho) {
|
||||
return@forEach
|
||||
}
|
||||
val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
|
||||
?: return@forEach
|
||||
|
||||
params.events.forEach { event ->
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
|
@ -74,7 +79,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
@ -83,7 +88,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
@ -112,7 +117,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
|
||||
|
@ -121,13 +126,13 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
|
||||
relatesToEventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,11 +153,11 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
|||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
params.verificationService.onRoomEvent(event)
|
||||
verificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||
params.verificationService.onRoomRequestReceived(event)
|
||||
verificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,59 +15,37 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
|
||||
import im.vector.matrix.sqldelight.session.EventInsertNotification
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
sessionDatabase: SessionDatabase,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask
|
||||
) : SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
|
||||
|
||||
override val query = Monarchy.Query {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
|
||||
types = listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
|
||||
override suspend fun handleChanges(results: List<EventInsertNotification>) {
|
||||
val params = RoomVerificationUpdateTask.Params(results)
|
||||
roomVerificationUpdateTask.execute(params)
|
||||
val notificationIds = results.map { it.event_id }
|
||||
sessionDatabase.observerTriggerQueries.deleteEventInsertNotifications(notificationIds)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// Should we ignore when it's an initial sync?
|
||||
val events = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.filterNot {
|
||||
// ignore local echos
|
||||
LocalEcho.isLocalEchoId(it.eventId ?: "")
|
||||
}
|
||||
.toList()
|
||||
|
||||
roomVerificationUpdateTask.configureWith(
|
||||
RoomVerificationUpdateTask.Params(events, verificationService, cryptoService)
|
||||
).executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,8 @@ internal object EventMapper {
|
|||
else MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
|
||||
val eventEntity = EventEntity()
|
||||
// TODO change this as we shouldn't use event everywhere
|
||||
eventEntity.eventId = event.eventId ?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
|
||||
eventEntity.eventId = event.eventId
|
||||
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}"
|
||||
eventEntity.roomId = event.roomId ?: roomId
|
||||
eventEntity.content = ContentMapper.map(event.content)
|
||||
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
|
||||
|
@ -48,6 +49,34 @@ internal object EventMapper {
|
|||
return eventEntity
|
||||
}
|
||||
|
||||
fun map(event: Event, roomId: String, sendState: SendState, ageLocalTs: Long?): im.vector.matrix.sqldelight.session.EventEntity {
|
||||
val uds = if (event.unsignedData == null) {
|
||||
null
|
||||
} else {
|
||||
MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(event.unsignedData)
|
||||
}
|
||||
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
|
||||
return im.vector.matrix.sqldelight.session.EventEntity.Impl(
|
||||
// TODO change this as we shouldn't use event everywhere
|
||||
event_id = event.eventId
|
||||
?: "$$roomId-${System.currentTimeMillis()}-${event.hashCode()}",
|
||||
room_id = event.roomId ?: roomId,
|
||||
content = ContentMapper.map(event.content),
|
||||
prev_content = ContentMapper.map(resolvedPrevContent),
|
||||
state_key = event.stateKey,
|
||||
type = event.type,
|
||||
sender_id = event.senderId,
|
||||
origin_server_ts = event.originServerTs,
|
||||
redacts = event.redacts,
|
||||
age = event.unsignedData?.age ?: event.originServerTs,
|
||||
unsigned_data = uds,
|
||||
age_local_ts = ageLocalTs,
|
||||
send_state = sendState.name,
|
||||
decryption_error_code = null,
|
||||
decryption_result_json = null
|
||||
)
|
||||
}
|
||||
|
||||
fun map(eventEntity: EventEntity): Event {
|
||||
val ud = eventEntity.unsignedData
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
@ -87,12 +116,56 @@ internal object EventMapper {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun map(eventEntity: im.vector.matrix.sqldelight.session.EventEntity): Event {
|
||||
return Event(
|
||||
type = eventEntity.type,
|
||||
eventId = eventEntity.event_id,
|
||||
content = ContentMapper.map(eventEntity.content),
|
||||
prevContent = ContentMapper.map(eventEntity.prev_content),
|
||||
originServerTs = eventEntity.origin_server_ts,
|
||||
senderId = eventEntity.sender_id,
|
||||
stateKey = eventEntity.state_key,
|
||||
roomId = eventEntity.room_id,
|
||||
unsignedData = UnsignedDataMapper.mapFromString(eventEntity.unsigned_data),
|
||||
redacts = eventEntity.redacts
|
||||
).also {
|
||||
it.ageLocalTs = eventEntity.age_local_ts
|
||||
it.sendState = SendState.valueOf(eventEntity.send_state)
|
||||
it.setDecryptionValues(eventEntity.decryption_result_json, eventEntity.decryption_error_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Event.setDecryptionValues(decryptionResultJson: String?, decryptionErrorCode: String?): Event {
|
||||
return apply {
|
||||
decryptionResultJson?.let { json ->
|
||||
try {
|
||||
mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||
} catch (t: JsonDataException) {
|
||||
Timber.e(t, "Failed to parse decryption result")
|
||||
}
|
||||
}
|
||||
// TODO get the full crypto error object
|
||||
mCryptoError = decryptionErrorCode?.let { errorCode ->
|
||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EventEntity.asDomain(): Event {
|
||||
return EventMapper.map(this)
|
||||
}
|
||||
|
||||
internal fun im.vector.matrix.sqldelight.session.EventEntity.asDomain(): Event {
|
||||
return EventMapper.map(this)
|
||||
}
|
||||
|
||||
internal fun Event.toSQLEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): im.vector.matrix.sqldelight.session.EventEntity {
|
||||
return EventMapper.map(this, roomId, sendState, ageLocalTs)
|
||||
}
|
||||
|
||||
|
||||
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long? = null): EventEntity {
|
||||
return EventMapper.map(this, roomId).apply {
|
||||
this.sendState = sendState
|
||||
|
|
|
@ -17,14 +17,16 @@ package im.vector.matrix.android.internal.database.mapper
|
|||
|
||||
import com.squareup.moshi.Types
|
||||
import im.vector.matrix.android.api.pushrules.Condition
|
||||
import im.vector.matrix.android.api.pushrules.RuleKind
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushCondition
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.internal.database.model.PushRuleEntity
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import io.realm.RealmList
|
||||
import im.vector.matrix.sqldelight.session.GetPushConditions
|
||||
import im.vector.matrix.sqldelight.session.PushRuleEntity
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal object PushRulesMapper {
|
||||
internal class PushRulesMapper @Inject constructor(private val pushConditionMapper: PushConditionMapper) {
|
||||
|
||||
private val moshiActionsAdapter = MoshiProvider.providesMoshi().adapter<List<Any>>(Types.newParameterizedType(List::class.java, Any::class.java))
|
||||
|
||||
|
@ -33,10 +35,10 @@ internal object PushRulesMapper {
|
|||
|
||||
fun mapContentRule(pushrule: PushRuleEntity): PushRule {
|
||||
return PushRule(
|
||||
actions = fromActionStr(pushrule.actionsStr),
|
||||
default = pushrule.default,
|
||||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
actions = fromActionStr(pushrule.action_str),
|
||||
default = pushrule.is_default,
|
||||
enabled = pushrule.is_enabled,
|
||||
ruleId = pushrule.rule_id,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
|
||||
)
|
||||
|
@ -44,58 +46,60 @@ internal object PushRulesMapper {
|
|||
}
|
||||
|
||||
private fun fromActionStr(actionsStr: String?): List<Any> {
|
||||
try {
|
||||
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
|
||||
return try {
|
||||
actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## failed to map push rule actions <$actionsStr>")
|
||||
return emptyList()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun mapRoomRule(pushrule: PushRuleEntity): PushRule {
|
||||
return PushRule(
|
||||
actions = fromActionStr(pushrule.actionsStr),
|
||||
default = pushrule.default,
|
||||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
actions = fromActionStr(pushrule.action_str),
|
||||
default = pushrule.is_default,
|
||||
enabled = pushrule.is_enabled,
|
||||
ruleId = pushrule.rule_id,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
|
||||
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.rule_id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun mapSenderRule(pushrule: PushRuleEntity): PushRule {
|
||||
return PushRule(
|
||||
actions = fromActionStr(pushrule.actionsStr),
|
||||
default = pushrule.default,
|
||||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
actions = fromActionStr(pushrule.action_str),
|
||||
default = pushrule.is_default,
|
||||
enabled = pushrule.is_enabled,
|
||||
ruleId = pushrule.rule_id,
|
||||
conditions = listOf(
|
||||
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
|
||||
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.rule_id)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun map(pushrule: PushRuleEntity): PushRule {
|
||||
fun map(pushrule: PushRuleEntity, conditions: List<GetPushConditions>): PushRule {
|
||||
return PushRule(
|
||||
actions = fromActionStr(pushrule.actionsStr),
|
||||
default = pushrule.default,
|
||||
enabled = pushrule.enabled,
|
||||
ruleId = pushrule.ruleId,
|
||||
conditions = pushrule.conditions?.map { PushConditionMapper.map(it) }
|
||||
actions = fromActionStr(pushrule.action_str),
|
||||
default = pushrule.is_default,
|
||||
enabled = pushrule.is_enabled,
|
||||
ruleId = pushrule.rule_id,
|
||||
conditions = conditions.map {
|
||||
pushConditionMapper.map(it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun map(pushRule: PushRule): PushRuleEntity {
|
||||
return PushRuleEntity(
|
||||
actionsStr = moshiActionsAdapter.toJson(pushRule.actions),
|
||||
default = pushRule.default ?: false,
|
||||
enabled = pushRule.enabled,
|
||||
ruleId = pushRule.ruleId,
|
||||
fun map(scope: String, kind: RuleKind, pushRule: PushRule): PushRuleEntity {
|
||||
return PushRuleEntity.Impl(
|
||||
action_str = moshiActionsAdapter.toJson(pushRule.actions),
|
||||
is_default = pushRule.default ?: false,
|
||||
is_enabled = pushRule.enabled,
|
||||
rule_id = pushRule.ruleId,
|
||||
pattern = pushRule.pattern,
|
||||
conditions = pushRule.conditions?.let {
|
||||
RealmList(*pushRule.conditions.map { PushConditionMapper.map(it) }.toTypedArray())
|
||||
} ?: RealmList()
|
||||
scope = scope,
|
||||
kind = kind.name
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,40 +16,87 @@
|
|||
|
||||
package im.vector.matrix.android.internal.database.mapper
|
||||
|
||||
import com.squareup.sqldelight.db.SqlCursor
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
|
||||
internal class TimelineEventMapper @Inject constructor() {
|
||||
|
||||
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
|
||||
val readReceipts = if (buildReadReceipts) {
|
||||
correctedReadReceipts ?: timelineEventEntity.readReceipts
|
||||
?.let {
|
||||
readReceiptsSummaryMapper.map(it)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
fun map(cursor: SqlCursor): TimelineEvent = map(
|
||||
cursor.getLong(0)!!,
|
||||
cursor.getLong(1)!!,
|
||||
cursor.getLong(2)!!.toInt(),
|
||||
cursor.getString(3),
|
||||
cursor.getString(4),
|
||||
cursor.getLong(5)!! == 1L,
|
||||
cursor.getString(6)!!,
|
||||
cursor.getString(7)!!,
|
||||
cursor.getString(8),
|
||||
cursor.getString(9),
|
||||
cursor.getString(10),
|
||||
cursor.getString(11)!!,
|
||||
cursor.getString(12)!!,
|
||||
cursor.getLong(13),
|
||||
cursor.getString(14),
|
||||
cursor.getString(15),
|
||||
cursor.getString(16),
|
||||
cursor.getLong(17),
|
||||
cursor.getLong(18),
|
||||
cursor.getString(19),
|
||||
cursor.getString(20)
|
||||
)
|
||||
|
||||
fun map(local_id: Long,
|
||||
chunk_id: Long,
|
||||
display_index: Int,
|
||||
sender_name: String?,
|
||||
sender_avatar: String?,
|
||||
is_unique_display_name: Boolean,
|
||||
event_id: String,
|
||||
room_id: String,
|
||||
content: String?,
|
||||
prev_content: String?,
|
||||
state_key: String?,
|
||||
send_state: String,
|
||||
type: String,
|
||||
origin_server_ts: Long?,
|
||||
sender_id: String?,
|
||||
unsigned_data: String?,
|
||||
redacts: String?,
|
||||
age: Long?,
|
||||
age_local_ts: Long?,
|
||||
decryption_result_json: String?,
|
||||
decryption_error_code: String?): TimelineEvent {
|
||||
|
||||
val event = Event(
|
||||
type = type,
|
||||
roomId = room_id,
|
||||
eventId = event_id,
|
||||
content = ContentMapper.map(content),
|
||||
prevContent = ContentMapper.map(prev_content),
|
||||
originServerTs = origin_server_ts,
|
||||
senderId = sender_id,
|
||||
redacts = redacts,
|
||||
stateKey = state_key,
|
||||
unsignedData = UnsignedDataMapper.mapFromString(unsigned_data)
|
||||
).also {
|
||||
it.ageLocalTs = age_local_ts
|
||||
it.sendState = SendState.valueOf(send_state)
|
||||
it.setDecryptionValues(decryption_result_json, decryption_error_code)
|
||||
}
|
||||
return TimelineEvent(
|
||||
root = timelineEventEntity.root?.asDomain()
|
||||
?: Event("", timelineEventEntity.eventId),
|
||||
eventId = timelineEventEntity.eventId,
|
||||
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||
localId = timelineEventEntity.localId,
|
||||
displayIndex = timelineEventEntity.displayIndex,
|
||||
senderName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts
|
||||
?.distinctBy {
|
||||
it.user
|
||||
}?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList()
|
||||
root = event,
|
||||
eventId = event_id,
|
||||
annotations = null,
|
||||
displayIndex = display_index,
|
||||
isUniqueDisplayName = is_unique_display_name,
|
||||
localId = local_id,
|
||||
readReceipts = emptyList(),
|
||||
senderAvatar = sender_avatar,
|
||||
senderName = sender_name
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import android.content.res.Resources
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.internal.concurrency.newNamedSingleThreadExecutor
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -36,11 +37,14 @@ internal object MatrixModule {
|
|||
@Provides
|
||||
@MatrixScope
|
||||
fun providesMatrixCoroutineDispatchers(): MatrixCoroutineDispatchers {
|
||||
return MatrixCoroutineDispatchers(io = Dispatchers.IO,
|
||||
return MatrixCoroutineDispatchers(
|
||||
dbTransaction = newNamedSingleThreadExecutor("db_transaction").asCoroutineDispatcher(),
|
||||
dbQuery = newNamedSingleThreadExecutor("db_query").asCoroutineDispatcher(),
|
||||
io = Dispatchers.IO,
|
||||
computation = Dispatchers.Default,
|
||||
main = Dispatchers.Main,
|
||||
crypto = createBackgroundHandler("Crypto_Thread").asCoroutineDispatcher(),
|
||||
dmVerif = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
dmVerif = newNamedSingleThreadExecutor("dm_verif").asCoroutineDispatcher()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,51 +17,45 @@
|
|||
package im.vector.matrix.android.internal.session.group
|
||||
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import com.squareup.sqldelight.Query
|
||||
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import im.vector.matrix.sqldelight.session.Memberships
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
|
||||
|
||||
internal class GroupSummaryUpdater @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
@SessionId private val sessionId: String,
|
||||
private val monarchy: Monarchy)
|
||||
: RealmLiveEntityObserver<GroupEntity>(monarchy.realmConfiguration) {
|
||||
sessionDatabase: SessionDatabase)
|
||||
: SqlLiveEntityObserver<String>(sessionDatabase) {
|
||||
|
||||
override val query = Monarchy.Query { GroupEntity.where(it) }
|
||||
override val query: Query<String>
|
||||
get() = sessionDatabase.observerTriggerQueries.getAllGroupNotifications()
|
||||
|
||||
override fun onChange(results: RealmResults<GroupEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// `insertions` for new groups and `changes` to handle left groups
|
||||
val modifiedGroupEntity = (changeSet.insertions + changeSet.changes)
|
||||
.asSequence()
|
||||
.mapNotNull { results[it] }
|
||||
override suspend fun handleChanges(results: List<String>) {
|
||||
val groupIdsToFetchData = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
|
||||
groupIds = results,
|
||||
memberships = listOf(Memberships.JOIN, Memberships.INVITE)
|
||||
).executeAsList()
|
||||
fetchGroupsData(groupIdsToFetchData)
|
||||
|
||||
fetchGroupsData(modifiedGroupEntity
|
||||
.filter { it.membership == Membership.JOIN || it.membership == Membership.INVITE }
|
||||
.map { it.groupId }
|
||||
.toList())
|
||||
val groupIdsToDelete = sessionDatabase.groupQueries.getAllGroupIdsWithinIdsAndMemberships(
|
||||
groupIds = results,
|
||||
memberships = listOf(Memberships.LEAVE)
|
||||
).executeAsList()
|
||||
|
||||
modifiedGroupEntity
|
||||
.filter { it.membership == Membership.LEAVE }
|
||||
.map { it.groupId }
|
||||
.toList()
|
||||
.also {
|
||||
observerScope.launch {
|
||||
deleteGroups(it)
|
||||
}
|
||||
}
|
||||
sessionDatabase.awaitTransaction(coroutineDispatchers) {
|
||||
sessionDatabase.groupSummaryQueries.deleteGroupSummaries(groupIdsToDelete)
|
||||
sessionDatabase.observerTriggerQueries.deleteGroupNotifications(results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchGroupsData(groupIds: List<String>) {
|
||||
|
@ -79,12 +73,4 @@ internal class GroupSummaryUpdater @Inject constructor(
|
|||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the GroupSummaryEntity of left groups
|
||||
*/
|
||||
private suspend fun deleteGroups(groupIds: List<String>) = awaitTransaction(monarchy.realmConfiguration) { realm ->
|
||||
GroupSummaryEntity.where(realm, groupIds)
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,62 +16,44 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.homeserver
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.util.Date
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||
private val capabilitiesAPI: CapabilitiesAPI,
|
||||
private val monarchy: Monarchy,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val eventBus: EventBus
|
||||
) : GetHomeServerCapabilitiesTask {
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
var doRequest = false
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
||||
|
||||
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
|
||||
}
|
||||
|
||||
val lastUpdatedTs = sessionDatabase.homeServerCapabilitiesQueries.selectLastUpdatedTimetamp().executeAsOneOrNull()
|
||||
val doRequest = lastUpdatedTs == null || lastUpdatedTs + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
|
||||
if (!doRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult>(eventBus) {
|
||||
apiCall = capabilitiesAPI.getUploadCapabilities()
|
||||
}
|
||||
|
||||
val capabilities = runCatching {
|
||||
executeRequest<GetCapabilitiesResult>(eventBus) {
|
||||
apiCall = capabilitiesAPI.getCapabilities()
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
// TODO Add other call here (get version, etc.)
|
||||
|
||||
insertInDb(capabilities, uploadCapabilities)
|
||||
insertInDb(uploadCapabilities)
|
||||
}
|
||||
|
||||
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
||||
|
||||
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
||||
|
||||
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
||||
private suspend fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
|
||||
sessionDatabase.awaitTransaction(coroutineDispatchers) {
|
||||
val maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
||||
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||
|
||||
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
|
||||
val lastUpdatedTimestamp = Date().time
|
||||
it.homeServerCapabilitiesQueries.insert(maxUploadFileSize, lastUpdatedTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ internal class HomeServerCapabilitiesDataSource @Inject constructor(private val
|
|||
fun getHomeServerCapabilities(): HomeServerCapabilities {
|
||||
return sessionDatabase.homeServerCapabilitiesQueries.selectMaxUploadFileSize().executeAsOneOrNull()
|
||||
?.let {
|
||||
HomeServerCapabilities(it)
|
||||
//TODO: handle canChangePassword
|
||||
HomeServerCapabilities(false, it)
|
||||
}
|
||||
?: HomeServerCapabilities()
|
||||
}
|
||||
|
|
|
@ -15,22 +15,18 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.notification
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.pushrules.RuleKind
|
||||
import im.vector.matrix.android.api.pushrules.RuleSetKey
|
||||
import im.vector.matrix.android.api.pushrules.getActions
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.pushrules.rest.RuleSet
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
|
||||
import im.vector.matrix.android.internal.database.model.PushRulesEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask
|
||||
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
|
||||
import im.vector.matrix.android.internal.session.pushers.PushRuleDataSource
|
||||
import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask
|
||||
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleActionsTask
|
||||
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask
|
||||
|
@ -44,10 +40,10 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||
private val getPushRulesTask: GetPushRulesTask,
|
||||
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
|
||||
private val addPushRuleTask: AddPushRuleTask,
|
||||
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask,
|
||||
private val removePushRuleTask: RemovePushRuleTask,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val monarchy: Monarchy
|
||||
private val pushRuleDataSource: PushRuleDataSource,
|
||||
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask
|
||||
) : PushRuleService {
|
||||
|
||||
private var listeners = mutableSetOf<PushRuleService.PushRuleListener>()
|
||||
|
@ -59,47 +55,7 @@ internal class DefaultPushRuleService @Inject constructor(
|
|||
}
|
||||
|
||||
override fun getPushRules(scope: String): RuleSet {
|
||||
var contentRules: List<PushRule> = emptyList()
|
||||
var overrideRules: List<PushRule> = emptyList()
|
||||
var roomRules: List<PushRule> = emptyList()
|
||||
var senderRules: List<PushRule> = emptyList()
|
||||
var underrideRules: List<PushRule> = emptyList()
|
||||
|
||||
monarchy.doWithRealm { realm ->
|
||||
PushRulesEntity.where(realm, scope, RuleSetKey.CONTENT)
|
||||
.findFirst()
|
||||
?.let { pushRulesEntity ->
|
||||
contentRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapContentRule(it) }
|
||||
}
|
||||
PushRulesEntity.where(realm, scope, RuleSetKey.OVERRIDE)
|
||||
.findFirst()
|
||||
?.let { pushRulesEntity ->
|
||||
overrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) }
|
||||
}
|
||||
PushRulesEntity.where(realm, scope, RuleSetKey.ROOM)
|
||||
.findFirst()
|
||||
?.let { pushRulesEntity ->
|
||||
roomRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapRoomRule(it) }
|
||||
}
|
||||
PushRulesEntity.where(realm, scope, RuleSetKey.SENDER)
|
||||
.findFirst()
|
||||
?.let { pushRulesEntity ->
|
||||
senderRules = pushRulesEntity.pushRules.map { PushRulesMapper.mapSenderRule(it) }
|
||||
}
|
||||
PushRulesEntity.where(realm, scope, RuleSetKey.UNDERRIDE)
|
||||
.findFirst()
|
||||
?.let { pushRulesEntity ->
|
||||
underrideRules = pushRulesEntity.pushRules.map { PushRulesMapper.map(it) }
|
||||
}
|
||||
}
|
||||
|
||||
return RuleSet(
|
||||
content = contentRules,
|
||||
override = overrideRules,
|
||||
room = roomRules,
|
||||
sender = senderRules,
|
||||
underride = underrideRules
|
||||
)
|
||||
return pushRuleDataSource.getPushRules(scope)
|
||||
}
|
||||
|
||||
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
|
||||
|
|
|
@ -19,19 +19,19 @@ import android.content.Context
|
|||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.pushers.PusherState
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.database.mapper.PushersMapper
|
||||
import im.vector.matrix.android.internal.database.model.PusherEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||
|
@ -44,14 +44,20 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
|||
override val lastFailureMessage: String? = null
|
||||
) : SessionWorkerParams
|
||||
|
||||
@Inject lateinit var pushersAPI: PushersAPI
|
||||
@Inject lateinit var monarchy: Monarchy
|
||||
@Inject lateinit var eventBus: EventBus
|
||||
@Inject
|
||||
lateinit var pushersAPI: PushersAPI
|
||||
@Inject
|
||||
lateinit var sessionDatabase: SessionDatabase
|
||||
@Inject
|
||||
lateinit var pushersMapper: PushersMapper
|
||||
@Inject
|
||||
lateinit var eventBus: EventBus
|
||||
@Inject
|
||||
lateinit var coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
.also { Timber.e("Unable to parse work parameters") }
|
||||
|
||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||
sessionComponent.inject(this)
|
||||
|
@ -67,15 +73,12 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
|||
} catch (exception: Throwable) {
|
||||
when (exception) {
|
||||
is Failure.NetworkConnection -> Result.retry()
|
||||
else -> {
|
||||
monarchy.awaitTransaction { realm ->
|
||||
PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
|
||||
// update it
|
||||
it.state = PusherState.FAILED_TO_REGISTER
|
||||
}
|
||||
else -> {
|
||||
sessionDatabase.awaitTransaction(coroutineDispatchers) {
|
||||
sessionDatabase.pushersQueries.updateState(PusherState.FAILED_TO_REGISTER.name, pusher.pushKey)
|
||||
}
|
||||
// always return success, or the chain will be stuck for ever!
|
||||
Result.failure()
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,24 +88,9 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
|||
executeRequest<Unit>(eventBus) {
|
||||
apiCall = pushersAPI.setPusher(pusher)
|
||||
}
|
||||
monarchy.awaitTransaction { realm ->
|
||||
val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
|
||||
if (echo != null) {
|
||||
// update it
|
||||
echo.appDisplayName = pusher.appDisplayName
|
||||
echo.appId = pusher.appId
|
||||
echo.kind = pusher.kind
|
||||
echo.lang = pusher.lang
|
||||
echo.profileTag = pusher.profileTag
|
||||
echo.data?.format = pusher.data?.format
|
||||
echo.data?.url = pusher.data?.url
|
||||
echo.state = PusherState.REGISTERED
|
||||
} else {
|
||||
pusher.toEntity().also {
|
||||
it.state = PusherState.REGISTERED
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
sessionDatabase.awaitTransaction(coroutineDispatchers) {
|
||||
val pusherEntity = pushersMapper.map(pusher, PusherState.REGISTERED)
|
||||
sessionDatabase.pushersQueries.insertOrReplace(pusherEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,9 +60,6 @@ internal abstract class PushersModule {
|
|||
@Binds
|
||||
abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,16 +15,9 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.AggregatedAnnotation
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.PollSummaryContent
|
||||
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
|
||||
import im.vector.matrix.android.api.session.room.model.VoteInfo
|
||||
|
@ -34,21 +27,18 @@ import im.vector.matrix.android.api.session.room.model.message.MessageRelationCo
|
|||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||
import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.sqldelight.session.*
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -56,8 +46,7 @@ import javax.inject.Inject
|
|||
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val userId: String
|
||||
val eventInsertNotifications: List<EventInsertNotification>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -92,59 +81,52 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
|
|||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||
*/
|
||||
internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
private val monarchy: Monarchy,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
@UserId private val userId: String,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val cryptoService: CryptoService) : EventRelationsAggregationTask {
|
||||
|
||||
// OPT OUT serer aggregation until API mature enough
|
||||
private val SHOULD_HANDLE_SERVER_AGREGGATION = false
|
||||
|
||||
override suspend fun execute(params: EventRelationsAggregationTask.Params) {
|
||||
val events = params.events
|
||||
val userId = params.userId
|
||||
monarchy.awaitTransaction { realm ->
|
||||
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${events.size} events")
|
||||
update(realm, events, userId)
|
||||
val eventInsertNotifications = params.eventInsertNotifications
|
||||
sessionDatabase.awaitTransaction(coroutineDispatchers) {
|
||||
Timber.v(">>> DefaultEventRelationsAggregationTask[${params.hashCode()}] called with ${eventInsertNotifications.size} events")
|
||||
update(eventInsertNotifications, userId)
|
||||
Timber.v("<<< DefaultEventRelationsAggregationTask[${params.hashCode()}] finished")
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, events: List<Event>, userId: String) {
|
||||
events.forEach { event ->
|
||||
private fun update(eventInsertNotifications: List<EventInsertNotification>, userId: String) {
|
||||
eventInsertNotifications.forEach { eventInsertNotification ->
|
||||
try { // Temporary catch, should be removed
|
||||
val roomId = event.roomId
|
||||
if (roomId == null) {
|
||||
Timber.w("Event has no room id ${event.eventId}")
|
||||
return@forEach
|
||||
}
|
||||
val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
|
||||
when (event.type) {
|
||||
EventType.REACTION -> {
|
||||
val roomId = eventInsertNotification.room_id
|
||||
val eventId = eventInsertNotification.event_id
|
||||
val isLocalEcho = LocalEcho.isLocalEchoId(eventId)
|
||||
val event = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()?.asDomain()
|
||||
?: return@forEach
|
||||
when (eventInsertNotification.type) {
|
||||
EventType.REACTION -> {
|
||||
// we got a reaction!!
|
||||
Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
|
||||
handleReaction(event, roomId, realm, userId, isLocalEcho)
|
||||
Timber.v("###REACTION in room $roomId , reaction eventID ${eventId}")
|
||||
handleReaction(event, roomId, userId, isLocalEcho)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||
|
||||
EventAnnotationsSummaryEntity.where(realm, event.eventId
|
||||
?: "").findFirst()?.let {
|
||||
TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId
|
||||
?: "").findFirst()?.let { tet ->
|
||||
tet.annotations = it
|
||||
}
|
||||
}
|
||||
//handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations)
|
||||
}
|
||||
|
||||
val content: MessageContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
handleReplace(event, content, roomId, isLocalEcho)
|
||||
} else if (content?.relatesTo?.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, content, roomId, isLocalEcho)
|
||||
handleResponse(userId, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,12 +140,12 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
|
||||
if (it.type == RelationType.REFERENCE && it.eventId != null) {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it.eventId, userId)
|
||||
handleVerification(event, roomId, isLocalEcho, it.eventId, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventType.ENCRYPTED -> {
|
||||
EventType.ENCRYPTED -> {
|
||||
// Relation type is in clear
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
|
||||
if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE
|
||||
|
@ -175,10 +157,10 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
// A replace!
|
||||
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
handleReplace(event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
} else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) {
|
||||
Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
|
||||
handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
//handleResponse( realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
|
||||
}
|
||||
}
|
||||
} else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) {
|
||||
|
@ -193,33 +175,33 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
|
||||
encryptedEventContent.relatesTo.eventId?.let {
|
||||
handleVerification(realm, event, roomId, isLocalEcho, it, userId)
|
||||
handleVerification(event, roomId, isLocalEcho, it, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { sessionDatabase.eventQueries.select(event.redacts).executeAsOneOrNull() }
|
||||
?: return@forEach
|
||||
when (eventToPrune.type) {
|
||||
EventType.MESSAGE -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
EventType.MESSAGE -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.event_id}")
|
||||
// val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
// ?: UnsignedData(null, null)
|
||||
|
||||
// was this event a m.replace
|
||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, roomId)
|
||||
}
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
handleReactionRedact(eventToPrune, realm, userId)
|
||||
handleReactionRedact(eventToPrune, roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Timber.v("UnHandled event ${event.eventId}")
|
||||
else -> Timber.v("UnHandled event ${eventInsertNotification.event_id}")
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Timber.e(t, "## Should not happen ")
|
||||
|
@ -230,7 +212,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
@ -244,69 +226,85 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||
private fun handleReplace(event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) {
|
||||
val eventId = event.eventId ?: return
|
||||
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
|
||||
val newContent = content.newContent ?: return
|
||||
// ok, this is a replace
|
||||
val existing = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, targetEventId)
|
||||
|
||||
// we have it
|
||||
val existingSummary = existing.editSummary
|
||||
val existingSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(targetEventId).executeAsOneOrNull()
|
||||
if (existingSummary == null) {
|
||||
Timber.v("###REPLACE new edit summary for $targetEventId, creating one (localEcho:$isLocalEcho)")
|
||||
// create the edit summary
|
||||
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
||||
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
val lastEditTs: Long
|
||||
val sourceLocalEchoEvents: List<String>
|
||||
val sourceEvents: List<String>
|
||||
if (isLocalEcho) {
|
||||
editSummary.lastEditTs = 0
|
||||
editSummary.sourceLocalEchoEvents.add(eventId)
|
||||
lastEditTs = 0
|
||||
sourceLocalEchoEvents = listOf(eventId)
|
||||
sourceEvents = emptyList()
|
||||
} else {
|
||||
editSummary.lastEditTs = event.originServerTs ?: 0
|
||||
editSummary.sourceEvents.add(eventId)
|
||||
lastEditTs = event.originServerTs ?: 0
|
||||
sourceLocalEchoEvents = emptyList()
|
||||
sourceEvents = listOf(eventId)
|
||||
}
|
||||
|
||||
existing.editSummary = editSummary
|
||||
val newEditSummary = EditAggregatedSummary.Impl(
|
||||
event_id = targetEventId,
|
||||
room_id = roomId,
|
||||
aggregated_content = ContentMapper.map(newContent),
|
||||
last_edit_ts = lastEditTs,
|
||||
source_local_echo_ids = sourceLocalEchoEvents,
|
||||
source_event_ids = sourceEvents
|
||||
)
|
||||
sessionDatabase.eventAnnotationsQueries.insertNewEdit(newEditSummary)
|
||||
} else {
|
||||
if (existingSummary.sourceEvents.contains(eventId)) {
|
||||
val sourceEvents = existingSummary.source_event_ids.toMutableList()
|
||||
if (sourceEvents.contains(eventId)) {
|
||||
// ignore this event, we already know it (??)
|
||||
Timber.v("###REPLACE ignoring event for summary, it's known $eventId")
|
||||
return
|
||||
}
|
||||
val txId = event.unsignedData?.transactionId
|
||||
// is it a remote echo?
|
||||
if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||
val sourceLocalEchoEvents = existingSummary.source_local_echo_ids.toMutableList()
|
||||
if (!isLocalEcho && sourceLocalEchoEvents.contains(txId)) {
|
||||
// ok it has already been managed
|
||||
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
|
||||
existingSummary.sourceLocalEchoEvents.remove(txId)
|
||||
existingSummary.sourceEvents.add(event.eventId)
|
||||
sourceLocalEchoEvents.remove(txId)
|
||||
sourceEvents.add(event.eventId)
|
||||
|
||||
} else if (
|
||||
isLocalEcho // do not rely on ts for local echo, take it
|
||||
|| event.originServerTs ?: 0 >= existingSummary.lastEditTs
|
||||
|| event.originServerTs ?: 0 >= existingSummary.last_edit_ts
|
||||
) {
|
||||
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
|
||||
if (!isLocalEcho) {
|
||||
// Do not take local echo originServerTs here, could mess up ordering (keep old ts)
|
||||
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
val newLastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditLastTs(newLastEditTs, targetEventId)
|
||||
}
|
||||
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
val newAggregatedContent = ContentMapper.map(newContent)
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditContent(newAggregatedContent, targetEventId)
|
||||
if (isLocalEcho) {
|
||||
existingSummary.sourceLocalEchoEvents.add(eventId)
|
||||
sourceLocalEchoEvents.add(eventId)
|
||||
} else {
|
||||
existingSummary.sourceEvents.add(eventId)
|
||||
sourceEvents.add(eventId)
|
||||
}
|
||||
} else {
|
||||
// ignore this event for the summary (back paginate)
|
||||
if (!isLocalEcho) {
|
||||
existingSummary.sourceEvents.add(eventId)
|
||||
sourceEvents.add(eventId)
|
||||
}
|
||||
Timber.v("###REPLACE ignoring event for summary, it's to old $eventId")
|
||||
}
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditSources(
|
||||
sourceEventIds = sourceEvents,
|
||||
sourceLocalEchoIds = sourceLocalEchoEvents,
|
||||
eventId = targetEventId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResponse(realm: Realm,
|
||||
userId: String,
|
||||
private fun handleResponse(userId: String,
|
||||
event: Event,
|
||||
content: MessageContent,
|
||||
roomId: String,
|
||||
|
@ -317,45 +315,38 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
|
||||
val eventTimestamp = event.originServerTs ?: return
|
||||
|
||||
// ok, this is a poll response
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||
if (existing == null) {
|
||||
Timber.v("## POLL creating new relation summary for $targetEventId")
|
||||
existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId)
|
||||
}
|
||||
|
||||
// we have it
|
||||
val existingPollSummary = existing.pollResponseSummary
|
||||
?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
|
||||
existing.pollResponseSummary = it
|
||||
}
|
||||
|
||||
val closedTime = existingPollSummary?.closedTime
|
||||
val existingPollSummary = sessionDatabase.eventAnnotationsQueries.selectPollForEvent(targetEventId).executeAsOneOrNull()
|
||||
val closedTime = existingPollSummary?.closed_time
|
||||
if (closedTime != null && eventTimestamp > closedTime) {
|
||||
Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
|
||||
return
|
||||
}
|
||||
val sumModel = ContentMapper.map(existingPollSummary?.content).toModel<PollSummaryContent>()
|
||||
?: PollSummaryContent()
|
||||
|
||||
val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent()
|
||||
|
||||
if (existingPollSummary!!.sourceEvents.contains(eventId)) {
|
||||
val sourceEvents = existingPollSummary?.source_event_ids?.toMutableList() ?: ArrayList()
|
||||
if (sourceEvents.contains(eventId)) {
|
||||
// ignore this event, we already know it (??)
|
||||
Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId")
|
||||
return
|
||||
}
|
||||
val txId = event.unsignedData?.transactionId
|
||||
// is it a remote echo?
|
||||
if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||
val sourceLocalEchoEvents = existingPollSummary?.source_local_echo_ids?.toMutableList()
|
||||
?: ArrayList()
|
||||
if (!isLocalEcho && sourceLocalEchoEvents.contains(txId)) {
|
||||
// ok it has already been managed
|
||||
Timber.v("## POLL Receiving remote echo of response eventId:$eventId")
|
||||
existingPollSummary.sourceLocalEchoEvents.remove(txId)
|
||||
existingPollSummary.sourceEvents.add(event.eventId)
|
||||
sourceLocalEchoEvents.remove(txId)
|
||||
sourceEvents.add(event.eventId)
|
||||
sessionDatabase.eventAnnotationsQueries.updatePollSources(sourceEvents, sourceLocalEchoEvents, targetEventId)
|
||||
return
|
||||
}
|
||||
|
||||
val responseContent = event.content.toModel<MessagePollResponseContent>() ?: return Unit.also {
|
||||
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
val responseContent = event.content.toModel<MessagePollResponseContent>()
|
||||
?: return Unit.also {
|
||||
Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}")
|
||||
}
|
||||
|
||||
val optionIndex = responseContent.relatesTo?.option ?: return Unit.also {
|
||||
Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
|
||||
|
@ -385,12 +376,13 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
}
|
||||
sumModel.votes = votes
|
||||
if (isLocalEcho) {
|
||||
existingPollSummary.sourceLocalEchoEvents.add(eventId)
|
||||
sourceLocalEchoEvents.add(eventId)
|
||||
} else {
|
||||
existingPollSummary.sourceEvents.add(eventId)
|
||||
sourceEvents.add(eventId)
|
||||
}
|
||||
|
||||
existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
|
||||
val newContent = ContentMapper.map(sumModel.toContent())
|
||||
sessionDatabase.eventAnnotationsQueries.updatePollSources(sourceEvents, sourceLocalEchoEvents, targetEventId)
|
||||
sessionDatabase.eventAnnotationsQueries.updatePollContent(newContent, targetEventId)
|
||||
}
|
||||
|
||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||
|
@ -415,7 +407,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
|
||||
private fun handleReaction(event: Event, roomId: String, userId: String, isLocalEcho: Boolean) {
|
||||
val content = event.content.toModel<ReactionContent>()
|
||||
if (content == null) {
|
||||
Timber.e("Malformed reaction content ${event.content}")
|
||||
|
@ -425,50 +417,70 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
val relatedEventID = content.relatesTo.eventId
|
||||
val reactionEventId = event.eventId
|
||||
val reactionEventId = event.eventId ?: return
|
||||
Timber.v("Reaction $reactionEventId relates to $relatedEventID")
|
||||
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventID)
|
||||
|
||||
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||
val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(relatedEventID, reaction).executeAsOneOrNull()
|
||||
val txId = event.unsignedData?.transactionId
|
||||
if (isLocalEcho && txId.isNullOrBlank()) {
|
||||
Timber.w("Received a local echo with no transaction ID")
|
||||
return
|
||||
}
|
||||
if (sum == null) {
|
||||
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||
sum.key = reaction
|
||||
sum.firstTimestamp = event.originServerTs ?: 0
|
||||
if (isLocalEcho) {
|
||||
if (reactionSummary == null) {
|
||||
Timber.v("$reaction is a new reaction")
|
||||
val (sourceEventIds, sourceLocalEchoIds) = if (isLocalEcho && txId != null) {
|
||||
Timber.v("Adding local echo reaction $reaction")
|
||||
sum.sourceLocalEcho.add(txId)
|
||||
sum.count = 1
|
||||
Pair(emptyList(), listOf(txId))
|
||||
} else {
|
||||
Timber.v("Adding synced reaction $reaction")
|
||||
sum.count = 1
|
||||
sum.sourceEvents.add(reactionEventId)
|
||||
Pair(listOf(reactionEventId), emptyList<String>())
|
||||
}
|
||||
sum.addedByMe = sum.addedByMe || (userId == event.senderId)
|
||||
eventSummary.reactionsSummary.add(sum)
|
||||
val newReactionSummary = ReactionAggregatedSummary.Impl(
|
||||
event_id = relatedEventID,
|
||||
room_id = roomId,
|
||||
key = reaction,
|
||||
count = 1,
|
||||
added_by_me = userId == event.senderId,
|
||||
first_timestamp = event.originServerTs ?: 0,
|
||||
source_event_ids = sourceEventIds,
|
||||
source_local_echo_ids = sourceLocalEchoIds
|
||||
)
|
||||
sessionDatabase.eventAnnotationsQueries.insertNewReaction(newReactionSummary)
|
||||
} else {
|
||||
Timber.v("$reaction is an already known reaction")
|
||||
// is this a known event (is possible? pagination?)
|
||||
if (!sum.sourceEvents.contains(reactionEventId)) {
|
||||
val sourceEvents = reactionSummary.source_event_ids.toMutableList()
|
||||
if (!sourceEvents.contains(reactionEventId)) {
|
||||
// check if it's not the sync of a local echo
|
||||
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
|
||||
val sourceLocalEcho = reactionSummary.source_local_echo_ids.toMutableList()
|
||||
if (!isLocalEcho && sourceLocalEcho.contains(txId)) {
|
||||
// ok it has already been counted, just sync the list, do not touch count
|
||||
Timber.v("Ignoring synced of local echo for reaction $reaction")
|
||||
sum.sourceLocalEcho.remove(txId)
|
||||
sum.sourceEvents.add(reactionEventId)
|
||||
sourceLocalEcho.remove(txId)
|
||||
sourceEvents.add(reactionEventId)
|
||||
sessionDatabase.eventAnnotationsQueries.updateLocalReaction(
|
||||
sourceEventIds = sourceEvents,
|
||||
sourceLocalEchoIds = sourceLocalEcho,
|
||||
eventId = relatedEventID,
|
||||
key = reaction
|
||||
)
|
||||
} else {
|
||||
sum.count += 1
|
||||
if (isLocalEcho) {
|
||||
val newCount = reactionSummary.count + 1
|
||||
val newAddedByMe = reactionSummary.added_by_me || (userId == event.senderId)
|
||||
if (isLocalEcho && txId != null) {
|
||||
Timber.v("Adding local echo reaction $reaction")
|
||||
sum.sourceLocalEcho.add(txId)
|
||||
sourceLocalEcho.add(txId)
|
||||
} else {
|
||||
Timber.v("Adding synced reaction $reaction")
|
||||
sum.sourceEvents.add(reactionEventId)
|
||||
sourceEvents.add(reactionEventId)
|
||||
}
|
||||
|
||||
sum.addedByMe = sum.addedByMe || (userId == event.senderId)
|
||||
sessionDatabase.eventAnnotationsQueries.updateReaction(
|
||||
count = newCount,
|
||||
addedByMe = newAddedByMe,
|
||||
sourceEventIds = sourceEvents,
|
||||
sourceLocalEchoIds = sourceLocalEcho,
|
||||
eventId = relatedEventID,
|
||||
key = reaction
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -480,31 +492,34 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
/**
|
||||
* Called when an event is deleted
|
||||
*/
|
||||
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
||||
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, roomId: String) {
|
||||
Timber.d("Handle redaction of m.replace")
|
||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
||||
if (eventSummary == null) {
|
||||
val editSummary = sessionDatabase.eventAnnotationsQueries.selectEditForEvent(relatedEventId).executeAsOneOrNull()
|
||||
if (editSummary == null) {
|
||||
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
||||
return
|
||||
}
|
||||
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
||||
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
||||
if (sourceToDiscard == null) {
|
||||
val sourceEvents = editSummary.source_event_ids.toMutableList()
|
||||
val sourceToDiscard = sourceEvents.indexOf(redacted.event_id)
|
||||
if (sourceToDiscard == -1) {
|
||||
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||
return
|
||||
}
|
||||
// Need to remove this event from the redaction list and compute new aggregation state
|
||||
sourceEvents.removeAt(sourceToDiscard)
|
||||
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
||||
val previousEdit = sourceEvents.mapNotNull { sessionDatabase.eventQueries.select(it).executeAsOneOrNull() }.sortedBy { it.origin_server_ts }.lastOrNull()
|
||||
if (previousEdit == null) {
|
||||
// revert to original
|
||||
eventSummary.editSummary?.deleteFromRealm()
|
||||
sessionDatabase.eventAnnotationsQueries.deleteEdit(relatedEventId, roomId)
|
||||
} else {
|
||||
// I have the last event
|
||||
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
||||
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
||||
val newLastEditTs = previousEdit.origin_server_ts
|
||||
?: System.currentTimeMillis()
|
||||
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
||||
val newAggregatedContent = ContentMapper.map(newContent)
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditSources(sourceEvents, editSummary.source_local_echo_ids, relatedEventId)
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditLastTs(newLastEditTs, relatedEventId)
|
||||
sessionDatabase.eventAnnotationsQueries.updateEditContent(newAggregatedContent, relatedEventId)
|
||||
} ?: run {
|
||||
Timber.e("Failed to udate edited summary")
|
||||
// TODO how to reccover that
|
||||
|
@ -512,8 +527,8 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
||||
Timber.v("REDACTION of reaction ${eventToPrune.eventId}")
|
||||
fun handleReactionRedact(eventToPrune: EventEntity, roomId: String) {
|
||||
Timber.v("REDACTION of reaction ${eventToPrune.event_id}")
|
||||
// delete a reaction, need to update the annotation summary if any
|
||||
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
||||
?: return
|
||||
|
@ -521,78 +536,110 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
|||
|
||||
val reactionKey = reactionContent.relatesTo.key
|
||||
Timber.v("REMOVE reaction for key $reactionKey")
|
||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
||||
if (summary != null) {
|
||||
summary.reactionsSummary.where()
|
||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)
|
||||
.findFirst()?.let { aggregation ->
|
||||
Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})")
|
||||
Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}")
|
||||
if (aggregation.sourceEvents.contains(eventToPrune.eventId)) {
|
||||
Timber.v("REMOVE reaction for key $reactionKey")
|
||||
aggregation.sourceEvents.remove(eventToPrune.eventId)
|
||||
Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}")
|
||||
aggregation.count = aggregation.count - 1
|
||||
if (eventToPrune.sender == userId) {
|
||||
// Was it a redact on my reaction?
|
||||
aggregation.addedByMe = false
|
||||
}
|
||||
if (aggregation.count == 0) {
|
||||
// delete!
|
||||
aggregation.deleteFromRealm()
|
||||
}
|
||||
} else {
|
||||
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
||||
}
|
||||
}
|
||||
val reactionSummary = sessionDatabase.eventAnnotationsQueries.selectReaction(eventThatWasReacted, reactionKey).executeAsOneOrNull()
|
||||
if (reactionSummary != null) {
|
||||
val sourceEvents = reactionSummary.source_event_ids.toMutableList()
|
||||
Timber.v("Find summary for key with ${sourceEvents.size} known reactions (count:${reactionSummary.count})")
|
||||
if (sourceEvents.contains(eventToPrune.event_id)) {
|
||||
Timber.v("REMOVE reaction for key $reactionKey")
|
||||
sourceEvents.remove(eventToPrune.event_id)
|
||||
val newCount = reactionSummary.count - 1
|
||||
val addedByMe = if (eventToPrune.sender_id == userId) {
|
||||
// Was it a redact on my reaction?
|
||||
false
|
||||
} else {
|
||||
reactionSummary.added_by_me
|
||||
}
|
||||
if (newCount == 0L) {
|
||||
sessionDatabase.eventAnnotationsQueries.deleteReaction(eventThatWasReacted, roomId, reactionKey)
|
||||
} else {
|
||||
sessionDatabase.eventAnnotationsQueries.updateReaction(
|
||||
count = newCount,
|
||||
addedByMe = addedByMe,
|
||||
sourceEventIds = sourceEvents,
|
||||
sourceLocalEchoIds = reactionSummary.source_local_echo_ids,
|
||||
eventId = eventThatWasReacted,
|
||||
key = reactionKey
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.event_id} is not known")
|
||||
}
|
||||
} else {
|
||||
Timber.e("## Cannot find summary for key $reactionKey")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleVerification(realm: Realm, event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
|
||||
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
|
||||
|
||||
val verifSummary = eventSummary.referencesSummaryEntity
|
||||
?: ReferencesAggregatedSummaryEntity.create(realm, relatedEventId).also {
|
||||
eventSummary.referencesSummaryEntity = it
|
||||
}
|
||||
|
||||
val txId = event.unsignedData?.transactionId
|
||||
|
||||
if (!isLocalEcho && verifSummary.sourceLocalEcho.contains(txId)) {
|
||||
// ok it has already been handled
|
||||
} else {
|
||||
ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
|
||||
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
|
||||
?: ReferencesAggregatedContent(VerificationState.REQUEST)
|
||||
// TODO ignore invalid messages? e.g a START after a CANCEL?
|
||||
// i.e. never change state if already canceled/done
|
||||
val currentState = data.verificationState
|
||||
val newState = when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING)
|
||||
EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) {
|
||||
VerificationState.CANCELED_BY_ME
|
||||
} else {
|
||||
VerificationState.CANCELED_BY_OTHER
|
||||
})
|
||||
EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE)
|
||||
else -> VerificationState.REQUEST
|
||||
private fun handleVerification(event: Event, roomId: String, isLocalEcho: Boolean, relatedEventId: String, userId: String) {
|
||||
val eventId = event.eventId ?: return
|
||||
val verifSummary = sessionDatabase.eventAnnotationsQueries.selectReferenceForEvent(relatedEventId).executeAsOneOrNull()
|
||||
if (verifSummary == null) {
|
||||
val state = VerificationState.REQUEST.computeNewVerificationState(event)
|
||||
val data = ReferencesAggregatedContent(state)
|
||||
val sourceLocalEchoEvents: List<String>
|
||||
val sourceEvents: List<String>
|
||||
if (isLocalEcho) {
|
||||
sourceLocalEchoEvents = listOf(eventId)
|
||||
sourceEvents = emptyList()
|
||||
} else {
|
||||
sourceLocalEchoEvents = emptyList()
|
||||
sourceEvents = listOf(eventId)
|
||||
}
|
||||
val newVerifSummary = ReferencesAggregatedSummary.Impl(
|
||||
event_id = relatedEventId,
|
||||
room_id = roomId,
|
||||
content = ContentMapper.map(data.toContent()),
|
||||
source_local_echo_ids = sourceLocalEchoEvents,
|
||||
source_event_ids = sourceEvents
|
||||
)
|
||||
sessionDatabase.eventAnnotationsQueries.insertNewReference(newVerifSummary)
|
||||
|
||||
data = data.copy(verificationState = newState)
|
||||
verifSummary.content = ContentMapper.map(data.toContent())
|
||||
}
|
||||
|
||||
if (isLocalEcho) {
|
||||
verifSummary.sourceLocalEcho.add(event.eventId)
|
||||
} else {
|
||||
verifSummary.sourceLocalEcho.remove(txId)
|
||||
verifSummary.sourceEvents.add(event.eventId)
|
||||
val txId = event.unsignedData?.transactionId
|
||||
val sourceEvents = verifSummary.source_event_ids.toMutableList()
|
||||
val sourceLocalEcho = verifSummary.source_local_echo_ids.toMutableList()
|
||||
if (!isLocalEcho && sourceLocalEcho.contains(txId)) {
|
||||
// ok it has already been handled
|
||||
} else {
|
||||
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
|
||||
?: ReferencesAggregatedContent(VerificationState.REQUEST)
|
||||
// TODO ignore invalid messages? e.g a START after a CANCEL?
|
||||
// i.e. never change state if already canceled/done
|
||||
val currentState = data.verificationState
|
||||
val newState = currentState.computeNewVerificationState(event)
|
||||
data = data.copy(verificationState = newState)
|
||||
val newContent = ContentMapper.map(data.toContent())
|
||||
sessionDatabase.eventAnnotationsQueries.updateReferenceContent(newContent, relatedEventId)
|
||||
}
|
||||
if (isLocalEcho) {
|
||||
sourceLocalEcho.add(eventId)
|
||||
} else {
|
||||
sourceLocalEcho.remove(txId)
|
||||
sourceEvents.add(event.eventId)
|
||||
}
|
||||
sessionDatabase.eventAnnotationsQueries.updateReferenceSources(
|
||||
sourceEventIds = sourceEvents,
|
||||
sourceLocalEchoIds = sourceLocalEcho,
|
||||
eventId = relatedEventId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun VerificationState.computeNewVerificationState(event: Event): VerificationState {
|
||||
return when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC -> toState(VerificationState.WAITING)
|
||||
EventType.KEY_VERIFICATION_CANCEL -> toState(if (event.senderId == userId) {
|
||||
VerificationState.CANCELED_BY_ME
|
||||
} else {
|
||||
VerificationState.CANCELED_BY_OTHER
|
||||
})
|
||||
EventType.KEY_VERIFICATION_DONE -> toState(VerificationState.DONE)
|
||||
else -> VerificationState.REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,19 +15,10 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import im.vector.matrix.android.internal.database.SqlLiveEntityObserver
|
||||
import im.vector.matrix.sqldelight.session.EventInsertNotification
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -36,41 +27,32 @@ import javax.inject.Inject
|
|||
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
||||
*/
|
||||
internal class EventRelationsAggregationUpdater @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
@UserId private val userId: String,
|
||||
sessionDatabase: SessionDatabase,
|
||||
private val task: EventRelationsAggregationTask) :
|
||||
RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
SqlLiveEntityObserver<EventInsertNotification>(sessionDatabase) {
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.REACTION,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
override val query = sessionDatabase.observerTriggerQueries.getAllEventInsertNotifications(
|
||||
types = listOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.REDACTION,
|
||||
EventType.REACTION,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
// TODO Add ?
|
||||
// EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
|
||||
|
||||
override suspend fun handleChanges(results: List<EventInsertNotification>) {
|
||||
val params = EventRelationsAggregationTask.Params(results)
|
||||
task.execute(params)
|
||||
val notificationIds = results.map { it.event_id }
|
||||
sessionDatabase.observerTriggerQueries.deleteEventInsertNotifications(notificationIds)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
|
||||
|
||||
val insertedDomains = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.toList()
|
||||
val params = EventRelationsAggregationTask.Params(
|
||||
insertedDomains,
|
||||
userId
|
||||
)
|
||||
observerScope.launch {
|
||||
task.execute(params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,48 +16,34 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.api.session.room.model.*
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.isEventRead
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||
import im.vector.matrix.android.internal.database.query.isEventRead
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.whereType
|
||||
import im.vector.matrix.android.internal.database.mapper.map
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomDisplayNameResolver
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventDecryptor
|
||||
import im.vector.matrix.android.internal.session.sync.RoomSyncHandler
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||
import io.realm.Realm
|
||||
import im.vector.matrix.sqldelight.session.RoomSummaryHeroes
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomSummaryUpdater @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||
private val roomAvatarResolver: RoomAvatarResolver,
|
||||
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
|
||||
private val eventBus: EventBus,
|
||||
private val monarchy: Monarchy) {
|
||||
private val eventBus: EventBus) {
|
||||
|
||||
companion object {
|
||||
// TODO: maybe allow user of SDK to give that list
|
||||
|
@ -79,107 +65,139 @@ internal class RoomSummaryUpdater @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun update(realm: Realm,
|
||||
roomId: String,
|
||||
membership: Membership? = null,
|
||||
fun update(roomId: String,
|
||||
newMembership: Membership? = null,
|
||||
roomSummary: RoomSyncSummary? = null,
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||
updateMembers: Boolean = false,
|
||||
ephemeralResult: RoomSyncHandler.EphemeralResult? = null,
|
||||
inviterId: String? = null) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
if (roomSummary != null) {
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
roomSummaryEntity.heroes.clear()
|
||||
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
|
||||
}
|
||||
if (roomSummary.invitedMembersCount != null) {
|
||||
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
|
||||
}
|
||||
if (roomSummary.joinedMembersCount != null) {
|
||||
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
|
||||
}
|
||||
}
|
||||
roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0
|
||||
roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0
|
||||
|
||||
if (membership != null) {
|
||||
roomSummaryEntity.membership = membership
|
||||
val currentRoomSummary = sessionDatabase.roomSummaryQueries.get(roomId).executeAsOneOrNull()
|
||||
val heroes = if (roomSummary != null && roomSummary.heroes.isNotEmpty()) {
|
||||
roomSummary.heroes
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
sessionDatabase.roomSummaryQueries.deleteHeroes(roomId)
|
||||
heroes.forEach {
|
||||
sessionDatabase.roomSummaryQueries.setHeroes(RoomSummaryHeroes.Impl(it, roomId))
|
||||
}
|
||||
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
|
||||
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
|
||||
val invitedMemberCount = if (roomSummary?.invitedMembersCount != null) {
|
||||
roomSummary.invitedMembersCount
|
||||
} else {
|
||||
currentRoomSummary?.invited_members_count ?: 0
|
||||
}
|
||||
val joinedMemberCount = if (roomSummary?.joinedMembersCount != null) {
|
||||
roomSummary.joinedMembersCount
|
||||
} else {
|
||||
currentRoomSummary?.joined_members_count ?: 0
|
||||
}
|
||||
val highlightCount = unreadNotifications?.highlightCount ?: 0
|
||||
val notificationCount = unreadNotifications?.notificationCount ?: 0
|
||||
|
||||
val membership = newMembership ?: currentRoomSummary?.membership?.map() ?: Membership.NONE
|
||||
val latestPreviewableEventId = getLastestKnownEventId(roomId = roomId)
|
||||
val lastTopicEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "").executeAsOneOrNull()
|
||||
val lastCanonicalAliasEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "").executeAsOneOrNull()
|
||||
val lastAliasesEvent = sessionDatabase.stateEventQueries.getCurrentStateEvent(roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "").executeAsOneOrNull()
|
||||
|
||||
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
|
||||
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
|
||||
val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
|
||||
|
||||
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
|
||||
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
|
||||
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
|
||||
.findFirst()
|
||||
val encryptionEvent = sessionDatabase.eventQueries.findWithContent(roomId = roomId, content = "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"").executeAsList().firstOrNull()
|
||||
|
||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||
val hasUnreadMessages = notificationCount > 0
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
|| !sessionDatabase.isEventRead(userId, roomId, latestPreviewableEventId)
|
||||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
|
||||
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
||||
val displayName = roomDisplayNameResolver.resolve(roomId, membership).toString()
|
||||
val avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
val topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
|
||||
val canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
||||
?.canonicalAlias
|
||||
|
||||
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
|
||||
?: emptyList()
|
||||
roomSummaryEntity.aliases.clear()
|
||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
||||
roomSummaryEntity.typingUserIds.clear()
|
||||
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
|
||||
|
||||
if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {
|
||||
roomSummaryEntity.inviterId = inviterId
|
||||
} else if (roomSummaryEntity.membership != Membership.INVITE) {
|
||||
roomSummaryEntity.inviterId = null
|
||||
sessionDatabase.roomAliasesQueries.deleteAllForRoom(roomId)
|
||||
roomAliases.forEach { alias ->
|
||||
sessionDatabase.roomAliasesQueries.insert(roomId, alias)
|
||||
}
|
||||
|
||||
if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
|
||||
Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
|
||||
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
|
||||
val isEncrypted = encryptionEvent != null
|
||||
val directUserId = currentRoomSummary?.direct_user_id
|
||||
sessionDatabase.userQueries.deleteAllTypingUsers()
|
||||
ephemeralResult?.typingUserIds?.forEach { typingId ->
|
||||
sessionDatabase.userQueries.insertTyping(roomId, typingId)
|
||||
}
|
||||
val isDirect = currentRoomSummary?.is_direct ?: false
|
||||
|
||||
if (updateMembers) {
|
||||
val otherRoomMembers = RoomMemberHelper(realm, roomId)
|
||||
.queryRoomMembersEvent()
|
||||
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
|
||||
.findAll()
|
||||
.asSequence()
|
||||
.map { it.userId }
|
||||
|
||||
roomSummaryEntity.otherMemberIds.clear()
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
|
||||
if (roomSummaryEntity.isEncrypted) {
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val listToCheck = if (roomSummaryEntity.isDirect) {
|
||||
roomSummaryEntity.otherMemberIds.toList()
|
||||
} else {
|
||||
roomSummaryEntity.otherMemberIds.toList() + userId
|
||||
}
|
||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
|
||||
if (latestPreviewableEventId != null && latestPreviewableEventId.isNotEmpty()) {
|
||||
if (selectEventType(latestPreviewableEventId) == EventType.ENCRYPTED
|
||||
&& selectDecryptionResult(latestPreviewableEventId) == null) {
|
||||
Timber.v("Should decrypt $latestPreviewableEventId for room: $displayName")
|
||||
timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEventId, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShieldTrust(realm: Realm,
|
||||
roomId: String,
|
||||
trust: RoomEncryptionTrustLevel?) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
if (roomSummaryEntity.isEncrypted) {
|
||||
roomSummaryEntity.roomEncryptionTrustLevel = trust
|
||||
val newInviterId = if (membership == Membership.INVITE && inviterId != null) {
|
||||
inviterId
|
||||
} else if (membership != Membership.INVITE) {
|
||||
null
|
||||
} else {
|
||||
currentRoomSummary?.inviter_id
|
||||
}
|
||||
val newRoomSummaryEntity = im.vector.matrix.sqldelight.session.RoomSummaryEntity.Impl(
|
||||
room_id = roomId,
|
||||
membership = membership.map(),
|
||||
avatar_url = avatarUrl,
|
||||
display_name = displayName,
|
||||
invited_members_count = invitedMemberCount,
|
||||
topic = topic,
|
||||
joined_members_count = joinedMemberCount,
|
||||
latest_previewable_event = latestPreviewableEventId,
|
||||
is_direct = isDirect,
|
||||
notification_count = notificationCount,
|
||||
highlight_count = highlightCount,
|
||||
canonical_alias = canonicalAlias,
|
||||
is_encrypted = isEncrypted,
|
||||
has_unread = hasUnreadMessages,
|
||||
direct_user_id = directUserId,
|
||||
versioning_state = currentRoomSummary?.versioning_state
|
||||
?: VersioningState.NONE.name,
|
||||
room_encryption_trust_level = currentRoomSummary?.room_encryption_trust_level,
|
||||
inviter_id = newInviterId
|
||||
)
|
||||
sessionDatabase.roomSummaryQueries.insertOrUpdate(newRoomSummaryEntity)
|
||||
if (isEncrypted && updateMembers) {
|
||||
// The set of “all users” depends on the type of room:
|
||||
// For regular / topic rooms, all users including yourself, are considered when decorating a room
|
||||
// For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
|
||||
val excludedIds = if (isDirect) {
|
||||
listOf(userId)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val listToCheck = sessionDatabase.roomMemberSummaryQueries.getAllUserIdFromRoom(
|
||||
memberships = Membership.activeMemberships().map(),
|
||||
excludedIds = excludedIds,
|
||||
roomId = roomId
|
||||
).executeAsList()
|
||||
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, listToCheck))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLastestKnownEventId(roomId: String): String? {
|
||||
return sessionDatabase.timelineEventQueries.getLatestKnownEventId(roomId = roomId, types = PREVIEWABLE_TYPES).executeAsOneOrNull()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun selectEventType(eventId: String): String? {
|
||||
return sessionDatabase.eventQueries.selectType(eventId).executeAsOneOrNull()
|
||||
}
|
||||
|
||||
private fun selectDecryptionResult(eventId: String): String? {
|
||||
return sessionDatabase.eventQueries.selectDecryptionResult(eventId).executeAsOneOrNull()?.decryption_result_json
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,34 +16,14 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room.notification
|
||||
|
||||
import im.vector.matrix.android.api.pushrules.Action
|
||||
import im.vector.matrix.android.api.pushrules.Condition
|
||||
import im.vector.matrix.android.api.pushrules.RuleSetKey
|
||||
import im.vector.matrix.android.api.pushrules.getActions
|
||||
import im.vector.matrix.android.api.pushrules.*
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushCondition
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.pushrules.toJson
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
|
||||
import im.vector.matrix.android.internal.database.model.PushRuleEntity
|
||||
|
||||
internal fun PushRuleEntity.toRoomPushRule(): RoomPushRule? {
|
||||
val kind = parent?.firstOrNull()?.kind
|
||||
val pushRule = when (kind) {
|
||||
RuleSetKey.OVERRIDE -> {
|
||||
PushRulesMapper.map(this)
|
||||
}
|
||||
RuleSetKey.ROOM -> {
|
||||
PushRulesMapper.mapRoomRule(this)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
return if (pushRule == null || kind == null) {
|
||||
null
|
||||
} else {
|
||||
RoomPushRule(kind, pushRule)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? {
|
||||
return when {
|
||||
|
|
|
@ -16,32 +16,26 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room.read
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.isEventRead
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.database.helper.isEventRead
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultReadService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val setReadMarkersTask: SetReadMarkersTask,
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val readMarkerDataSource: ReadMarkerDataSource,
|
||||
private val readReceiptDataSource: ReadReceiptDataSource,
|
||||
@UserId private val userId: String
|
||||
) : ReadService {
|
||||
|
||||
|
@ -82,37 +76,19 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun isEventRead(eventId: String): Boolean {
|
||||
return isEventRead(monarchy, userId, roomId, eventId)
|
||||
return sessionDatabase.isEventRead(userId, roomId, eventId)
|
||||
}
|
||||
|
||||
override fun getReadMarkerLive(): LiveData<Optional<String>> {
|
||||
val liveRealmData = monarchy.findAllMappedWithChanges(
|
||||
{ ReadMarkerEntity.where(it, roomId) },
|
||||
{ it.eventId }
|
||||
)
|
||||
return Transformations.map(liveRealmData) {
|
||||
it.firstOrNull().toOptional()
|
||||
}
|
||||
override fun getReadMarkerLive(): Flow<Optional<String>> {
|
||||
return readMarkerDataSource.getReadMarkerLive(roomId)
|
||||
}
|
||||
|
||||
override fun getMyReadReceiptLive(): LiveData<Optional<String>> {
|
||||
val liveRealmData = monarchy.findAllMappedWithChanges(
|
||||
{ ReadReceiptEntity.where(it, roomId = roomId, userId = userId) },
|
||||
{ it.eventId }
|
||||
)
|
||||
return Transformations.map(liveRealmData) {
|
||||
it.firstOrNull().toOptional()
|
||||
}
|
||||
override fun getMyReadReceiptLive(): Flow<Optional<String>> {
|
||||
return readReceiptDataSource.getReadReceiptLive(roomId, userId)
|
||||
}
|
||||
|
||||
override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
|
||||
val liveRealmData = monarchy.findAllMappedWithChanges(
|
||||
{ ReadReceiptsSummaryEntity.where(it, eventId) },
|
||||
{ readReceiptsSummaryMapper.map(it) }
|
||||
)
|
||||
return Transformations.map(liveRealmData) {
|
||||
it.firstOrNull() ?: emptyList()
|
||||
}
|
||||
override fun getEventReadReceiptsLive(eventId: String): Flow<List<ReadReceipt>> {
|
||||
return readReceiptDataSource.getEventReadReceiptsLive(eventId)
|
||||
}
|
||||
|
||||
private fun ReadService.MarkAsReadParams.forceReadMarker(): Boolean {
|
||||
|
|
|
@ -168,7 +168,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
|
||||
override fun deleteFailedEcho(localEcho: TimelineEvent) {
|
||||
taskExecutor.executorScope.launch {
|
||||
localEchoRepository.deleteFailedEcho(roomId, localEcho)
|
||||
localEchoRepository.deleteFailedEcho(localEcho)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
eventsToResend.forEach {
|
||||
sendEvent(it)
|
||||
}
|
||||
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
localEchoRepository.updateSendState(eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,8 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
|||
senderCurve25519Key = result.eventContent["sender_key"] as? String,
|
||||
claimedEd25519Key = crypto.getMyDevice().fingerprint()
|
||||
)
|
||||
localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
|
||||
//TODO
|
||||
//localEchoUpdater.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
|
||||
}
|
||||
|
||||
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)
|
||||
|
|
|
@ -43,9 +43,7 @@ import javax.inject.Inject
|
|||
internal class LocalEchoRepository @Inject constructor(private val sessionDatabase: SessionDatabase,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val eventBus: EventBus,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val roomMemberSummaryDataSource: RoomMemberSummaryDataSource) {
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||
|
||||
suspend fun createLocalEcho(event: Event) {
|
||||
val roomId = event.roomId
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -16,34 +16,18 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
private val monarchy: Monarchy,
|
||||
private val eventBus: EventBus,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
||||
private val timelineEventDataSource: TimelineEventDataSource,
|
||||
private val sqlTimelineFactory: SQLTimeline.Factory
|
||||
) : TimelineService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -52,37 +36,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
|||
}
|
||||
|
||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||
return DefaultTimeline(
|
||||
roomId = roomId,
|
||||
initialEventId = eventId,
|
||||
realmConfiguration = monarchy.realmConfiguration,
|
||||
taskExecutor = taskExecutor,
|
||||
contextOfEventTask = contextOfEventTask,
|
||||
paginationTask = paginationTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
settings = settings,
|
||||
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
eventBus = eventBus,
|
||||
eventDecryptor = eventDecryptor
|
||||
)
|
||||
return sqlTimelineFactory.create(roomId, eventId, settings)
|
||||
}
|
||||
|
||||
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||
return monarchy
|
||||
.fetchCopyMap({
|
||||
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
|
||||
}, { entity, _ ->
|
||||
timelineEventMapper.map(entity)
|
||||
})
|
||||
return timelineEventDataSource.getTimeLineEvent(eventId)
|
||||
}
|
||||
|
||||
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||
val liveData = monarchy.findAllMappedWithChanges(
|
||||
{ TimelineEventEntity.where(it, roomId = roomId, eventId = eventId) },
|
||||
{ timelineEventMapper.map(it) }
|
||||
)
|
||||
return Transformations.map(liveData) { events ->
|
||||
events.firstOrNull().toOptional()
|
||||
}
|
||||
override fun getTimeLineEventLive(eventId: String): Flow<Optional<TimelineEvent>> {
|
||||
return timelineEventDataSource.getTimeLineEventLive(eventId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,11 @@ import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
|||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.internal.crypto.NewSessionListener
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.database.helper.setDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import im.vector.matrix.sqldelight.session.SessionDatabase
|
||||
import okhttp3.internal.tryExecute
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
@ -34,8 +32,7 @@ import javax.inject.Inject
|
|||
|
||||
@SessionScope
|
||||
internal class TimelineEventDecryptor @Inject constructor(
|
||||
@SessionDatabase
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val sessionDatabase: SessionDatabase,
|
||||
private val cryptoService: CryptoService
|
||||
) {
|
||||
|
||||
|
@ -93,31 +90,33 @@ internal class TimelineEventDecryptor @Inject constructor(
|
|||
return
|
||||
}
|
||||
}
|
||||
executor?.execute {
|
||||
Realm.getInstance(realmConfiguration).use { realm ->
|
||||
processDecryptRequest(request, realm)
|
||||
}
|
||||
executor?.tryExecute("process_decrypt_request") {
|
||||
processDecryptRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
|
||||
private fun processDecryptRequest(request: DecryptionRequest) {
|
||||
val eventId = request.eventId
|
||||
val timelineId = request.timelineId
|
||||
Timber.v("Decryption request for event $eventId")
|
||||
val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
|
||||
?: return@executeTransaction Unit.also {
|
||||
Timber.d("Decryption request for unknown message")
|
||||
}
|
||||
val eventEntity = sessionDatabase.eventQueries.select(eventId).executeAsOneOrNull()
|
||||
if (eventEntity == null) {
|
||||
Timber.d("Decryption request for unknown message")
|
||||
synchronized(existingRequests) {
|
||||
existingRequests.remove(request)
|
||||
}
|
||||
return
|
||||
}
|
||||
val event = eventEntity.asDomain()
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, timelineId)
|
||||
Timber.v("Successfully decrypted event $eventId")
|
||||
eventEntity.setDecryptionResult(result)
|
||||
sessionDatabase.eventQueries.setDecryptionResult(result, eventId)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v(e, "Failed to decrypt event $eventId")
|
||||
if (e is MXCryptoError.Base && e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
|
||||
// Keep track of unknown sessions to automatically try to decrypt on new session
|
||||
eventEntity.decryptionErrorCode = e.errorType.name
|
||||
sessionDatabase.eventQueries.setDecryptionError(e.errorType.name, eventId)
|
||||
event.content?.toModel<EncryptedEventContent>()?.let { content ->
|
||||
content.sessionId?.let { sessionId ->
|
||||
synchronized(unknownSessionsFailure) {
|
||||
|
|
|
@ -249,7 +249,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val sessionD
|
|||
val chunks = sessionDatabase.chunkQueriesChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||
val chunksToDelete = ArrayList<ChunkEntity>()
|
||||
chunks.forEach {
|
||||
if (it != currentChunk) {
|
||||
if (it != currentChunk) {s
|
||||
currentChunk.merge(roomId, it, direction)
|
||||
chunksToDelete.add(it)
|
||||
}
|
||||
|
|
|
@ -16,26 +16,17 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionCacheDirectory
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.database.DatabaseKeysUtils
|
||||
import im.vector.matrix.android.internal.di.*
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.SessionModule
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
@ -54,13 +45,10 @@ internal class DefaultSignOutTask @Inject constructor(
|
|||
private val signOutAPI: SignOutAPI,
|
||||
private val sessionManager: SessionManager,
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
|
||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||
@RealmCryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||
@SessionFilesDirectory private val sessionFiles: File,
|
||||
@SessionCacheDirectory private val sessionCache: File,
|
||||
private val realmKeysUtils: RealmKeysUtils,
|
||||
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
|
||||
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
|
||||
private val databaseKeysUtils: DatabaseKeysUtils,
|
||||
@UserMd5 private val userMd5: String,
|
||||
private val eventBus: EventBus
|
||||
) : SignOutTask {
|
||||
|
@ -96,9 +84,6 @@ internal class DefaultSignOutTask @Inject constructor(
|
|||
Timber.d("SignOut: delete session params...")
|
||||
sessionParamsStore.delete(sessionId)
|
||||
|
||||
Timber.d("SignOut: clear session data...")
|
||||
clearSessionDataTask.execute(Unit)
|
||||
|
||||
Timber.d("SignOut: clear crypto data...")
|
||||
clearCryptoDataTask.execute(Unit)
|
||||
|
||||
|
@ -107,17 +92,7 @@ internal class DefaultSignOutTask @Inject constructor(
|
|||
sessionCache.deleteRecursively()
|
||||
|
||||
Timber.d("SignOut: clear the database keys")
|
||||
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
|
||||
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
|
||||
|
||||
// Sanity check
|
||||
if (BuildConfig.DEBUG) {
|
||||
Realm.getGlobalInstanceCount(realmSessionConfiguration)
|
||||
.takeIf { it > 0 }
|
||||
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
|
||||
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
|
||||
.takeIf { it > 0 }
|
||||
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
|
||||
}
|
||||
databaseKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
|
||||
databaseKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,16 @@ internal class Debouncer(private val handler: Handler) {
|
|||
|
||||
private val runnables = HashMap<String, Runnable>()
|
||||
|
||||
fun debounce(identifier: String, r: Runnable, millis: Long): Boolean {
|
||||
// debounce
|
||||
runnables[identifier]?.let { runnable -> handler.removeCallbacks(runnable) }
|
||||
fun cancelAll() {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
fun debounce(identifier: String, r: Runnable, millis: Long): Boolean {
|
||||
if (runnables.containsKey(identifier)) {
|
||||
// debounce
|
||||
val old = runnables[identifier]
|
||||
handler.removeCallbacks(old)
|
||||
}
|
||||
insertRunnable(identifier, r, millis)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -271,6 +271,7 @@ dependencies {
|
|||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
|
||||
|
||||
implementation "org.threeten:threetenbp:1.4.0:no-tzdb"
|
||||
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
|
||||
|
|
|
@ -20,8 +20,8 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import arrow.core.Option
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.roomSummaryQueryParams
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.features.grouplist.ALL_COMMUNITIES_GROUP_ID
|
||||
|
@ -29,11 +29,9 @@ import im.vector.riotx.features.grouplist.SelectedGroupDataSource
|
|||
import im.vector.riotx.features.home.HomeRoomListDataSource
|
||||
import im.vector.riotx.features.home.room.list.ChronologicalRoomComparator
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -62,32 +60,30 @@ class AppStateHandler @Inject constructor(
|
|||
|
||||
private fun observeRoomsAndGroup() {
|
||||
Observable
|
||||
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
|
||||
sessionDataSource.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.switchMap {
|
||||
val query = roomSummaryQueryParams {}
|
||||
it.orNull()?.rx()?.liveRoomSummaries(query)
|
||||
?: Observable.just(emptyList())
|
||||
}
|
||||
.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
.combineLatest<Option<Session>, Option<GroupSummary>, Pair<Option<Session>, Option<GroupSummary>>>(
|
||||
sessionDataSource.observe(),
|
||||
selectedGroupDataSource.observe(),
|
||||
BiFunction { rooms, selectedGroupOption ->
|
||||
val selectedGroup = selectedGroupOption.orNull()
|
||||
val filteredRooms = rooms.filter {
|
||||
if (selectedGroup == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
|
||||
true
|
||||
} else if (it.isDirect) {
|
||||
it.otherMemberIds
|
||||
.intersect(selectedGroup.userIds)
|
||||
.isNotEmpty()
|
||||
} else {
|
||||
selectedGroup.roomIds.contains(it.roomId)
|
||||
}
|
||||
}
|
||||
filteredRooms.sortedWith(chronologicalRoomComparator)
|
||||
BiFunction { sessionOption, selectedGroupOption ->
|
||||
Pair(sessionOption, selectedGroupOption)
|
||||
}
|
||||
)
|
||||
).switchMap {
|
||||
val selectedGroup = it.second.orNull()
|
||||
val session = it.first.orNull()
|
||||
val queryParams = if (selectedGroup?.groupId == null || selectedGroup.groupId == ALL_COMMUNITIES_GROUP_ID) {
|
||||
roomSummaryQueryParams()
|
||||
} else {
|
||||
roomSummaryQueryParams {
|
||||
fromGroupId = selectedGroup.groupId
|
||||
}
|
||||
}
|
||||
session
|
||||
?.rx()
|
||||
?.liveRoomSummaries(queryParams)
|
||||
?: Observable.empty()
|
||||
}
|
||||
.map {
|
||||
it.sortedWith(chronologicalRoomComparator)
|
||||
}
|
||||
.subscribe {
|
||||
homeRoomListDataSource.post(it)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.home
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.asLiveData
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.toMatrixItem
|
||||
import im.vector.riotx.R
|
||||
|
@ -40,7 +41,7 @@ class HomeDrawerFragment @Inject constructor(
|
|||
if (savedInstanceState == null) {
|
||||
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
|
||||
}
|
||||
session.getUserLive(session.myUserId).observeK(viewLifecycleOwner) { optionalUser ->
|
||||
session.getUserLive(session.myUserId).asLiveData().observeK(viewLifecycleOwner) { optionalUser ->
|
||||
val user = optionalUser?.getOrNull()
|
||||
if (user != null) {
|
||||
avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)
|
||||
|
|
|
@ -19,8 +19,8 @@ package im.vector.riotx.features.home.room.breadcrumbs
|
|||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.Breadcrumb
|
||||
|
||||
data class BreadcrumbsViewState(
|
||||
val asyncBreadcrumbs: Async<List<RoomSummary>> = Uninitialized
|
||||
val asyncBreadcrumbs: Async<List<Breadcrumb>> = Uninitialized
|
||||
) : MvRxState
|
||||
|
|
|
@ -154,7 +154,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
.liveAnnotationSummary(eventId)
|
||||
.map { annotations ->
|
||||
EmojiDataSource.quickEmojis.map { emoji ->
|
||||
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
ToggleState(emoji, annotations.reactionsSummary.firstOrNull { it.key == emoji }?.addedByMe ?: false)
|
||||
}
|
||||
}
|
||||
.execute {
|
||||
|
|
|
@ -88,7 +88,6 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
|
|||
private fun observeEventAnnotationSummaries() {
|
||||
RxRoom(room)
|
||||
.liveAnnotationSummary(eventId)
|
||||
.unwrap()
|
||||
.flatMapSingle { summaries ->
|
||||
Observable
|
||||
.fromIterable(summaries.reactionsSummary)
|
||||
|
|
Loading…
Reference in New Issue