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())
}

View file

@ -16,8 +16,6 @@
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.room.Room
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.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import io.reactivex.Observable
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>> {
val summaryObservable = room.getRoomSummaryLive()
return room.getRoomSummaryLive()
.asObservable()
.startWithCallable {
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}") }
.startWithCallable { room.roomSummary().toOptional() }
}
fun liveRoomMembers(queryParams: RoomMemberQueryParams): Observable<List<RoomMemberSummary>> {
val roomMembersObservable = room.getRoomMembersLive(queryParams).asObservable()
return room.getRoomMembersLive(queryParams).asObservable()
.startWithCallable {
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>> {
@ -180,6 +97,6 @@ class RxRoom(private val room: Room, private val session: Session) {
}
}
fun Room.rx(session: Session): RxRoom {
return RxRoom(this, session)
fun Room.rx(): RxRoom {
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 io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RxSession(private val session: Session) {
fun liveRoomSummaries(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
val summariesObservable = session.getRoomSummariesLive(queryParams).asObservable()
return session.getRoomSummariesLive(queryParams).asObservable()
.startWithCallable {
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>> {
@ -136,12 +110,16 @@ class RxSession(private val session: Session) {
}
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>> {
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 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.internal.crypto.crosssigning.DeviceTrustResult
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
@ -63,6 +62,4 @@ interface CrossSigningService {
fun checkDeviceTrust(otherUserId: String,
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
fun getTrustLevelForUsers(userIds: List<String>): RoomEncryptionTrustLevel
}

View file

@ -16,8 +16,6 @@
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
*/
@ -25,7 +23,5 @@ data class RoomMemberSummary constructor(
val membership: Membership,
val userId: String,
val displayName: String? = null,
val avatarUrl: String? = null,
// TODO Warning: Will not be populated if not using RxRoom
val userEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
val avatarUrl: String? = null
)

View file

@ -19,10 +19,11 @@ package im.vector.matrix.android.internal.crypto
import dagger.Binds
import dagger.Module
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.crosssigning.CrossSigningService
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.keysbackup.api.RoomKeysApi
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
@ -137,15 +138,6 @@ internal abstract class CryptoModule {
return RealmClearCacheTask(realmConfiguration)
}
@JvmStatic
@Provides
fun providesCryptoStore(@CryptoDatabase
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
return RealmCryptoStore(
realmConfiguration,
credentials)
}
@JvmStatic
@Provides
@SessionScope
@ -249,4 +241,10 @@ internal abstract class CryptoModule {
@Binds
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.session.SessionScope
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 javax.inject.Inject
@ -36,10 +39,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val olmDevice: MXOlmDevice,
private val syncTokenStore: SyncTokenStore,
private val credentials: Credentials,
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
private val downloadKeysForUsersTask: DownloadKeysForUsersTask,
coroutineDispatchers: MatrixCoroutineDispatchers,
taskExecutor: TaskExecutor) {
interface UserDevicesUpdateListener {
fun onUsersDeviceUpdate(users: List<String>)
fun onUsersDeviceUpdate(userIds: List<String>)
}
private val deviceChangeListeners = mutableListOf<UserDevicesUpdateListener>()
@ -72,17 +77,19 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
private val notReadyToRetryHS = mutableSetOf<String>()
init {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
taskExecutor.executorScope.launch(coroutineDispatchers.crypto) {
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
for ((userId, status) in deviceTrackingStatuses) {
if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) {
// if a download was in progress when we got shut down, it isn't any more.
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
isUpdated = true
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}
if (isUpdated) {
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
}
}

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 dagger.Lazy
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.MXCrossSigningInfo
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.session.SessionScope
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.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.withoutPrefix
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility
import timber.log.Timber
@ -56,9 +57,11 @@ internal class DefaultCrossSigningService @Inject constructor(
private val deviceListManager: DeviceListManager,
private val uploadSigningKeysTask: UploadSigningKeysTask,
private val uploadSignaturesTask: UploadSignaturesTask,
private val cryptoCoroutineScope: CoroutineScope,
private val computeTrustTask: ComputeTrustTask,
private val taskExecutor: TaskExecutor,
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
@ -210,6 +213,7 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey?.toBase64NoPadding(), uskPrivateKey?.toBase64NoPadding(), sskPrivateKey?.toBase64NoPadding())
uploadSigningKeysTask.configureWith(params) {
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - Keys successfully uploaded")
@ -245,6 +249,7 @@ internal class DefaultCrossSigningService @Inject constructor(
resetTrustOnKeyChange()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadSignatureQueryBuilder.build())) {
// this.retryCount = 3
this.executionThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.i("## CrossSigning - signatures successfully uploaded")
@ -497,6 +502,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
}
@ -543,6 +549,7 @@ internal class DefaultCrossSigningService @Inject constructor(
.withDeviceInfo(toUpload)
.build()
uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}.executeBy(taskExecutor)
}
@ -608,81 +615,52 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
override fun onUsersDeviceUpdate(users: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${users.size} users")
users.forEach { otherUserId ->
override fun onUsersDeviceUpdate(userIds: List<String>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
userIds.forEach { otherUserId ->
checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
checkUserTrust(otherUserId).let {
Timber.d("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
setUserKeysAsTrusted(otherUserId, it.isVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
// TODO if my keys have changes, i should recheck all devices of all users?
val devices = cryptoStore.getUserDeviceList(otherUserId)
devices?.forEach { device ->
val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
if (otherUserId == userId) {
// It's me, i should check if a newly trusted device is signing my master key
// 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())
}
if (otherUserId == userId) {
// It's me, i should check if a newly trusted device is signing my master key
// 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())
eventBus.post(CryptoToSessionUserTrustChange(userIds))
}
}
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val currentTrust = cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted()
cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
// If it's me, recheck trust of all users and devices?
val users = ArrayList<String>()
if (otherUserId == userId && currentTrust != trusted) {
cryptoStore.updateUsersTrust {
users.add(it)
checkUserTrust(it).isVerified()
}
}
}
}
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
users.forEach {
cryptoStore.getUserDeviceList(it)?.forEach { device ->
val updatedTrust = checkDeviceTrust(it, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
Timber.d("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
cryptoStore.setDeviceTrust(it, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
}
}
}
}

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.getById
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 io.realm.Realm
import io.realm.RealmConfiguration
@ -70,11 +71,13 @@ import io.realm.kotlin.where
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.set
@SessionScope
internal class RealmCryptoStore(private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
internal class RealmCryptoStore @Inject constructor(
@CryptoDatabase private val realmConfiguration: RealmConfiguration,
private val credentials: Credentials) : IMXCryptoStore {
/* ==========================================================================================
* Memory cache, to correctly release JNI objects
@ -403,14 +406,14 @@ internal class RealmCryptoStore(private val realmConfiguration: RealmConfigurati
{ realm: Realm ->
realm
.where<UserEntity>()
.`in`(UserEntityFields.USER_ID, userIds.toTypedArray())
.`in`(UserEntityFields.USER_ID, userIds.distinct().toTypedArray())
},
{ entity ->
entity.devices.map { CryptoMapper.mapToModel(it) }
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
it.flatten()
}
}

View file

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

View file

@ -17,11 +17,11 @@
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.MXCryptoError
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.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -49,7 +49,8 @@ internal class RoomSummaryMapper @Inject constructor(
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
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(),
isEncrypted = roomSummaryEntity.isEncrypted,
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
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.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState
@ -47,7 +48,8 @@ internal open class RoomSummaryEntity(
// this is required for querying
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var typingUserIds: RealmList<String> = RealmList()
var typingUserIds: RealmList<String> = RealmList(),
var roomEncryptionTrustLevelStr: String? = null
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name
@ -68,5 +70,19 @@ internal open class RoomSummaryEntity(
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
}

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

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room
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.toModel
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.RoomTopicContent
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.model.CurrentStateEventEntity
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.RoomSyncUnreadNotifications
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal class RoomSummaryUpdater @Inject constructor(
@UserId private val userId: String,
private val roomDisplayNameResolver: RoomDisplayNameResolver,
private val roomAvatarResolver: RoomAvatarResolver,
private val eventBus: EventBus,
private val monarchy: Monarchy) {
companion object {
@ -139,6 +143,18 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.otherMemberIds.clear()
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
@EpoxyAttribute
var subtitle: String? = null
@EpoxyAttribute
var iconRes: Int = 0
@EpoxyAttribute
var tintIcon: Boolean = true
@EpoxyAttribute
var editableRes: Int = R.drawable.ic_arrow_right
@EpoxyAttribute
var accessoryRes: Int = 0
@EpoxyAttribute
var editable: Boolean = true
@ -70,12 +77,23 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
holder.subtitle.setTextOrHide(subtitle)
if (iconRes != 0) {
holder.icon.setImageResource(iconRes)
ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(tintColor))
if (tintIcon) {
ImageViewCompat.setImageTintList(holder.icon, ColorStateList.valueOf(tintColor))
} else {
ImageViewCompat.setImageTintList(holder.icon, null)
}
holder.icon.isVisible = true
} else {
holder.icon.isVisible = false
}
if (accessoryRes != 0) {
holder.secondaryAccessory.setImageResource(accessoryRes)
holder.secondaryAccessory.isVisible = true
} else {
holder.secondaryAccessory.isVisible = false
}
if (editableRes != 0) {
val tintColorSecondary = if (destructive) {
tintColor
@ -95,5 +113,6 @@ abstract class ProfileActionItem : VectorEpoxyModel<ProfileActionItem.Holder>()
val title by bind<TextView>(R.id.actionTitle)
val subtitle by bind<TextView>(R.id.actionSubtitle)
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,
editable: Boolean = true,
@DrawableRes icon: Int = 0,
tintIcon: Boolean = true,
@DrawableRes editableRes: Int? = null,
destructive: Boolean = false,
divider: Boolean = true,
action: ClickListener? = null
action: ClickListener? = null,
@DrawableRes accessory: Int = 0
) {
profileActionItem {
iconRes(icon)
tintIcon(tintIcon)
id("action_$id")
subtitle(subtitle)
editable(editable)
editableRes?.let { editableRes(editableRes) }
destructive(destructive)
title(title)
accessoryRes(accessory)
listener { _ ->
action?.invoke()
}

View file

@ -31,10 +31,12 @@ class RxConfig @Inject constructor(
fun setupRxPlugin() {
RxJavaPlugins.setErrorHandler { throwable ->
Timber.e(throwable, "RxError")
// Avoid crash in production, except if user wants it
if (vectorPreferences.failFast()) {
throw throwable
// 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
if (vectorPreferences.failFast()) {
throw throwable
}
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,8 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
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.session.Session
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.powerlevels.PowerLevelsConstants
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.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiFunction
import timber.log.Timber
class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState: RoomMemberListViewState,
private val roomMemberSummaryComparator: RoomMemberSummaryComparator,
@ -70,10 +75,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
displayName = QueryStringValue.IsNotEmpty
memberships = Membership.activeMemberships()
}
Observable
.combineLatest<List<RoomMemberSummary>, PowerLevelsContent, RoomMemberSummaries>(
room.rx(session).liveRoomMembers(roomMemberQueryParams),
room.rx(session)
room.rx().liveRoomMembers(roomMemberQueryParams),
room.rx()
.liveStateEvent(EventType.STATE_ROOM_POWER_LEVELS, "")
.mapOptional { it.content.toModel<PowerLevelsContent>() }
.unwrap(),
@ -84,10 +90,36 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState
.execute { 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() {
room.rx(session).liveRoomSummary()
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
copy(roomSummary = async)

View file

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

View file

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

View file

@ -426,6 +426,15 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// ==============================================================================================================
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...
session.getDevicesList(object : MatrixCallback<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.
*/
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 {
copy(
// 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_centerVertical="true"
android:scaleType="center"
android:tint="?riotx_text_primary"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -60,13 +59,26 @@
android:textSize="12sp"
app:layout_constrainedWidth="true"
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_constraintStart_toEndOf="@id/actionIcon"
app:layout_constraintTop_toBottomOf="@id/actionTitle"
app:layout_goneMarginStart="0dp"
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
android:id="@+id/actionEditable"
android:layout_width="wrap_content"