diff --git a/CHANGES.md b/CHANGES.md index 08eafff2d3..78118adc07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -29,6 +29,7 @@ Improvements 🙌: - 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 - Manage Session Settings / Cross Signing update (#1295) + - Cross-Signing | Review sessions toast update old vs new (#1293, #1306) Bugfix 🐛: - Fix summary notification staying after "mark as read" diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt index 87ff6f0390..c2c8978500 100644 --- a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxSession.kt @@ -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.toOptional 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 io.reactivex.Observable import io.reactivex.Single @@ -58,6 +60,13 @@ class RxSession(private val session: Session) { } } + fun liveMyDeviceInfo(): Observable> { + return session.cryptoService().getLiveMyDevicesInfo().asObservable() + .startWithCallable { + session.cryptoService().getMyDevicesInfo() + } + } + fun liveSyncState(): Observable { return session.getSyncStateLive().asObservable() } @@ -123,6 +132,13 @@ class RxSession(private val session: Session) { } } + fun liveCrossSigningPrivateKeys(): Observable> { + return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable() + .startWithCallable { + session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional() + } + } + fun liveAccountData(types: Set): Observable> { return session.getLiveAccountDataEvents(types).asObservable() .startWithCallable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index b325be4451..7b5dffb21e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.failure import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.di.MoshiProvider +import java.io.IOException import javax.net.ssl.HttpsURLConnection fun Throwable.is401() = @@ -32,6 +33,7 @@ fun Throwable.isTokenError() = fun Throwable.shouldBeRetried(): Boolean { return this is Failure.NetworkConnection + || this is IOException || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index e6fbaaf9a6..2c96465313 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -98,7 +98,9 @@ interface CryptoService { fun removeRoomKeysRequestListener(listener: GossipingRequestListener) - fun getDevicesList(callback: MatrixCallback) + fun fetchDevicesList(callback: MatrixCallback) + fun getMyDevicesInfo() : List + fun getLiveMyDevicesInfo() : LiveData> fun getDeviceInfo(deviceId: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index 4085e1233d..f1c998cee5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -55,6 +55,8 @@ interface CrossSigningService { fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData> + fun canCrossSign(): Boolean fun trustUser(otherUserId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index a789814958..1d3c0f4dcd 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor( return myDeviceInfoHolder.get().myDevice } - override fun getDevicesList(callback: MatrixCallback) { + override fun fetchDevicesList(callback: MatrixCallback) { getDevicesTask .configureWith { // this.executionThread = TaskThread.CRYPTO - this.callback = callback + this.callback = object : MatrixCallback { + 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) } + override fun getLiveMyDevicesInfo(): LiveData> { + return cryptoStore.getLiveMyDevicesInfo() + } + + override fun getMyDevicesInfo(): List { + return cryptoStore.getMyDevicesInfo() + } + override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) { getDeviceInfoTask .configureWith(GetDeviceInfoTask.Params(deviceId)) { @@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor( cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { internalStart(isInitialSync) } + // Just update + fetchDevicesList(NoOpMatrixCallback()) } private suspend fun internalStart(isInitialSync: Boolean) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 2166e4be3a..2fee8130fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor( return cryptoStore.getCrossSigningPrivateKeys() } + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + return cryptoStore.getLiveCrossSigningPrivateKeys() + } + override fun canCrossSign(): Boolean { return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && cryptoStore.getCrossSigningPrivateKeys()?.user != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt index b124f7590e..fc6e2cc436 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt @@ -29,7 +29,8 @@ data class CryptoDeviceInfo( override val signatures: Map>? = null, val unsigned: JsonDict? = null, var trustLevel: DeviceTrustLevel? = null, - var isBlocked: Boolean = false + var isBlocked: Boolean = false, + val firstTimeSeenLocalTs: Long? = null ) : CryptoInfo { val isVerified: Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt index 4459d508ff..f3ddfb8faa 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoInfoMapper.kt @@ -61,20 +61,4 @@ internal object CryptoInfoMapper { 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) - } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt index 0d1026b69f..18c85f78fb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -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.OlmInboundGroupSessionWrapper 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.store.db.model.KeysBackupDataEntity import org.matrix.olm.OlmAccount @@ -218,6 +219,9 @@ internal interface IMXCryptoStore { // TODO temp fun getLiveDeviceList(): LiveData> + fun getMyDevicesInfo() : List + fun getLiveMyDevicesInfo() : LiveData> + fun saveMyDevicesInfo(info: List) /** * Store the crypto algorithm for a room. * @@ -405,6 +409,7 @@ internal interface IMXCryptoStore { fun storeUSKPrivateKey(usk: String?) fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun getLiveCrossSigningPrivateKeys(): LiveData> fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt index 107d286d43..c57dff046b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -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.OlmInboundGroupSessionWrapper 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.toEntity 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.KeyInfoEntity 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.OlmInboundGroupSessionEntityFields import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity @@ -287,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor( UserEntity.getOrCreate(realm, userId) .let { u -> // 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 u.devices.deleteAllFromRealm() - val new = devices.map { entry -> entry.value.toEntity() } - new.forEach { realm.insertOrUpdate(it) } u.devices.addAll(new) } } @@ -358,6 +366,25 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getLiveCrossSigningPrivateKeys(): LiveData> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm + .where() + }, + { + 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?) { doRealmTransaction(realmConfiguration) { realm -> realm.where().findFirst()?.apply { @@ -482,6 +509,52 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getMyDevicesInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDevicesInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> + DeviceInfo( + deviceId = entity.deviceId, + lastSeenIp = entity.lastSeenIp, + lastSeenTs = entity.lastSeenTs, + displayName = entity.displayName + ) + } + ) + } + + override fun saveMyDevicesInfo(info: List) { + val entities = info.map { + MyDeviceLastSeenInfoEntity( + lastSeenTs = it.lastSeenTs, + lastSeenIp = it.lastSeenIp, + displayName = it.displayName, + deviceId = it.deviceId + ) + } + monarchy.writeAsync { realm -> + realm.where().findAll().deleteAllFromRealm() + entities.forEach { + realm.insertOrUpdate(it) + } + } + } + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { doRealmTransaction(realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt index c0949319c1..c1897c76d9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store.db import com.squareup.moshi.Moshi 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.internal.crypto.model.MXDeviceInfo 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.IncomingGossipingRequestEntityFields 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.TrustLevelEntityFields 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 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) { @@ -50,6 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 3) migrateTo4(realm) + if (oldVersion <= 4) migrateTo5(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -212,4 +215,24 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi } 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) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt index 3da91c6268..a8eb1db612 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -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.KeyInfoEntity 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.OlmSessionEntity import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity @@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule TrustLevelEntity::class, GossipingEventEntity::class, IncomingGossipingRequestEntity::class, - OutgoingGossipingRequestEntity::class + OutgoingGossipingRequestEntity::class, + MyDeviceLastSeenInfoEntity::class ]) internal class RealmCryptoStoreModule diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt index 5a4938d1fe..d222fe0eed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMapper.kt @@ -104,7 +104,8 @@ object CryptoMapper { Timber.e(failure) null } - } + }, + firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs ) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt index 98f931a455..7f16ad6357 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt @@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "", var keysMapJson: String? = null, var signatureMapJson: 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() { // // Deserialize data diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt new file mode 100644 index 0000000000..04d258ed5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt index 58c461888b..361fd25cee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt @@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor( params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(), sendToDeviceBody ) + isRetryable = true + maxRetryCount = 3 } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt index 7fd97d0231..689829b8e3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultOutgoingSASDefaultVerificationTransaction.kt @@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( override fun onVerificationAccept(accept: ValidVerificationInfoAccept) { 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") cancel(CancelCode.UnexpectedMessage) return @@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction( || !KNOWN_HASHES.contains(accept.hash) || !KNOWN_MACS.contains(accept.messageAuthenticationCode) || 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) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index 290fc88878..59a5b80b99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -117,6 +117,7 @@ internal class VerificationTransportToDevice( onDone: (() -> Unit)?) { Timber.d("## SAS sending msg type $type") Timber.v("## SAS sending msg info $verificationInfo") + val stateBeforeCall = tx?.state val tx = tx ?: return val contentMap = MXUsersDevicesMap() val toSendToDeviceObject = verificationInfo.toSendToDeviceObject() @@ -132,7 +133,11 @@ internal class VerificationTransportToDevice( if (onDone != null) { onDone() } 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 + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 9714f42bb0..ea036f775b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.network import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.shouldBeRetried import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import org.greenrobot.eventbus.EventBus @@ -46,7 +47,7 @@ internal class Request(private val eventBus: EventBus?) { throw response.toFailure(eventBus) } } catch (exception: Throwable) { - if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) { + if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) { delay(currentDelay) currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay) return execute() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index ddb50628d6..0159dc7c3a 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -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.VerificationTransaction 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.IncomingRoomKeyRequest import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest 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.MXUsersDevicesMap 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.features.popup.DefaultVectorAlert import im.vector.riotx.features.popup.PopupAlertManager @@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat 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 Timber.v("## onSecretShareRequest() : Ignoring $request") request.ignore?.run() @@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) // can we get more info on this device? - session?.cryptoService()?.getDevicesList(object : MatrixCallback { - override fun onSuccess(data: DevicesListResponse) { - data.devices?.find { it.deviceId == deviceId }?.let { - postAlert(context, userId, deviceId, true, deviceInfo, it) - } ?: run { - postAlert(context, userId, deviceId, true, deviceInfo) - } - } - - override fun onFailure(failure: Throwable) { - postAlert(context, userId, deviceId, true, deviceInfo) - } - }) + session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let { + postAlert(context, userId, deviceId, true, deviceInfo, it) + } ?: kotlin.run { + postAlert(context, userId, deviceId, true, deviceInfo) + } } else { postAlert(context, userId, deviceId, false, deviceInfo) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index ccd3e6578a..2bd815b6df 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor( uid, context.getString(R.string.sas_incoming_request_notif_title), context.getString(R.string.sas_incoming_request_notif_content, name), - R.drawable.shield, + R.drawable.ic_shield_black, shouldBeDisplayedIn = { activity -> if (activity is VectorBaseActivity) { // TODO a bit too hugly :/ diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 6a2b7825ac..7a003c3722 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } } catch (failure: Throwable) { // Just ignore for now - Timber.v("## Failed to restore backup after SSSS recovery") + Timber.e(failure, "## Failed to restore backup after SSSS recovery") } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt index 76d0f8a2e2..b2638e65fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt @@ -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.group.model.GroupSummary 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.core.extensions.commitTransactionNow 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.popup.PopupAlertManager 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 kotlinx.android.synthetic.main.fragment_home_detail.* import timber.log.Timber @@ -86,43 +88,82 @@ class HomeDetailFragment @Inject constructor( switchDisplayMode(displayMode) } - unknownDeviceDetectorSharedViewModel.subscribe { - it.unknownSessions.invoke()?.let { unknownDevices -> - Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") - unknownDevices.forEachIndexed { index, deviceInfo -> - Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}") - } - val uid = "Newest_Device" - alertManager.cancelAlert(uid) - if (it.canCrossSign && unknownDevices.isNotEmpty()) { - val newest = unknownDevices.first().second - val user = unknownDevices.first().first - 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 ?: "") - } - dismissedAction = Runnable { - unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId ?: "") - ) - } - } - ) + unknownDeviceDetectorSharedViewModel.subscribe { state -> + state.unknownSessions.invoke()?.let { unknownDevices -> +// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}") + if (unknownDevices.firstOrNull()?.currentSessionTrust == true) { + val uid = "review_login" + alertManager.cancelAlert(uid) + val olderUnverified = unknownDevices.filter { !it.isNew } + val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo + if (newest != null) { + promptForNewUnknownDevices(uid, state, newest) + } else if (olderUnverified.isNotEmpty()) { + // In this case we prompt to go to settings to review logins + promptToReviewChanges(uid, state, olderUnverified.map { it.deviceInfo }) + } } } } } + 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) { + 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?) { groupSummary?.let { // Use GlideApp with activity context to avoid the glideRequests to be paused diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt new file mode 100644 index 0000000000..a05e9ee985 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -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> = 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(initialState) { + + sealed class Action : VectorViewModelAction { + data class IgnoreDevice(val deviceIds: List) : Action() + } + + private val ignoredDeviceList = ArrayList() + + 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, Optional, List>( + 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 { + + override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { + val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() + return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt deleted file mode 100644 index 12485500c1..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt +++ /dev/null @@ -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>> = Uninitialized, - val canCrossSign: Boolean = false -) : MvRxState - -class UnknownDeviceDetectorSharedViewModel( - session: Session, - private val vectorPreferences: VectorPreferences, - initialState: UnknownDevicesState) - : VectorViewModel(initialState) { - - sealed class Action : VectorViewModelAction { - data class IgnoreDevice(val deviceId: String) : Action() - } - - val ignoredDeviceList = ArrayList() - - 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 { - 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 { - override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? { - val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() - return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state) - } - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 0f19a1292a..2e2814cb78 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -96,7 +96,7 @@ class DefaultNavigator @Inject constructor( roomId = null, otherUserId = session.myUserId, transactionId = pr.transactionId - ).show(context.supportFragmentManager, "REQPOP") + ).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 6d00f02c97..0c73c0f5d3 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -26,6 +26,7 @@ import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import kotlinx.android.synthetic.main.activity_vector_settings.* import timber.log.Timber import javax.inject.Inject @@ -58,11 +59,16 @@ class VectorSettingsActivity : VectorBaseActivity(), if (isFirstCreation()) { // display the fragment 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) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> 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) } } @@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(), const val EXTRA_DIRECT_ACCESS_ROOT = 0 const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1 const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2 + const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index bb83658ae7..394587ea5d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( refreshCryptographyPreference(it) } // TODO Move to a ViewModel... - session.cryptoService().getDevicesList(object : MatrixCallback { + session.cryptoService().fetchDevicesList(object : MatrixCallback { override fun onSuccess(data: DevicesListResponse) { if (isAdded) { refreshCryptographyPreference(data.devices ?: emptyList()) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt index 8ed4d0ef64..5802bebf39 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -57,6 +57,9 @@ abstract class DeviceItem : VectorEpoxyModel() { @EpoxyAttribute var trusted: DeviceTrustLevel? = null + @EpoxyAttribute + var e2eCapable: Boolean = true + @EpoxyAttribute var legacyMode: Boolean = false @@ -79,7 +82,11 @@ abstract class DeviceItem : VectorEpoxyModel() { trusted ) - holder.trustIcon.setImageResource(shield) + if (e2eCapable) { + holder.trustIcon.setImageResource(shield) + } else { + holder.trustIcon.setImageDrawable(null) + } val detailedModeLabels = listOf( holder.displayNameLabelText, diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt index 47b64df927..53b95299e1 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceVerificationInfoBottomSheetViewModel.kt @@ -16,17 +16,14 @@ package im.vector.riotx.features.settings.devices import com.airbnb.mvrx.Async -import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.Loading 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 com.squareup.inject.assisted.Assisted 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.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo @@ -40,7 +37,7 @@ data class DeviceVerificationInfoBottomSheetViewState( val deviceInfo: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false, - val isMine : Boolean = false + val isMine: Boolean = false ) : MvRxState class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState, @@ -79,22 +76,18 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId ) } + setState { copy(deviceInfo = Loading()) } - session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback { - override fun onSuccess(data: DeviceInfo) { - setState { - copy(deviceInfo = Success(data)) - } - } - override fun onFailure(failure: Throwable) { - setState { - copy(deviceInfo = Fail(failure)) + session.rx().liveMyDeviceInfo() + .map { devices -> + devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId) + } + .execute { + copy(deviceInfo = it) } - } - }) } companion object : MvRxViewModelFactory { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt index 22dcc9cfc3..854f5ea895 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesAction.kt @@ -20,7 +20,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.riotx.core.platform.VectorViewModelAction sealed class DevicesAction : VectorViewModelAction { - object Retry : DevicesAction() + object Refresh : DevicesAction() data class Delete(val deviceId: String) : DevicesAction() data class Password(val password: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction() diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt index 817a3a3c53..1b08f23996 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -21,9 +21,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success 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.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.riotx.R import im.vector.riotx.core.epoxy.errorWithRetryItem @@ -73,20 +71,19 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } 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, - cryptoDevices: List?, + private fun buildDevicesList(devices: List, myDeviceId: String, legacyMode: Boolean, currentSessionCrossTrusted: Boolean) { devices - .firstOrNull() { - it.deviceId == myDeviceId - }?.let { deviceInfo -> - + .firstOrNull { + it.deviceInfo.deviceId == myDeviceId + }?.let { fullInfo -> + val deviceInfo = fullInfo.deviceInfo // Current device genericItemHeader { id("current") @@ -102,6 +99,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor detailedMode(vectorPreferences.developerMode()) deviceInfo(deviceInfo) currentDevice(true) + e2eCapable(true) itemClickAction { callback?.onDeviceClicked(deviceInfo) } trusted(DeviceTrustLevel(currentSessionCrossTrusted, true)) } @@ -129,12 +127,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor devices .filter { - it.deviceId != myDeviceId + it.deviceInfo.deviceId != myDeviceId } - // sort before display: most recent first - .sortByLastSeen() - .forEachIndexed { idx, deviceInfo -> - val isCurrentDevice = deviceInfo.deviceId == myDeviceId + .forEachIndexed { idx, deviceInfoPair -> + val deviceInfo = deviceInfoPair.deviceInfo + val cryptoInfo = deviceInfoPair.cryptoDeviceInfo deviceItem { id("device$idx") legacyMode(legacyMode) @@ -143,9 +140,10 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor colorProvider(colorProvider) detailedMode(vectorPreferences.developerMode()) deviceInfo(deviceInfo) - currentDevice(isCurrentDevice) + currentDevice(false) itemClickAction { callback?.onDeviceClicked(deviceInfo) } - trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.trustLevel) + e2eCapable(cryptoInfo != null) + trusted(cryptoInfo?.trustLevel) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt index 560e6f396d..a59bdc425d 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -29,6 +29,7 @@ import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject 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.failure.Failure 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.crypto.crosssigning.DeviceTrustLevel 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.DevicesListResponse import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.rx.rx 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 java.util.concurrent.TimeUnit data class DevicesViewState( val myDeviceId: String = "", - val devices: Async> = Uninitialized, - val cryptoDevices: Async> = Uninitialized, +// val devices: Async> = Uninitialized, +// val cryptoDevices: Async> = Uninitialized, + val devices: Async> = Uninitialized, // TODO Replace by isLoading boolean val request: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false ) : MvRxState +data class DeviceFullInfo( + val deviceInfo: DeviceInfo, + val cryptoDeviceInfo: CryptoDeviceInfo? +) class DevicesViewModel @AssistedInject constructor( @Assisted initialState: DevicesViewState, - private val session: Session, - private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider) - : VectorViewModel(initialState), VerificationService.Listener { + private val session: Session +) : VectorViewModel(initialState), VerificationService.Listener { @AssistedInject.Factory interface Factory { @@ -82,14 +88,37 @@ class DevicesViewModel @AssistedInject constructor( private var _currentDeviceId: String? = null private var _currentSession: String? = null + private val refreshPublisher: PublishSubject = PublishSubject.create() + init { setState { copy( hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null, - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() + accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), + myDeviceId = session.sessionParams.credentials.deviceId ?: "" ) } + + Observable.combineLatest, List, List>( + 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) .execute { copy( @@ -97,16 +126,38 @@ class DevicesViewModel @AssistedInject constructor( accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true ) } - - refreshDevicesList() session.cryptoService().verificationService().addListener(this) +// session.rx().liveMyDeviceInfo() +// .execute { +// copy( +// devices = it +// ) +// } + session.rx().liveUserCryptoDevices(session.myUserId) - .execute { - copy( - cryptoDevices = it - ) + .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() + +// 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() { @@ -116,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor( override fun transactionUpdated(tx: VerificationTransaction) { 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. * It can be any mobile devices, and any browsers. */ - private fun refreshDevicesList() { - if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) { - // 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 { - 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> { - override fun onSuccess(data: MXUsersDevicesMap) { - setState { - copy( - cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId)) - ) - } - } - }) - } else { - // Should not happen - } + private fun queryRefreshDevicesList() { + refreshPublisher.onNext(Unit) } override fun handle(action: DevicesAction) { return when (action) { - is DevicesAction.Retry -> refreshDevicesList() + is DevicesAction.Refresh -> queryRefreshDevicesList() is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Password -> handlePassword(action) is DevicesAction.Rename -> handleRename(action) @@ -210,10 +205,10 @@ class DevicesViewModel @AssistedInject constructor( } private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state -> - state.cryptoDevices.invoke() - ?.firstOrNull { it.deviceId == action.deviceId } + state.devices.invoke() + ?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId } ?.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 -> - val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId } + val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId } 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 - refreshDevicesList() + queryRefreshDevicesList() } override fun onFailure(failure: Throwable) { @@ -324,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } }) } @@ -353,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } override fun onFailure(failure: Throwable) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt index aedeb7d651..fa8ee16931 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -102,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor( override fun onResume() { super.onResume() - (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage) + viewModel.handle(DevicesAction.Refresh) } override fun onDeviceClicked(deviceInfo: DeviceInfo) { @@ -122,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor( // } override fun retry() { - viewModel.handle(DevicesAction.Retry) + viewModel.handle(DevicesAction.Refresh) } /** diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 3969c06af0..8023451a93 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2201,7 +2201,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming Refresh - Unverified login. Was this you? + New login. Was this you? Tap to review & verify Use this session to verify your new one, granting it access to encrypted messages. This wasn’t me diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3e23f61acf..e5d720c081 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -6,7 +6,8 @@ - + Review where you’re logged in + Verify all your sessions to ensure your account & messages are safe