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..c2e65823ba 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,7 @@ 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.session.sync.model.accountdata.UserAccountDataEvent import io.reactivex.Observable import io.reactivex.Single @@ -58,6 +59,13 @@ class RxSession(private val session: Session) { } } + fun liveMyDeviceInfo(): Observable> { + return session.cryptoService().getLiveMyDeviceInfo().asObservable() + .startWithCallable { + session.cryptoService().getMyDeviceInfo() + } + } + fun liveSyncState(): Observable { return session.getSyncStateLive().asObservable() } 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..25aca24966 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 getMyDeviceInfo() : List + fun getLiveMyDeviceInfo() : LiveData> fun getDeviceInfo(deviceId: String, callback: MatrixCallback) 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..a92ec14e2b 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.saveMyDeviceInfos(data.devices ?: emptyList()) + callback.onSuccess(data) + } + } } .executeBy(taskExecutor) } + override fun getLiveMyDeviceInfo(): LiveData> { + return cryptoStore.getLiveMyDeviceInfo() + } + + override fun getMyDeviceInfo(): List { + return cryptoStore.getMyDeviceInfo() + } + 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/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/CryptoDeviceInfo.kt index b124f7590e..52cd35b031 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 firsTimeSeenLocalTs: 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..53598a1bb5 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 getMyDeviceInfo() : List + fun getLiveMyDeviceInfo() : LiveData> + fun saveMyDeviceInfos(info: List) /** * Store the crypto algorithm for a room. * 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..942c51feb6 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) } } @@ -482,6 +490,52 @@ internal class RealmCryptoStore @Inject constructor( } } + override fun getMyDeviceInfo(): List { + return monarchy.fetchAllCopiedSync { + it.where() + }.map { + DeviceInfo( + deviceId = it.deviceId, + lastSeenIp = it.lastSeenIp, + lastSeenTs = it.lastSeenTs, + displayName = it.displayName + ) + } + } + + override fun getLiveMyDeviceInfo(): LiveData> { + return monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where() + }, + { entity -> + DeviceInfo( + deviceId = entity.deviceId, + lastSeenIp = entity.lastSeenIp, + lastSeenTs = entity.lastSeenTs, + displayName = entity.displayName + ) + } + ) + } + + override fun saveMyDeviceInfos(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..8bf278c3d8 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,8 @@ 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.MyDeviceLastSeenInfoEntity +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 +43,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 +53,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 +216,23 @@ 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) + .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..87a8f530ee 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 } - } + }, + firsTimeSeenLocalTs = 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/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..8b14aa024b 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()?.getMyDeviceInfo()?.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/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 6a2b7825ac..3bd0a8e2a1 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..3857cbc48e 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 -> + if (state.canCrossSign && unknownDevices.isNotEmpty()) { + Timber.v("## Detector - ${unknownDevices.size} Unknown sessions") + unknownDevices.forEachIndexed { index, detectionInfo -> + Timber.v("## Detector - #$index deviceId:${detectionInfo.deviceInfo.deviceId} lastSeenTs:${detectionInfo.deviceInfo.lastSeenTs} is New: ${detectionInfo.isNew}") + } + 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, user?.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?.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/UnknwonDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt index 12485500c1..c366a75c33 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/UnknwonDeviceDetectorSharedViewModel.kt @@ -22,27 +22,35 @@ 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.NoOpCancellable 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.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 io.reactivex.Observable +import io.reactivex.functions.BiFunction import timber.log.Timber import java.util.concurrent.TimeUnit data class UnknownDevicesState( - val unknownSessions: Async>> = Uninitialized, + val myMatrixItem: MatrixItem.UserItem? = null, + val unknownSessions: Async> = Uninitialized, val canCrossSign: Boolean = false ) : MvRxState +data class DeviceDetectionInfo( + val deviceInfo: DeviceInfo, + val isNew: Boolean +) + class UnknownDeviceDetectorSharedViewModel( session: Session, private val vectorPreferences: VectorPreferences, @@ -50,72 +58,97 @@ class UnknownDeviceDetectorSharedViewModel( : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { - data class IgnoreDevice(val deviceId: String) : Action() + data class IgnoreDevice(val deviceIds: List) : Action() } - val ignoredDeviceList = ArrayList() + private val ignoredDeviceList = ArrayList() init { + val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull { + it.deviceId == session.sessionParams.credentials.deviceId + }?.firsTimeSeenLocalTs ?: System.currentTimeMillis() + Timber.v("## Detector - Current Session first time seen $currentSessionTs") + ignoredDeviceList.addAll( vectorPreferences.getUnknownDeviceDismissedList().also { Timber.v("## Detector - Remembered ignored list $it") } ) - session.rx().liveUserCryptoDevices(session.myUserId) - .debounce(600, TimeUnit.MILLISECONDS) + + Observable.combineLatest, List, List>( + session.rx().liveUserCryptoDevices(session.myUserId), + session.rx().liveMyDeviceInfo(), + BiFunction { cryptoList, infoList -> + 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 }?.firsTimeSeenLocalTs ?: 0 + DeviceDetectionInfo( + deviceInfo, + deviceKnownSince > currentSessionTs + 60_000 // short window to avoid false positive + ) +// .also { +// Timber.v("## Detector - first seen Difference with ${deviceInfo.deviceId} is ${deviceKnownSince - currentSessionTs}") +// } + } + } + ) .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) + 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() + session.rx().liveCrossSigningInfo(session.myUserId) .execute { copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign()) } + + // 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 { - val updated = it.filter { it.second.deviceId != action.deviceId } + state.unknownSessions.invoke()?.let { detectedSessions -> + val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) } setState { copy(unknownSessions = Success(updated)) } } } - ignoredDeviceList.add(action.deviceId) - vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) } } } + 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/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/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..de2d1e75ef 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 @@ -83,7 +83,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor legacyMode: Boolean, currentSessionCrossTrusted: Boolean) { devices - .firstOrNull() { + .firstOrNull { it.deviceId == myDeviceId }?.let { deviceInfo -> 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..f948173649 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,14 +40,13 @@ 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.subjects.PublishSubject import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit data class DevicesViewState( val myDeviceId: String = "", @@ -60,9 +60,8 @@ data class DevicesViewState( 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,12 +81,15 @@ 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 ?: "" ) } session.rx().liveCrossSigningInfo(session.myUserId) @@ -97,16 +99,29 @@ 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 ) } + + 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 +131,7 @@ class DevicesViewModel @AssistedInject constructor( override fun transactionUpdated(tx: VerificationTransaction) { if (tx.state == VerificationTxState.Verified) { - refreshDevicesList() + queryRefreshDevicesList() } } @@ -125,75 +140,68 @@ 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() - ) - } + private fun queryRefreshDevicesList() { + refreshPublisher.onNext(Unit) - 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 - } +// 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().fetchDevicesList(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)) +// ) +// } +// +// +// } else { +// // Should not happen +// } } override fun handle(action: DevicesAction) { return when (action) { - is DevicesAction.Retry -> refreshDevicesList() - is DevicesAction.Delete -> handleDelete(action) - is DevicesAction.Password -> handlePassword(action) - is DevicesAction.Rename -> handleRename(action) - is DevicesAction.PromptRename -> handlePromptRename(action) - is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) - is DevicesAction.CompleteSecurity -> handleCompleteSecurity() + is DevicesAction.Refresh -> queryRefreshDevicesList() + is DevicesAction.Delete -> handleDelete(action) + is DevicesAction.Password -> handlePassword(action) + is DevicesAction.Rename -> handleRename(action) + is DevicesAction.PromptRename -> handlePromptRename(action) + is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) + is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) } @@ -253,7 +261,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } override fun onFailure(failure: Throwable) { @@ -324,7 +332,7 @@ class DevicesViewModel @AssistedInject constructor( ) } // force settings update - refreshDevicesList() + queryRefreshDevicesList() } }) } @@ -353,7 +361,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_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3e23f61acf..6b1a244f84 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 your other sessions