Merge pull request #959 from vector-im/feature/roomshields_perf

Refactor Room Shield / Profile shield
This commit is contained in:
Valere 2020-02-06 16:54:17 +01:00 committed by GitHub
commit 67bc100782
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 498 additions and 259 deletions

View file

@ -58,7 +58,7 @@ private class LiveDataObservable<T>(
} }
} }
internal fun <T> LiveData<T>.asObservable(): Observable<T> { fun <T> LiveData<T>.asObservable(): Observable<T> {
return LiveDataObservable(this).observeOn(Schedulers.computation()) return LiveDataObservable(this).observeOn(Schedulers.computation())
} }

View file

@ -16,8 +16,6 @@
package im.vector.matrix.rx package im.vector.matrix.rx
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
@ -30,103 +28,22 @@ import im.vector.matrix.android.api.session.room.send.UserDraft
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxRoom(private val room: Room, private val session: Session) { class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<Optional<RoomSummary>> { fun liveRoomSummary(): Observable<Optional<RoomSummary>> {
val summaryObservable = room.getRoomSummaryLive() return room.getRoomSummaryLive()
.asObservable() .asObservable()
.startWithCallable { .startWithCallable { room.roomSummary().toOptional() }
room.roomSummary().toOptional()
}
.doOnNext { Timber.v("RX: summary emitted for: ${it.getOrNull()?.roomId}") }
val memberIdsChangeObservable = summaryObservable
.map {
it.getOrNull()?.let { roomSummary ->
if (roomSummary.isEncrypted) {
// Return the list of other users
roomSummary.otherMemberIds + listOf(session.myUserId)
} else {
// Return an empty list, the room is not encrypted
emptyList()
}
}.orEmpty()
}.distinctUntilChanged()
.doOnNext { Timber.v("RX: memberIds emitted. Size: ${it.size}") }
// Observe the device info of the users in the room
val cryptoDeviceInfoObservable = memberIdsChangeObservable
.switchMap { membersIds ->
session.getLiveCryptoDeviceInfo(membersIds)
.asObservable()
.map {
// If any key change, emit the userIds list
membersIds
}
.startWith(membersIds)
.doOnNext { Timber.v("RX: CryptoDeviceInfo emitted. Size: ${it.size}") }
}
.doOnNext { Timber.v("RX: cryptoDeviceInfo emitted 2. Size: ${it.size}") }
val roomEncryptionTrustLevelObservable = cryptoDeviceInfoObservable
.map { userIds ->
if (userIds.isEmpty()) {
Optional<RoomEncryptionTrustLevel>(null)
} else {
session.getCrossSigningService().getTrustLevelForUsers(userIds).toOptional()
}
}
.doOnNext { Timber.v("RX: roomEncryptionTrustLevel emitted: ${it.getOrNull()?.name}") }
return Observable
.combineLatest<Optional<RoomSummary>, Optional<RoomEncryptionTrustLevel>, Optional<RoomSummary>>(
summaryObservable,
roomEncryptionTrustLevelObservable,
BiFunction { summary, level ->
summary.getOrNull()?.copy(
roomEncryptionTrustLevel = level.getOrNull()
).toOptional()
}
)
.doOnNext { Timber.v("RX: final room summary emitted for ${it.getOrNull()?.roomId}") }
} }
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> { fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable() return room.getRoomMembersLive(queryParams).asObservable()
.startWithCallable { .startWithCallable {
room.getRoomMembers(queryParams) room.getRoomMembers(queryParams)
} }
.doOnNext { Timber.v("RX: room members emitted. Size: ${it.size}") }
// TODO Do it only for room members of the room (switchMap)
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.v("RX: cryptoDeviceInfo emitted. Size: ${it.size}") }
return Observable
.combineLatest<List<RoomMemberSummary>, List<CryptoDeviceInfo>, List<RoomMemberSummary>>(
roomMembersObservable,
cryptoDeviceInfoObservable,
BiFunction { summaries, _ ->
summaries.map {
if (room.isEncrypted()) {
it.copy(
// Get the trust level of a virtual room with only this user
userEncryptionTrustLevel = session.getCrossSigningService().getTrustLevelForUsers(listOf(it.userId))
)
} else {
it
}
}
}
)
.doOnNext { Timber.v("RX: final room members emitted. Size: ${it.size}") }
} }
fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> { fun liveAnnotationSummary(eventId: String): Observable<Optional<EventAnnotationsSummary>> {
@ -180,6 +97,6 @@ class RxRoom(private val room: Room, private val session: Session) {
} }
} }
fun Room.rx(session: Session): RxRoom { fun Room.rx(): RxRoom {
return RxRoom(this, session) return RxRoom(this)
} }

View file

@ -33,40 +33,14 @@ import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxSession(private val session: Session) { class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> { fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable() return session.getRoomSummariesLive(queryParams).asObservable()
.startWithCallable { .startWithCallable {
session.getRoomSummaries(queryParams) session.getRoomSummaries(queryParams)
} }
.doOnNext { Timber.v("RX: summaries emitted: size: ${it.size}") }
val cryptoDeviceInfoObservable = session.getLiveCryptoDeviceInfo().asObservable()
.startWith(emptyList<CryptoDeviceInfo>())
.doOnNext { Timber.v("RX: crypto device info emitted: size: ${it.size}") }
return Observable
.combineLatest<List<RoomSummary>, List<CryptoDeviceInfo>, List<RoomSummary>>(
summariesObservable,
cryptoDeviceInfoObservable,
BiFunction { summaries, _ ->
summaries.map {
if (it.isEncrypted) {
it.copy(
roomEncryptionTrustLevel = session.getCrossSigningService()
.getTrustLevelForUsers(it.otherMemberIds + session.myUserId)
)
} else {
it
}
}
}
)
.doOnNext { Timber.d("RX: final summaries emitted: size: ${it.size}") }
} }
fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> { fun liveGroupSummaries(queryParams: GroupSummaryQueryParams): Observable<List<GroupSummary>> {
@ -136,12 +110,16 @@ class RxSession(private val session: Session) {
} }
fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> { fun liveUserCryptoDevices(userId: String): Observable<List<CryptoDeviceInfo>> {
return session.getLiveCryptoDeviceInfo(userId).asObservable() return session.getLiveCryptoDeviceInfo(userId).asObservable().startWithCallable {
session.getCryptoDeviceInfo(userId)
}
} }
fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> { fun liveCrossSigningInfo(userId: String): Observable<Optional<MXCrossSigningInfo>> {
return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable() return session.getCrossSigningService().getLiveCrossSigningKeys(userId).asObservable()
.startWith(session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional()) .startWithCallable {
session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional()
}
} }
} }

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.session.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
@ -63,6 +62,4 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String, fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String, otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult locallyTrusted: Boolean?): DeviceTrustResult
fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel
} }

View file

@ -16,8 +16,6 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
/** /**
* Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
*/ */
@ -25,7 +23,5 @@ data class RoomMemberSummary constructor(
val membership: Membership, val membership: Membership,
val userId: String, val userId: String,
val displayName: String? = null, val displayName: String? = null,
val avatarUrl: String? = null, val avatarUrl: String? = null
// TODO Warning: Will not be populated if not using RxRoom
val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) )

View file

@ -19,10 +19,11 @@ package im.vector.matrix.android.internal.crypto
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.internal.crypto.api.CryptoApi import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.crosssigning.ComputeTrustTask
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultComputeTrustTask
import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService import im.vector.matrix.android.internal.crypto.crosssigning.DefaultCrossSigningService
import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
@ -137,15 +138,6 @@ internal abstract class CryptoModule {
return RealmClearCacheTask(realmConfiguration) return RealmClearCacheTask(realmConfiguration)
} }
@JvmStatic
@Provides
fun providesCryptoStore(@CryptoDatabase
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
return RealmCryptoStore(
realmConfiguration,
credentials)
}
@JvmStatic @JvmStatic
@Provides @Provides
@SessionScope @SessionScope
@ -249,4 +241,10 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService
@Binds
abstract fun bindCryptoStore(realmCryptoStore: RealmCryptoStore): IMXCryptoStore
@Binds
abstract fun bindComputeShieldTrustTask(defaultShieldTrustUpdater: DefaultComputeTrustTask): ComputeTrustTask
} }

View file

@ -27,6 +27,9 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -36,10 +39,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val olmDevice: MXOlmDevice, private val olmDevice: MXOlmDevice,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials, private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) { private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener { interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(users: List<String>) fun onUsersDeviceUpdate(userIds: List<String>)
} }
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>() private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
@ -72,6 +77,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val notReadyToRetryHS = mutableSetOf<String>() private val notReadyToRetryHS = mutableSetOf<String>()
init { init {
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
var isUpdated = false var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap() val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) { for ((userId, status) in deviceTrackingStatuses) {
@ -85,6 +91,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
} }
} }
}
/** /**
* Tells if the key downloads should be tried * Tells if the key downloads should be tried

View file

@ -0,0 +1,78 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.crosssigning
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
data class Params(
val userIds: List<String>
)
}
internal class DefaultComputeTrustTask @Inject constructor(
val cryptoStore: IMXCryptoStore
) : ComputeTrustTask {
override suspend fun execute(params: ComputeTrustTask.Params): RoomEncryptionTrustLevel {
val allTrustedUserIds = params.userIds
.filter { userId -> getUserCrossSigningKeys(userId)?.isTrusted() == true }
return if (allTrustedUserIds.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// If all devices of all verified users are trusted -> green
// else -> black
allTrustedUserIds
.mapNotNull { cryptoStore.getUserDeviceList(it) }
.flatten()
.let { allDevices ->
if (getMyCrossSigningKeys() != null) {
allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
} else {
// Legacy method
allDevices.any { !it.isVerified }
}
}
.let { hasWarning ->
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (params.userIds.size == allTrustedUserIds.size) {
// all users are trusted and all devices are verified
RoomEncryptionTrustLevel.Trusted
} else {
RoomEncryptionTrustLevel.Default
}
}
}
}
}
private fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? {
return cryptoStore.getCrossSigningInfo(otherUserId)
}
private fun getMyCrossSigningKeys(): MXCrossSigningInfo? {
return cryptoStore.getMyCrossSigningInfo()
}
}

View file

@ -19,8 +19,6 @@ package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
@ -37,11 +35,14 @@ import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
import timber.log.Timber import timber.log.Timber
@ -56,9 +57,11 @@ internal class DefaultCrossSigningService @Inject constructor(
private val deviceListManager: DeviceListManager, private val deviceListManager: DeviceListManager,
private val uploadSigningKeysTask: UploadSigningKeysTask, private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask, private val uploadSignaturesTask: UploadSignaturesTask,
private val cryptoCoroutineScope: CoroutineScope, private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private val cryptoCoroutineScope: CoroutineScope,
private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
@ -210,6 +213,7 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding()) cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
uploadSigningKeysTask.configureWith(params) { uploadSigningKeysTask.configureWith(params) {
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - Keys successfully uploaded") Timber.i("## CrossSigning - Keys successfully uploaded")
@ -245,6 +249,7 @@ internal class DefaultCrossSigningService @Inject constructor(
resetTrustOnKeyChange() resetTrustOnKeyChange()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
// this.retryCount = 3 // this.retryCount = 3
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - signatures successfully uploaded") Timber.i("## CrossSigning - signatures successfully uploaded")
@ -497,6 +502,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature)) .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
.build() .build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
@ -543,6 +549,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withDeviceInfo(toUpload) .withDeviceInfo(toUpload)
.build() .build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) { uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
}.executeBy(taskExecutor) }.executeBy(taskExecutor)
} }
@ -608,10 +615,10 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
override fun onUsersDeviceUpdate(users: List<String>) { override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users") cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
users.forEach { otherUserId -> Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
userIds.forEach { otherUserId ->
checkUserTrust(otherUserId).let { checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}") Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified()) setUserKeysAsTrusted(otherUserId, it.isVerified())
@ -630,13 +637,16 @@ internal class DefaultCrossSigningService @Inject constructor(
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
} }
eventBus.post(CryptoToSessionUserTrustChange(userIds))
}
} }
} }
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted) cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices? // If it's me, recheck trust of all users and devices?
val users = ArrayList<String>() val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) { if (otherUserId == userId && currentTrust != trusted) {
@ -654,37 +664,5 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
} }
override fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel {
val allTrusted = userIds
.filter { getUserCrossSigningKeys(it)?.isTrusted() == true }
val allUsersAreVerified = userIds.size == allTrusted.size
return if (allTrusted.isEmpty()) {
RoomEncryptionTrustLevel.Default
} else {
// If one of the verified user as an untrusted device -> warning
// Green if all devices of all verified users are trusted -> green
// else black
val allDevices = allTrusted.mapNotNull {
cryptoStore.getUserDeviceList(it)
}.flatten()
if (getMyCrossSigningKeys() != null) {
val hasWarning = allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
if (hasWarning) {
RoomEncryptionTrustLevel.Warning
} else {
if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
}
} else {
val hasWarningLegacy = allDevices.any { !it.isVerified }
if (hasWarningLegacy) {
RoomEncryptionTrustLevel.Warning
} else {
if (allUsersAreVerified) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Default
}
}
}
} }
} }

View file

@ -0,0 +1,25 @@
/*
* 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.crypto.crosssigning
data class SessionToCryptoRoomMembersUpdate(
val roomId: String,
val userIds: List<String>
)
data class CryptoToSessionUserTrustChange(
val userIds: List<String>
)

View file

@ -0,0 +1,132 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.crosssigning
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomMemberSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.createBackgroundHandler
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
internal class ShieldTrustUpdater @Inject constructor(
private val eventBus: EventBus,
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
@CryptoDatabase private val cryptoRealmConfiguration: RealmConfiguration,
@SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
private val roomSummaryUpdater: RoomSummaryUpdater
) {
companion object {
private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
}
private val backgroundCryptoRealm = AtomicReference<Realm>()
private val backgroundSessionRealm = AtomicReference<Realm>()
// private var cryptoDevicesResult: RealmResults<DeviceInfoEntity>? = null
// private val cryptoDeviceChangeListener = object : OrderedRealmCollectionChangeListener<RealmResults<DeviceInfoEntity>> {
// override fun onChange(t: RealmResults<DeviceInfoEntity>, changeSet: OrderedCollectionChangeSet) {
// val grouped = t.groupBy { it.userId }
// onCryptoDevicesChange(grouped.keys.mapNotNull { it })
// }
// }
fun start() {
eventBus.register(this)
BACKGROUND_HANDLER.post {
val cryptoRealm = Realm.getInstance(cryptoRealmConfiguration)
backgroundCryptoRealm.set(cryptoRealm)
// cryptoDevicesResult = cryptoRealm.where<DeviceInfoEntity>().findAll()
// cryptoDevicesResult?.addChangeListener(cryptoDeviceChangeListener)
backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
}
}
fun stop() {
eventBus.unregister(this)
BACKGROUND_HANDLER.post {
// cryptoDevicesResult?.removeAllChangeListeners()
backgroundCryptoRealm.getAndSet(null).also {
it?.close()
}
backgroundSessionRealm.getAndSet(null).also {
it?.close()
}
}
}
@Subscribe
fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
taskExecutor.executorScope.launch {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds))
// We need to send that back to session base
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get().executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
}
}
}
}
@Subscribe
fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
onCryptoDevicesChange(update.userIds)
}
private fun onCryptoDevicesChange(users: List<String>) {
BACKGROUND_HANDLER.post {
val impactedRoomsId = backgroundSessionRealm.get().where(RoomMemberSummaryEntity::class.java)
.`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
.findAll()
.map { it.roomId }
.distinct()
val map = HashMap<String, List<String>>()
impactedRoomsId.forEach { roomId ->
RoomMemberSummaryEntity.where(backgroundSessionRealm.get(), roomId).findAll()?.let { results ->
map[roomId] = results.map { it.userId }
}
}
map.forEach { entry ->
val roomId = entry.key
val userList = entry.value
taskExecutor.executorScope.launch {
val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(userList))
BACKGROUND_HANDLER.post {
backgroundSessionRealm.get().executeTransaction { realm ->
roomSummaryUpdater.updateShieldTrust(realm, roomId, updatedTrust)
}
}
}
}
}
}
}

View file

@ -61,6 +61,7 @@ import im.vector.matrix.android.internal.crypto.store.db.query.delete
import im.vector.matrix.android.internal.crypto.store.db.query.get import im.vector.matrix.android.internal.crypto.store.db.query.get
import im.vector.matrix.android.internal.crypto.store.db.query.getById import im.vector.matrix.android.internal.crypto.store.db.query.getById
import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -70,10 +71,12 @@ import io.realm.kotlin.where
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.set import kotlin.collections.set
@SessionScope @SessionScope
internal class RealmCryptoStore(private val realmConfiguration: RealmConfiguration, internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore { private val credentials: Credentials) : IMXCryptoStore {
/* ========================================================================================== /* ==========================================================================================
@ -403,14 +406,14 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
{ realm: Realm -> { realm: Realm ->
realm realm
.where<UserEntity>() .where<UserEntity>()
.`in`(UserEntityFields.USER_ID, userIds.toTypedArray()) .`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray())
}, },
{ entity -> { entity ->
entity.devices.map { CryptoMapper.mapToModel(it) } entity.devices.map { CryptoMapper.mapToModel(it) }
} }
) )
return Transformations.map(liveData) { return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList() it.flatten()
} }
} }

View file

@ -26,8 +26,7 @@ internal object RoomMemberSummaryMapper {
userId = roomMemberSummaryEntity.userId, userId = roomMemberSummaryEntity.userId,
avatarUrl = roomMemberSummaryEntity.avatarUrl, avatarUrl = roomMemberSummaryEntity.avatarUrl,
displayName = roomMemberSummaryEntity.displayName, displayName = roomMemberSummaryEntity.displayName,
membership = roomMemberSummaryEntity.membership, membership = roomMemberSummaryEntity.membership
userEncryptionTrustLevel = null
) )
} }
} }

View file

@ -17,11 +17,11 @@
package im.vector.matrix.android.internal.database.mapper package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -49,7 +49,8 @@ internal class RoomSummaryMapper @Inject constructor(
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
) )
} catch (e: MXCryptoError) { } catch (e: Throwable) {
Timber.d(e)
} }
} }
@ -75,7 +76,8 @@ internal class RoomSummaryMapper @Inject constructor(
aliases = roomSummaryEntity.aliases.toList(), aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted, isEncrypted = roomSummaryEntity.isEncrypted,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel
) )
} }
} }

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.database.model package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
@ -47,7 +48,8 @@ internal open class RoomSummaryEntity(
// this is required for querying // this is required for querying
var flatAliases: String = "", var flatAliases: String = "",
var isEncrypted: Boolean = false, var isEncrypted: Boolean = false,
var typingUserIds: RealmList<String> = RealmList() var typingUserIds: RealmList<String> = RealmList(),
var roomEncryptionTrustLevelStr: String? = null
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
@ -68,5 +70,19 @@ internal open class RoomSummaryEntity(
versioningStateStr = value.name versioningStateStr = value.name
} }
var roomEncryptionTrustLevel: RoomEncryptionTrustLevel?
get() {
return roomEncryptionTrustLevelStr?.let {
try {
RoomEncryptionTrustLevel.valueOf(it)
} catch (failure: Throwable) {
null
}
}
}
set(value) {
roomEncryptionTrustLevelStr = value?.name
}
companion object companion object
} }

View file

@ -43,6 +43,7 @@ import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.crosssigning.ShieldTrustUpdater
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.di.WorkManagerProvider
@ -89,7 +90,8 @@ internal class DefaultSession @Inject constructor(
private val sessionParamsStore: SessionParamsStore, private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>) private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session, : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
@ -119,6 +121,7 @@ internal class DefaultSession @Inject constructor(
isOpen = true isOpen = true
liveEntityObservers.forEach { it.start() } liveEntityObservers.forEach { it.start() }
eventBus.register(this) eventBus.register(this)
shieldTrustUpdater.start()
} }
override fun requireBackgroundSync() { override fun requireBackgroundSync() {
@ -160,6 +163,7 @@ internal class DefaultSession @Inject constructor(
isOpen = false isOpen = false
eventBus.unregister(this) eventBus.unregister(this)
syncTaskSequencer.close() syncTaskSequencer.close()
shieldTrustUpdater.stop()
} }
override fun getSyncStateLive(): LiveData<SyncState> { override fun getSyncStateLive(): LiveData<SyncState> {

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
@ -24,6 +25,7 @@ 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.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -43,12 +45,14 @@ import im.vector.matrix.android.internal.session.sync.RoomSyncHandler
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor( internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver, private val roomAvatarResolver: RoomAvatarResolver,
private val eventBus: EventBus,
private val monarchy: Monarchy) { private val monarchy: Monarchy) {
companion object { companion object {
@ -139,6 +143,18 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) {
eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.otherMemberIds.toList() + userId))
}
}
}
fun updateShieldTrust(realm: Realm,
roomId: String,
trust: RoomEncryptionTrustLevel?) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummaryEntity.isEncrypted) {
roomSummaryEntity.roomEncryptionTrustLevel = trust
} }
} }
} }

View file

@ -38,12 +38,19 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
lateinit var title: String lateinit var title: String
@EpoxyAttribute @EpoxyAttribute
var subtitle: String? = null var subtitle: String? = null
@EpoxyAttribute @EpoxyAttribute
var iconRes: Int = 0 var iconRes: Int = 0
@EpoxyAttribute
var tintIcon: Boolean = true
@EpoxyAttribute @EpoxyAttribute
var editableRes: Int = R.drawable.ic_arrow_right var editableRes: Int = R.drawable.ic_arrow_right
@EpoxyAttribute
var accessoryRes: Int = 0
@EpoxyAttribute @EpoxyAttribute
var editable: Boolean = true var editable: Boolean = true
@ -70,12 +77,23 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
holder.subtitle.setTextOrHide(subtitle) holder.subtitle.setTextOrHide(subtitle)
if (iconRes != 0) { if (iconRes != 0) {
holder.icon.setImageResource(iconRes) holder.icon.setImageResource(iconRes)
if (tintIcon) {
ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(tintColor)) ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(tintColor))
} else {
ImageViewCompat.setImageTintList(holder.icon, null)
}
holder.icon.isVisible = true holder.icon.isVisible = true
} else { } else {
holder.icon.isVisible = false holder.icon.isVisible = false
} }
if (accessoryRes != 0) {
holder.secondaryAccessory.setImageResource(accessoryRes)
holder.secondaryAccessory.isVisible = true
} else {
holder.secondaryAccessory.isVisible = false
}
if (editableRes != 0) { if (editableRes != 0) {
val tintColorSecondary = if (destructive) { val tintColorSecondary = if (destructive) {
tintColor tintColor
@ -95,5 +113,6 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
val title by bind<TextView>(R.id.actionTitle) val title by bind<TextView>(R.id.actionTitle)
val subtitle by bind<TextView>(R.id.actionSubtitle) val subtitle by bind<TextView>(R.id.actionSubtitle)
val editable by bind<ImageView>(R.id.actionEditable) val editable by bind<ImageView>(R.id.actionEditable)
val secondaryAccessory by bind<ImageView>(R.id.actionSecondaryAccessory)
} }
} }

View file

@ -36,19 +36,23 @@ fun EpoxyController.buildProfileAction(
subtitle: String? = null, subtitle: String? = null,
editable: Boolean = true, editable: Boolean = true,
@DrawableRes icon: Int = 0, @DrawableRes icon: Int = 0,
tintIcon: Boolean = true,
@DrawableRes editableRes: Int? = null, @DrawableRes editableRes: Int? = null,
destructive: Boolean = false, destructive: Boolean = false,
divider: Boolean = true, divider: Boolean = true,
action: ClickListener? = null action: ClickListener? = null,
@DrawableRes accessory: Int = 0
) { ) {
profileActionItem { profileActionItem {
iconRes(icon) iconRes(icon)
tintIcon(tintIcon)
id("action_$id") id("action_$id")
subtitle(subtitle) subtitle(subtitle)
editable(editable) editable(editable)
editableRes?.let { editableRes(editableRes) } editableRes?.let { editableRes(editableRes) }
destructive(destructive) destructive(destructive)
title(title) title(title)
accessoryRes(accessory)
listener { _ -> listener { _ ->
action?.invoke() action?.invoke()
} }

View file

@ -31,7 +31,8 @@ class RxConfig @Inject constructor(
fun setupRxPlugin() { fun setupRxPlugin() {
RxJavaPlugins.setErrorHandler { throwable -> RxJavaPlugins.setErrorHandler { throwable ->
Timber.e(throwable, "RxError") Timber.e(throwable, "RxError")
// is InterruptedException -> fine, some blocking code was interrupted by a dispose call
if (throwable !is InterruptedException) {
// Avoid crash in production, except if user wants it // Avoid crash in production, except if user wants it
if (vectorPreferences.failFast()) { if (vectorPreferences.failFast()) {
throw throwable throw throwable
@ -39,3 +40,4 @@ class RxConfig @Inject constructor(
} }
} }
} }
}

View file

@ -159,7 +159,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
observeMyRoomMember() observeMyRoomMember()
room.getRoomSummaryLive() room.getRoomSummaryLive()
room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback())
room.rx(session).loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId) session.onRoomDisplayed(initialState.roomId)
} }
@ -168,7 +168,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val queryParams = roomMemberQueryParams { val queryParams = roomMemberQueryParams {
this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE) this.userId = QueryStringValue.Equals(session.myUserId, QueryStringValue.Case.SENSITIVE)
} }
room.rx(session) room.rx()
.liveRoomMembers(queryParams) .liveRoomMembers(queryParams)
.map { .map {
it.firstOrNull().toOptional() it.firstOrNull().toOptional()
@ -255,7 +255,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun observeDrafts() { private fun observeDrafts() {
room.rx(session).liveDrafts() room.rx().liveDrafts()
.subscribe { .subscribe {
Timber.d("Draft update --> SetState") Timber.d("Draft update --> SetState")
setState { setState {
@ -896,7 +896,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx(session).liveRoomSummary() room.rx().liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
val typingRoomMembers = val typingRoomMembers =
@ -914,7 +914,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
Observable Observable
.combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>( .combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(
timelineEvents.observeOn(Schedulers.computation()), timelineEvents.observeOn(Schedulers.computation()),
room.rx(session).liveRoomSummary().unwrap(), room.rx().liveRoomSummary().unwrap(),
BiFunction { timelineEvents, roomSummary -> BiFunction { timelineEvents, roomSummary ->
computeUnreadState(timelineEvents, roomSummary) computeUnreadState(timelineEvents, roomSummary)
} }

View file

@ -134,7 +134,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeEvent() { private fun observeEvent() {
if (room == null) return if (room == null) return
room.rx(session) room.rx()
.liveTimelineEvent(eventId) .liveTimelineEvent(eventId)
.unwrap() .unwrap()
.execute { .execute {
@ -144,7 +144,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private fun observeReactions() { private fun observeReactions() {
if (room == null) return if (room == null) return
room.rx(session) room.rx()
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
EmojiDataSource.quickEmojis.map { emoji -> EmojiDataSource.quickEmojis.map { emoji ->

View file

@ -86,7 +86,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
} }
private fun observeEventAnnotationSummaries() { private fun observeEventAnnotationSummaries() {
RxRoom(room, session) RxRoom(room)
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.unwrap() .unwrap()
.flatMapSingle { summaries -> .flatMapSingle { summaries ->

View file

@ -54,7 +54,7 @@ class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initia
private fun observeNotificationState() { private fun observeNotificationState() {
room room
.rx(session) .rx()
.liveNotificationState() .liveNotificationState()
.execute { .execute {
copy(roomNotificationState = it) copy(roomNotificationState = it)
@ -63,7 +63,7 @@ class RoomListQuickActionsViewModel @AssistedInject constructor(@Assisted initia
private fun observeRoomSummary() { private fun observeRoomSummary() {
room room
.rx(session) .rx()
.liveRoomSummary() .liveRoomSummary()
.unwrap() .unwrap()
.execute { .execute {

View file

@ -97,6 +97,7 @@ class RoomMemberProfileController @Inject constructor(
dividerColor = dividerColor, dividerColor = dividerColor,
editable = true, editable = true,
icon = icon, icon = icon,
tintIcon = false,
divider = false, divider = false,
action = { callback?.onShowDeviceList() } action = { callback?.onShowDeviceList() }
) )

View file

@ -170,7 +170,7 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
val queryParams = roomMemberQueryParams { val queryParams = roomMemberQueryParams {
this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE) this.userId = QueryStringValue.Equals(initialState.userId, QueryStringValue.Case.SENSITIVE)
} }
room.rx(session).liveRoomMembers(queryParams) room.rx().liveRoomMembers(queryParams)
.map { it.firstOrNull()?.toMatrixItem().toOptional() } .map { it.firstOrNull()?.toMatrixItem().toOptional() }
.unwrap() .unwrap()
.execute { .execute {
@ -193,8 +193,8 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
} }
private fun observeRoomSummaryAndPowerLevels(room: Room) { private fun observeRoomSummaryAndPowerLevels(room: Room) {
val roomSummaryLive = room.rx(session).liveRoomSummary().unwrap() val roomSummaryLive = room.rx().liveRoomSummary().unwrap()
val powerLevelsContentLive = room.rx(session).liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") val powerLevelsContentLive = room.rx().liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
.mapOptional { it.content.toModel<PowerLevelsContent>() } .mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap() .unwrap()

View file

@ -18,6 +18,7 @@
package im.vector.riotx.features.roomprofile package im.vector.riotx.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.profiles.buildProfileAction import im.vector.riotx.core.epoxy.profiles.buildProfileAction
import im.vector.riotx.core.epoxy.profiles.buildProfileSection import im.vector.riotx.core.epoxy.profiles.buildProfileSection
@ -79,11 +80,13 @@ class RoomProfileController @Inject constructor(
action = { callback?.onNotificationsClicked() } action = { callback?.onNotificationsClicked() }
) )
val numberOfMembers = roomSummary.joinedMembersCount ?: 0 val numberOfMembers = roomSummary.joinedMembersCount ?: 0
val hasWarning = roomSummary.isEncrypted && roomSummary.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Warning
buildProfileAction( buildProfileAction(
id = "member_list", id = "member_list",
title = stringProvider.getQuantityString(R.plurals.room_profile_section_more_member_list, numberOfMembers, numberOfMembers), title = stringProvider.getQuantityString(R.plurals.room_profile_section_more_member_list, numberOfMembers, numberOfMembers),
dividerColor = dividerColor, dividerColor = dividerColor,
icon = R.drawable.ic_room_profile_member_list, icon = R.drawable.ic_room_profile_member_list,
accessory = R.drawable.ic_shield_warning.takeIf { hasWarning } ?: 0,
action = { callback?.onMemberListClicked() } action = { callback?.onMemberListClicked() }
) )
buildProfileAction( buildProfileAction(

View file

@ -56,7 +56,7 @@ class RoomProfileViewModel @AssistedInject constructor(@Assisted initialState: R
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx(session).liveRoomSummary() room.rx().liveRoomSummary()
.unwrap() .unwrap()
.execute { .execute {
copy(roomSummary = it) copy(roomSummary = it)

View file

@ -62,7 +62,7 @@ class RoomMemberListController @Inject constructor(
id(roomMember.userId) id(roomMember.userId)
matrixItem(roomMember.toMatrixItem()) matrixItem(roomMember.toMatrixItem())
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
userEncryptionTrustLevel(roomMember.userEncryptionTrustLevel) userEncryptionTrustLevel(data.trustLevelMap.invoke()?.get(roomMember.userId))
clickListener { _ -> clickListener { _ ->
callback?.onRoomMemberClicked(roomMember) callback?.onRoomMemberClicked(roomMember)
} }

View file

@ -21,6 +21,8 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -31,13 +33,16 @@ import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsConstants
import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper import im.vector.matrix.android.api.session.room.powerlevels.PowerLevelsHelper
import im.vector.matrix.rx.asObservable
import im.vector.matrix.rx.mapOptional import im.vector.matrix.rx.mapOptional
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.EmptyViewEvents import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import timber.log.Timber
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState, class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
private val roomMemberSummaryComparator: RoomMemberSummaryComparator, private val roomMemberSummaryComparator: RoomMemberSummaryComparator,
@ -70,10 +75,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
displayName = QueryStringValue.IsNotEmpty displayName = QueryStringValue.IsNotEmpty
memberships = Membership.activeMemberships() memberships = Membership.activeMemberships()
} }
Observable Observable
.combineLatest<List<RoomMemberSummary>, PowerLevelsContent, RoomMemberSummaries>( .combineLatest<List<RoomMemberSummary>, PowerLevelsContent, RoomMemberSummaries>(
room.rx(session).liveRoomMembers(roomMemberQueryParams), room.rx().liveRoomMembers(roomMemberQueryParams),
room.rx(session) room.rx()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "") .liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
.mapOptional { it.content.toModel<PowerLevelsContent>() } .mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap(), .unwrap(),
@ -84,10 +90,36 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
.execute { async -> .execute { async ->
copy(roomMemberSummaries = async) copy(roomMemberSummaries = async)
} }
if (room.isEncrypted()) {
room.rx().liveRoomMembers(roomMemberQueryParams)
.observeOn(AndroidSchedulers.mainThread())
.switchMap { membersSummary ->
session.getLiveCryptoDeviceInfo(membersSummary.map { it.userId })
.asObservable()
.doOnError { Timber.e(it) }
.map { deviceList ->
// If any key change, emit the userIds list
deviceList.groupBy { it.userId }.mapValues {
val allDeviceTrusted = it.value.fold(it.value.isNotEmpty()) { prev, next ->
prev && next.trustLevel?.isCrossSigningVerified().orFalse()
}
if (session.getCrossSigningService().getUserCrossSigningKeys(it.key)?.isTrusted().orFalse()) {
if (allDeviceTrusted) RoomEncryptionTrustLevel.Trusted else RoomEncryptionTrustLevel.Warning
} else {
RoomEncryptionTrustLevel.Default
}
}
}
}
.execute { async ->
copy(trustLevelMap = async)
}
}
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx(session).liveRoomSummary() room.rx().liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
copy(roomSummary = async) copy(roomSummary = async)

View file

@ -20,6 +20,7 @@ import androidx.annotation.StringRes
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotx.R import im.vector.riotx.R
@ -28,7 +29,8 @@ import im.vector.riotx.features.roomprofile.RoomProfileArgs
data class RoomMemberListViewState( data class RoomMemberListViewState(
val roomId: String, val roomId: String,
val roomSummary: Async<RoomSummary> = Uninitialized, val roomSummary: Async<RoomSummary> = Uninitialized,
val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized,
val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized
) : MvRxState { ) : MvRxState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View file

@ -52,7 +52,7 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState:
} }
private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx(session).liveRoomSummary() room.rx().liveRoomSummary()
.unwrap() .unwrap()
.execute { async -> .execute { async ->
copy(roomSummary = async) copy(roomSummary = async)

View file

@ -426,6 +426,15 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// ============================================================================================================== // ==============================================================================================================
private fun refreshMyDevice() { private fun refreshMyDevice() {
session.getUserDevices(session.myUserId).map {
DeviceInfo(
user_id = session.myUserId,
deviceId = it.deviceId,
displayName = it.displayName()
)
}.let {
refreshCryptographyPreference(it)
}
// TODO Move to a ViewModel... // TODO Move to a ViewModel...
session.getDevicesList(object : MatrixCallback<DevicesListResponse> { session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) { override fun onSuccess(data: DevicesListResponse) {

View file

@ -115,11 +115,20 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
* It can be any mobile devices, and any browsers. * It can be any mobile devices, and any browsers.
*/ */
private fun refreshDevicesList() { private fun refreshDevicesList() {
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) { if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
// display something asap
val localKnown = session.getUserDevices(session.myUserId).map {
DeviceInfo(
user_id = session.myUserId,
deviceId = it.deviceId,
displayName = it.displayName()
)
}
setState { setState {
copy( copy(
// Keep known list if we have it, and let refresh go in backgroung // Keep known list if we have it, and let refresh go in backgroung
devices = this.devices.takeIf { it is Success } ?: Loading() devices = this.devices.takeIf { it is Success } ?: Success(localKnown)
) )
} }

View file

@ -20,7 +20,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:scaleType="center" android:scaleType="center"
android:tint="?riotx_text_primary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -60,13 +59,26 @@
android:textSize="12sp" android:textSize="12sp"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/actionEditable" app:layout_constraintEnd_toStartOf="@+id/actionSecondaryAccessory"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/actionIcon" app:layout_constraintStart_toEndOf="@id/actionIcon"
app:layout_constraintTop_toBottomOf="@id/actionTitle" app:layout_constraintTop_toBottomOf="@id/actionTitle"
app:layout_goneMarginStart="0dp" app:layout_goneMarginStart="0dp"
tools:text="@string/room_profile_encrypted_subtitle" /> tools:text="@string/room_profile_encrypted_subtitle" />
<ImageView
android:id="@+id/actionSecondaryAccessory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@drawable/ic_shield_warning"
android:layout_marginRight="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/actionEditable"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/actionEditable" android:id="@+id/actionEditable"
android:layout_width="wrap_content" android:layout_width="wrap_content"