Merge pull request #1303 from vector-im/feature/xs_old_new_session_detection

Feature/xs old new session detection
This commit is contained in:
Valere 2020-04-30 11:23:15 +02:00 committed by GitHub
commit 43eb804b23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 577 additions and 329 deletions

View File

@ -29,6 +29,7 @@ Improvements 🙌:
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199 - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
- Manage Session Settings / Cross Signing update (#1295) - Manage Session Settings / Cross Signing update (#1295)
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - Fix summary notification staying after "mark as read"

View File

@ -31,6 +31,8 @@ import im.vector.matrix.android.api.util.JsonDict
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 im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -58,6 +60,13 @@ class RxSession(private val session: Session) {
} }
} }
fun liveMyDeviceInfo(): Observable<List<DeviceInfo>> {
return session.cryptoService().getLiveMyDevicesInfo().asObservable()
.startWithCallable {
session.cryptoService().getMyDevicesInfo()
}
}
fun liveSyncState(): Observable<SyncState> { fun liveSyncState(): Observable<SyncState> {
return session.getSyncStateLive().asObservable() return session.getSyncStateLive().asObservable()
} }
@ -123,6 +132,13 @@ class RxSession(private val session: Session) {
} }
} }
fun liveCrossSigningPrivateKeys(): Observable<Optional<PrivateKeysInfo>> {
return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable()
.startWithCallable {
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional()
}
}
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> { fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
return session.getLiveAccountDataEvents(types).asObservable() return session.getLiveAccountDataEvents(types).asObservable()
.startWithCallable { .startWithCallable {

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.api.failure
import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import java.io.IOException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun Throwable.is401() = fun Throwable.is401() =
@ -32,6 +33,7 @@ fun Throwable.isTokenError() =
fun Throwable.shouldBeRetried(): Boolean { fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| this is IOException
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }

View File

@ -98,7 +98,9 @@ interface CryptoService {
fun removeRoomKeysRequestListener(listener: GossipingRequestListener) fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)

View File

@ -55,6 +55,8 @@ interface CrossSigningService {
fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
fun canCrossSign(): Boolean fun canCrossSign(): Boolean
fun trustUser(otherUserId: String, fun trustUser(otherUserId: String,

View File

@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
return myDeviceInfoHolder.get().myDevice return myDeviceInfoHolder.get().myDevice
} }
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) { override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
getDevicesTask getDevicesTask
.configureWith { .configureWith {
// this.executionThread = TaskThread.CRYPTO // this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = object : MatrixCallback<DevicesListResponse> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList())
callback.onSuccess(data)
}
}
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return cryptoStore.getLiveMyDevicesInfo()
}
override fun getMyDevicesInfo(): List<DeviceInfo> {
return cryptoStore.getMyDevicesInfo()
}
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) { .configureWith(GetDeviceInfoTask.Params(deviceId)) {
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync) internalStart(isInitialSync)
} }
// Just update
fetchDevicesList(NoOpMatrixCallback())
} }
private suspend fun internalStart(isInitialSync: Boolean) { private suspend fun internalStart(isInitialSync: Boolean) {

View File

@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor(
return cryptoStore.getCrossSigningPrivateKeys() return cryptoStore.getCrossSigningPrivateKeys()
} }
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
return cryptoStore.getLiveCrossSigningPrivateKeys()
}
override fun canCrossSign(): Boolean { override fun canCrossSign(): Boolean {
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null && cryptoStore.getCrossSigningPrivateKeys()?.user != null

View File

@ -29,7 +29,8 @@ data class CryptoDeviceInfo(
override val signatures: Map<String, Map<String, String>>? = null, override val signatures: Map<String, Map<String, String>>? = null,
val unsigned: JsonDict? = null, val unsigned: JsonDict? = null,
var trustLevel: DeviceTrustLevel? = null, var trustLevel: DeviceTrustLevel? = null,
var isBlocked: Boolean = false var isBlocked: Boolean = false,
val firstTimeSeenLocalTs: Long? = null
) : CryptoInfo { ) : CryptoInfo {
val isVerified: Boolean val isVerified: Boolean

View File

@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
signatures = keyInfo.signatures signatures = keyInfo.signatures
) )
} }
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
return map(this)
}
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
return map(this)
}
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
// return map(this)
// }
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
return map(this)
}
} }

View File

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
@ -218,6 +219,9 @@ internal interface IMXCryptoStore {
// TODO temp // TODO temp
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>> fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun saveMyDevicesInfo(info: List<DeviceInfo>)
/** /**
* Store the crypto algorithm for a room. * Store the crypto algorithm for a room.
* *
@ -405,6 +409,7 @@ internal interface IMXCryptoStore {
fun storeUSKPrivateKey(usk: String?) fun storeUSKPrivateKey(usk: String?)
fun getCrossSigningPrivateKeys(): PrivateKeysInfo? fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?

View File

@ -40,6 +40,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.model.toEntity import im.vector.matrix.android.internal.crypto.model.toEntity
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossiping
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
@ -287,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor(
UserEntity.getOrCreate(realm, userId) UserEntity.getOrCreate(realm, userId)
.let { u -> .let { u ->
// Add the devices // Add the devices
val currentKnownDevices = u.devices.toList()
val new = devices.map { entry -> entry.value.toEntity() }
new.forEach { entity ->
// Maintain first time seen
val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
realm.insertOrUpdate(entity)
}
// Ensure all other devices are deleted // Ensure all other devices are deleted
u.devices.deleteAllFromRealm() u.devices.deleteAllFromRealm()
val new = devices.map { entry -> entry.value.toEntity() }
new.forEach { realm.insertOrUpdate(it) }
u.devices.addAll(new) u.devices.addAll(new)
} }
} }
@ -358,6 +366,25 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
val liveData = monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm
.where<CryptoMetadataEntity>()
},
{
PrivateKeysInfo(
master = it.xSignMasterPrivateKey,
selfSigned = it.xSignSelfSignedPrivateKey,
user = it.xSignUserPrivateKey
)
}
)
return Transformations.map(liveData) {
it.firstOrNull().toOptional()
}
}
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
@ -482,6 +509,52 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun getMyDevicesInfo(): List<DeviceInfo> {
return monarchy.fetchAllCopiedSync {
it.where<MyDeviceLastSeenInfoEntity>()
}.map {
DeviceInfo(
deviceId = it.deviceId,
lastSeenIp = it.lastSeenIp,
lastSeenTs = it.lastSeenTs,
displayName = it.displayName
)
}
}
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
return monarchy.findAllMappedWithChanges(
{ realm: Realm ->
realm.where<MyDeviceLastSeenInfoEntity>()
},
{ entity ->
DeviceInfo(
deviceId = entity.deviceId,
lastSeenIp = entity.lastSeenIp,
lastSeenTs = entity.lastSeenTs,
displayName = entity.displayName
)
}
)
}
override fun saveMyDevicesInfo(info: List<DeviceInfo>) {
val entities = info.map {
MyDeviceLastSeenInfoEntity(
lastSeenTs = it.lastSeenTs,
lastSeenIp = it.lastSeenIp,
displayName = it.displayName,
deviceId = it.deviceId
)
}
monarchy.writeAsync { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach {
realm.insertOrUpdate(it)
}
}
}
override fun storeRoomAlgorithm(roomId: String, algorithm: String) { override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
@ -27,6 +28,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityF
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
@ -40,7 +42,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// Version 1L added Cross Signing info persistence // Version 1L added Cross Signing info persistence
companion object { companion object {
const val CRYPTO_STORE_SCHEMA_VERSION = 4L const val CRYPTO_STORE_SCHEMA_VERSION = 5L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -50,6 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -212,4 +215,24 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
} catch (failure: Throwable) { } catch (failure: Throwable) {
} }
} }
private fun migrateTo5(realm: DynamicRealm) {
realm.schema.create("MyDeviceLastSeenInfoEntity")
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
.addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
.setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
val now = System.currentTimeMillis()
realm.schema.get("DeviceInfoEntity")
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity ->
tryThis {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
}
}
}
} }

View File

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule
TrustLevelEntity::class, TrustLevelEntity::class,
GossipingEventEntity::class, GossipingEventEntity::class,
IncomingGossipingRequestEntity::class, IncomingGossipingRequestEntity::class,
OutgoingGossipingRequestEntity::class OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class
]) ])
internal class RealmCryptoStoreModule internal class RealmCryptoStoreModule

View File

@ -104,7 +104,8 @@ object CryptoMapper {
Timber.e(failure) Timber.e(failure)
null null
} }
} },
firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
) )
} }
} }

View File

@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
var keysMapJson: String? = null, var keysMapJson: String? = null,
var signatureMapJson: String? = null, var signatureMapJson: String? = null,
var unsignedMapJson: String? = null, var unsignedMapJson: String? = null,
var trustLevelEntity: TrustLevelEntity? = null var trustLevelEntity: TrustLevelEntity? = null,
/**
* We use that to make distinction between old devices (there before mine)
* and new ones. Used for example to detect new unverified login
*/
var firstTimeSeenLocalTs: Long? = null
) : RealmObject() { ) : RealmObject() {
// // Deserialize data // // Deserialize data

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) 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.store.db.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class MyDeviceLastSeenInfoEntity(
/**The device id*/
@PrimaryKey var deviceId: String? = null,
/** The device display name*/
var displayName: String? = null,
/** The last time this device has been seen. */
var lastSeenTs: Long? = null,
/** The last ip address*/
var lastSeenIp: String? = null
) : RealmObject() {
companion object
}

View File

@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor(
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
sendToDeviceBody sendToDeviceBody
) )
isRetryable = true
maxRetryCount = 3
} }
} }
} }

View File

@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
Timber.v("## SAS O: onVerificationAccept id:$transactionId") Timber.v("## SAS O: onVerificationAccept id:$transactionId")
if (state != VerificationTxState.Started) { if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) {
Timber.e("## SAS O: received accept request from invalid state $state") Timber.e("## SAS O: received accept request from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|| !KNOWN_HASHES.contains(accept.hash) || !KNOWN_HASHES.contains(accept.hash)
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode) || !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|| accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) { || accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) {
Timber.e("## SAS O: received accept request from invalid state") Timber.e("## SAS O: received invalid accept")
cancel(CancelCode.UnknownMethod) cancel(CancelCode.UnknownMethod)
return return
} }

View File

@ -117,6 +117,7 @@ internal class VerificationTransportToDevice(
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type") Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo") Timber.v("## SAS sending msg info $verificationInfo")
val stateBeforeCall = tx?.state
val tx = tx ?: return val tx = tx ?: return
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
@ -132,7 +133,11 @@ internal class VerificationTransportToDevice(
if (onDone != null) { if (onDone != null) {
onDone() onDone()
} else { } else {
tx.state = nextState // we may have received next state (e.g received accept in sending_start)
// We only put next state if the state was what is was before we started
if (tx.state == stateBeforeCall) {
tx.state = nextState
}
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.shouldBeRetried
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -46,7 +47,7 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
throw response.toFailure(eventBus) throw response.toFailure(eventBus)
} }
} catch (exception: Throwable) { } catch (exception: Throwable) {
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) { if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
delay(currentDelay) delay(currentDelay)
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
return execute() return execute()

View File

@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.DefaultVectorAlert
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
session = null session = null
} }
override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean { override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled // By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
Timber.v("## onSecretShareRequest() : Ignoring $request") Timber.v("## onSecretShareRequest() : Ignoring $request")
request.ignore?.run() request.ignore?.run()
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
// can we get more info on this device? // can we get more info on this device?
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> { session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
override fun onSuccess(data: DevicesListResponse) { postAlert(context, userId, deviceId, true, deviceInfo, it)
data.devices?.find { it.deviceId == deviceId }?.let { } ?: kotlin.run {
postAlert(context, userId, deviceId, true, deviceInfo, it) postAlert(context, userId, deviceId, true, deviceInfo)
} ?: run { }
postAlert(context, userId, deviceId, true, deviceInfo)
}
}
override fun onFailure(failure: Throwable) {
postAlert(context, userId, deviceId, true, deviceInfo)
}
})
} else { } else {
postAlert(context, userId, deviceId, false, deviceInfo) postAlert(context, userId, deviceId, false, deviceInfo)
} }

View File

@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
uid, uid,
context.getString(R.string.sas_incoming_request_notif_title), context.getString(R.string.sas_incoming_request_notif_title),
context.getString(R.string.sas_incoming_request_notif_content, name), context.getString(R.string.sas_incoming_request_notif_content, name),
R.drawable.shield, R.drawable.ic_shield_black,
shouldBeDisplayedIn = { activity -> shouldBeDisplayedIn = { activity ->
if (activity is VectorBaseActivity) { if (activity is VectorBaseActivity) {
// TODO a bit too hugly :/ // TODO a bit too hugly :/

View File

@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
// Just ignore for now // Just ignore for now
Timber.v("## Failed to restore backup after SSSS recovery") Timber.e(failure, "## Failed to restore backup after SSSS recovery")
} }
} }
} }

View File

@ -30,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
@ -42,6 +43,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.PopupAlertManager
import im.vector.riotx.features.popup.VerificationVectorAlert import im.vector.riotx.features.popup.VerificationVectorAlert
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.riotx.features.workers.signout.SignOutViewModel import im.vector.riotx.features.workers.signout.SignOutViewModel
import kotlinx.android.synthetic.main.fragment_home_detail.* import kotlinx.android.synthetic.main.fragment_home_detail.*
import timber.log.Timber import timber.log.Timber
@ -86,43 +88,82 @@ class HomeDetailFragment @Inject constructor(
switchDisplayMode(displayMode) switchDisplayMode(displayMode)
} }
unknownDeviceDetectorSharedViewModel.subscribe { unknownDeviceDetectorSharedViewModel.subscribe { state ->
it.unknownSessions.invoke()?.let { unknownDevices -> state.unknownSessions.invoke()?.let { unknownDevices ->
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") // Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
unknownDevices.forEachIndexed { index, deviceInfo -> if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") val uid = "review_login"
} alertManager.cancelAlert(uid)
val uid = "Newest_Device" val olderUnverified = unknownDevices.filter { !it.isNew }
alertManager.cancelAlert(uid) val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
if (it.canCrossSign && unknownDevices.isNotEmpty()) { if (newest != null) {
val newest = unknownDevices.first().second promptForNewUnknownDevices(uid, state, newest)
val user = unknownDevices.first().first } else if (olderUnverified.isNotEmpty()) {
alertManager.postVectorAlert( // In this case we prompt to go to settings to review logins
VerificationVectorAlert( promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo })
uid = uid, }
title = getString(R.string.new_session),
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId ?: "")
)
}
}
)
} }
} }
} }
} }
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.new_session),
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
)
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
)
}
}
)
}
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
val user = state.myMatrixItem
alertManager.postVectorAlert(
VerificationVectorAlert(
uid = uid,
title = getString(R.string.review_logins),
description = getString(R.string.verify_other_sessions),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = user
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
// mark as ignored to avoid showing it again
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
}
}
dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
)
}
}
)
}
private fun onGroupChange(groupSummary: GroupSummary?) { private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let { groupSummary?.let {
// Use GlideApp with activity context to avoid the glideRequests to be paused // Use GlideApp with activity context to avoid the glideRequests to be paused

View File

@ -0,0 +1,156 @@
/*
* Copyright (c) 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.riotx.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable
import io.reactivex.functions.Function3
import timber.log.Timber
import java.util.concurrent.TimeUnit
data class UnknownDevicesState(
val myMatrixItem: MatrixItem.UserItem? = null,
val unknownSessions: Async<List<DeviceDetectionInfo>> = Uninitialized
) : MvRxState
data class DeviceDetectionInfo(
val deviceInfo: DeviceInfo,
val isNew: Boolean,
val currentSessionTrust: Boolean
)
class UnknownDeviceDetectorSharedViewModel(
session: Session,
private val vectorPreferences: VectorPreferences,
initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceIds: List<String>) : Action()
}
private val ignoredDeviceList = ArrayList<String>()
init {
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull {
it.deviceId == session.sessionParams.credentials.deviceId
}?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it")
}
)
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, Optional<PrivateKeysInfo>, List<DeviceDetectionInfo>>(
session.rx().liveUserCryptoDevices(session.myUserId),
session.rx().liveMyDeviceInfo(),
session.rx().liveCrossSigningPrivateKeys(),
Function3 { cryptoList, infoList, pInfo ->
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
infoList
.filter { info ->
// filter verified session, by checking the crypto device info
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
}
// filter out ignored devices
.filter { !ignoredDeviceList.contains(it.deviceId) }
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
DeviceDetectionInfo(
deviceInfo,
deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive,
pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
)
}
}
)
.distinctUntilChanged()
.execute { async ->
// Timber.v("## Detector trigger passed distinct")
copy(
myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(),
unknownSessions = async
)
}
session.rx().liveUserCryptoDevices(session.myUserId)
.distinct()
.throttleLast(5_000, TimeUnit.MILLISECONDS)
.subscribe {
// If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}.disposeOnClear()
// trigger a refresh of lastSeen / last Ip
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}
override fun handle(action: Action) {
when (action) {
is Action.IgnoreDevice -> {
ignoredDeviceList.addAll(action.deviceIds)
// local echo
withState { state ->
state.unknownSessions.invoke()?.let { detectedSessions ->
val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
setState {
copy(unknownSessions = Success(updated))
}
}
}
}
}
}
override fun onCleared() {
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
super.onCleared()
}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
}
}
}

View File

@ -1,124 +0,0 @@
/*
* Copyright (c) 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.riotx.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.singleBuilder
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
import java.util.concurrent.TimeUnit
data class UnknownDevicesState(
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
val canCrossSign: Boolean = false
) : MvRxState
class UnknownDeviceDetectorSharedViewModel(
session: Session,
private val vectorPreferences: VectorPreferences,
initialState: UnknownDevicesState)
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
sealed class Action : VectorViewModelAction {
data class IgnoreDevice(val deviceId: String) : Action()
}
val ignoredDeviceList = ArrayList<String>()
init {
ignoredDeviceList.addAll(
vectorPreferences.getUnknownDeviceDismissedList().also {
Timber.v("## Detector - Remembered ignored list $it")
}
)
session.rx().liveUserCryptoDevices(session.myUserId)
.debounce(600, TimeUnit.MILLISECONDS)
.distinct()
.switchMap { deviceList ->
Timber.v("## Detector - ============================")
Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
singleBuilder<DevicesListResponse> {
session.cryptoService().getDevicesList(it)
NoOpCancellable
}.map { resp ->
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
resp.devices?.filter { info ->
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
!it.isVerified
} ?: false
}
?.sortedByDescending { it.lastSeenTs }
?.map {
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
}
?.filter { !ignoredDeviceList.contains(it.second.deviceId) }
?: emptyList()
}
.toObservable()
}
.execute { async ->
copy(unknownSessions = async)
}
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
}
}
override fun handle(action: Action) {
when (action) {
is Action.IgnoreDevice -> {
// local echo
withState { state ->
state.unknownSessions.invoke()?.let {
val updated = it.filter { it.second.deviceId != action.deviceId }
setState {
copy(unknownSessions = Success(updated))
}
}
}
ignoredDeviceList.add(action.deviceId)
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
}
}
}
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
}
}
}

View File

@ -96,7 +96,7 @@ class DefaultNavigator @Inject constructor(
roomId = null, roomId = null,
otherUserId = session.myUserId, otherUserId = session.myUserId,
transactionId = pr.transactionId transactionId = pr.transactionId
).show(context.supportFragmentManager, "REQPOP") ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
} }
} }

View File

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import kotlinx.android.synthetic.main.activity_vector_settings.* import kotlinx.android.synthetic.main.activity_vector_settings.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -58,11 +59,16 @@ class VectorSettingsActivity : VectorBaseActivity(),
if (isFirstCreation()) { if (isFirstCreation()) {
// display the fragment // display the fragment
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else -> EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS ->
replaceFragment(R.id.vector_settings_page,
VectorSettingsDevicesFragment::class.java,
null,
FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
} }
} }
@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
} }

View File

@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
refreshCryptographyPreference(it) refreshCryptographyPreference(it)
} }
// TODO Move to a ViewModel... // TODO Move to a ViewModel...
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> { session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) { override fun onSuccess(data: DevicesListResponse) {
if (isAdded) { if (isAdded) {
refreshCryptographyPreference(data.devices ?: emptyList()) refreshCryptographyPreference(data.devices ?: emptyList())

View File

@ -57,6 +57,9 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var trusted: DeviceTrustLevel? = null var trusted: DeviceTrustLevel? = null
@EpoxyAttribute
var e2eCapable: Boolean = true
@EpoxyAttribute @EpoxyAttribute
var legacyMode: Boolean = false var legacyMode: Boolean = false
@ -79,7 +82,11 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
trusted trusted
) )
holder.trustIcon.setImageResource(shield) if (e2eCapable) {
holder.trustIcon.setImageResource(shield)
} else {
holder.trustIcon.setImageDrawable(null)
}
val detailedModeLabels = listOf( val detailedModeLabels = listOf(
holder.displayNameLabelText, holder.displayNameLabelText,

View File

@ -16,17 +16,14 @@
package im.vector.riotx.features.settings.devices package im.vector.riotx.features.settings.devices
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
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.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -40,7 +37,7 @@ data class DeviceVerificationInfoBottomSheetViewState(
val deviceInfo: Async<DeviceInfo> = Uninitialized, val deviceInfo: Async<DeviceInfo> = Uninitialized,
val hasAccountCrossSigning: Boolean = false, val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false,
val isMine : Boolean = false val isMine: Boolean = false
) : MvRxState ) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
@ -79,22 +76,18 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
) )
} }
setState { setState {
copy(deviceInfo = Loading()) copy(deviceInfo = Loading())
} }
session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback<DeviceInfo> {
override fun onSuccess(data: DeviceInfo) {
setState {
copy(deviceInfo = Success(data))
}
}
override fun onFailure(failure: Throwable) { session.rx().liveMyDeviceInfo()
setState { .map { devices ->
copy(deviceInfo = Fail(failure)) devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId)
}
.execute {
copy(deviceInfo = it)
} }
}
})
} }
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> { companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {

View File

@ -20,7 +20,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction() object Refresh : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction()

View File

@ -21,9 +21,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
@ -73,20 +71,19 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
listener { callback?.retry() } listener { callback?.retry() }
} }
is Success -> is Success ->
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted) buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
} }
} }
private fun buildDevicesList(devices: List<DeviceInfo>, private fun buildDevicesList(devices: List<DeviceFullInfo>,
cryptoDevices: List<CryptoDeviceInfo>?,
myDeviceId: String, myDeviceId: String,
legacyMode: Boolean, legacyMode: Boolean,
currentSessionCrossTrusted: Boolean) { currentSessionCrossTrusted: Boolean) {
devices devices
.firstOrNull() { .firstOrNull {
it.deviceId == myDeviceId it.deviceInfo.deviceId == myDeviceId
}?.let { deviceInfo -> }?.let { fullInfo ->
val deviceInfo = fullInfo.deviceInfo
// Current device // Current device
genericItemHeader { genericItemHeader {
id("current") id("current")
@ -102,6 +99,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(true) currentDevice(true)
e2eCapable(true)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true)) trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
} }
@ -129,12 +127,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
devices devices
.filter { .filter {
it.deviceId != myDeviceId it.deviceInfo.deviceId != myDeviceId
} }
// sort before display: most recent first .forEachIndexed { idx, deviceInfoPair ->
.sortByLastSeen() val deviceInfo = deviceInfoPair.deviceInfo
.forEachIndexed { idx, deviceInfo -> val cryptoInfo = deviceInfoPair.cryptoDeviceInfo
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem { deviceItem {
id("device$idx") id("device$idx")
legacyMode(legacyMode) legacyMode(legacyMode)
@ -143,9 +140,10 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
colorProvider(colorProvider) colorProvider(colorProvider)
detailedMode(vectorPreferences.developerMode()) detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(isCurrentDevice) currentDevice(false)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.trustLevel) e2eCapable(cryptoInfo != null)
trusted(cryptoInfo?.trustLevel)
} }
} }
} }

View File

@ -29,6 +29,7 @@ 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.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -39,30 +40,35 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTxSt
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, // val devices: Async<List<DeviceInfo>> = Uninitialized,
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized, // val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
// TODO Replace by isLoading boolean // TODO Replace by isLoading boolean
val request: Async<Unit> = Uninitialized, val request: Async<Unit> = Uninitialized,
val hasAccountCrossSigning: Boolean = false, val hasAccountCrossSigning: Boolean = false,
val accountCrossSigningIsTrusted: Boolean = false val accountCrossSigningIsTrusted: Boolean = false
) : MvRxState ) : MvRxState
data class DeviceFullInfo(
val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo?
)
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session, private val session: Session
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) ) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -82,14 +88,37 @@ class DevicesViewModel @AssistedInject constructor(
private var _currentDeviceId: String? = null private var _currentDeviceId: String? = null
private var _currentSession: String? = null private var _currentSession: String? = null
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
init { init {
setState { setState {
copy( copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
) )
} }
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, List<DeviceFullInfo>>(
session.rx().liveUserCryptoDevices(session.myUserId),
session.rx().liveMyDeviceInfo(),
BiFunction { cryptoList, infoList ->
infoList
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
DeviceFullInfo(deviceInfo, cryptoDeviceInfo)
}
}
)
.distinct()
.execute { async ->
copy(
devices = async
)
}
session.rx().liveCrossSigningInfo(session.myUserId) session.rx().liveCrossSigningInfo(session.myUserId)
.execute { .execute {
copy( copy(
@ -97,16 +126,38 @@ class DevicesViewModel @AssistedInject constructor(
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
) )
} }
refreshDevicesList()
session.cryptoService().verificationService().addListener(this) session.cryptoService().verificationService().addListener(this)
// session.rx().liveMyDeviceInfo()
// .execute {
// copy(
// devices = it
// )
// }
session.rx().liveUserCryptoDevices(session.myUserId) session.rx().liveUserCryptoDevices(session.myUserId)
.execute { .distinct()
copy( .throttleLast(5_000, TimeUnit.MILLISECONDS)
cryptoDevices = it .subscribe {
) // If we have a new crypto device change, we might want to trigger refresh of device info
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
}.disposeOnClear()
// session.rx().liveUserCryptoDevices(session.myUserId)
// .execute {
// copy(
// cryptoDevices = it
// )
// }
refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS)
.subscribe {
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
} }
.disposeOnClear()
// then force download
queryRefreshDevicesList()
} }
override fun onCleared() { override fun onCleared() {
@ -116,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor(
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) { if (tx.state == VerificationTxState.Verified) {
refreshDevicesList() queryRefreshDevicesList()
} }
} }
@ -125,69 +176,13 @@ class DevicesViewModel @AssistedInject constructor(
* The devices list is the list of the devices where the user is logged in. * The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers. * It can be any mobile devices, and any browsers.
*/ */
private fun refreshDevicesList() { private fun queryRefreshDevicesList() {
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { refreshPublisher.onNext(Unit)
// display something asap
val localKnown = session.cryptoService().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 } ?: Success(localKnown)
)
}
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
devices = Success(data.devices.orEmpty())
)
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(
devices = Fail(failure)
)
}
}
})
// Put cached state
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
)
}
// then force download
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
setState {
copy(
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
)
}
}
})
} else {
// Should not happen
}
} }
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
@ -210,10 +205,10 @@ class DevicesViewModel @AssistedInject constructor(
} }
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state -> private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
state.cryptoDevices.invoke() state.devices.invoke()
?.firstOrNull { it.deviceId == action.deviceId } ?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId }
?.let { ?.let {
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it)) _viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it.cryptoDeviceInfo!!))
} }
} }
@ -238,9 +233,9 @@ class DevicesViewModel @AssistedInject constructor(
} }
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state -> private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId }
if (info != null) { if (info != null) {
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info)) _viewEvents.post(DevicesViewEvents.PromptRenameDevice(info.deviceInfo))
} }
} }
@ -253,7 +248,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -324,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
}) })
} }
@ -353,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
// force settings update // force settings update
refreshDevicesList() queryRefreshDevicesList()
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {

View File

@ -102,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
viewModel.handle(DevicesAction.Refresh)
} }
override fun onDeviceClicked(deviceInfo: DeviceInfo) { override fun onDeviceClicked(deviceInfo: DeviceInfo) {
@ -122,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
// } // }
override fun retry() { override fun retry() {
viewModel.handle(DevicesAction.Retry) viewModel.handle(DevicesAction.Refresh)
} }
/** /**

View File

@ -2201,7 +2201,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="refresh">Refresh</string> <string name="refresh">Refresh</string>
<string name="new_session">Unverified login. Was this you?</string> <string name="new_session">New login. Was this you?</string>
<string name="new_session_review">Tap to review &amp; verify</string> <string name="new_session_review">Tap to review &amp; verify</string>
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string> <string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
<string name="verify_new_session_was_not_me">This wasnt me</string> <string name="verify_new_session_was_not_me">This wasnt me</string>

View File

@ -6,7 +6,8 @@
<!-- Sections has been created to limit merge conflicts. --> <!-- Sections has been created to limit merge conflicts. -->
<!-- BEGIN Strings added by Valere --> <!-- BEGIN Strings added by Valere -->
<string name="review_logins">Review where youre logged in</string>
<string name="verify_other_sessions">Verify all your sessions to ensure your account &amp; messages are safe</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->