mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Merge pull request #1303 from vector-im/feature/xs_old_new_session_detection
Feature/xs old new session detection
This commit is contained in:
commit
43eb804b23
37 changed files with 577 additions and 329 deletions
|
@ -29,6 +29,7 @@ Improvements 🙌:
|
||||||
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
|
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
|
||||||
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
|
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
|
||||||
- Manage Session Settings / Cross Signing update (#1295)
|
- Manage Session Settings / Cross Signing update (#1295)
|
||||||
|
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix summary notification staying after "mark as read"
|
- Fix summary notification staying after "mark as read"
|
||||||
|
|
|
@ -31,6 +31,8 @@ import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.api.util.toOptional
|
import im.vector.matrix.android.api.util.toOptional
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
@ -58,6 +60,13 @@ class RxSession(private val session: Session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveMyDeviceInfo(): Observable<List<DeviceInfo>> {
|
||||||
|
return session.cryptoService().getLiveMyDevicesInfo().asObservable()
|
||||||
|
.startWithCallable {
|
||||||
|
session.cryptoService().getMyDevicesInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun liveSyncState(): Observable<SyncState> {
|
fun liveSyncState(): Observable<SyncState> {
|
||||||
return session.getSyncStateLive().asObservable()
|
return session.getSyncStateLive().asObservable()
|
||||||
}
|
}
|
||||||
|
@ -123,6 +132,13 @@ class RxSession(private val session: Session) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun liveCrossSigningPrivateKeys(): Observable<Optional<PrivateKeysInfo>> {
|
||||||
|
return session.cryptoService().crossSigningService().getLiveCrossSigningPrivateKeys().asObservable()
|
||||||
|
.startWithCallable {
|
||||||
|
session.cryptoService().crossSigningService().getCrossSigningPrivateKeys().toOptional()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
|
fun liveAccountData(types: Set<String>): Observable<List<UserAccountDataEvent>> {
|
||||||
return session.getLiveAccountDataEvents(types).asObservable()
|
return session.getLiveAccountDataEvents(types).asObservable()
|
||||||
.startWithCallable {
|
.startWithCallable {
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.failure
|
||||||
import im.vector.matrix.android.api.extensions.tryThis
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import java.io.IOException
|
||||||
import javax.net.ssl.HttpsURLConnection
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
|
||||||
fun Throwable.is401() =
|
fun Throwable.is401() =
|
||||||
|
@ -32,6 +33,7 @@ fun Throwable.isTokenError() =
|
||||||
|
|
||||||
fun Throwable.shouldBeRetried(): Boolean {
|
fun Throwable.shouldBeRetried(): Boolean {
|
||||||
return this is Failure.NetworkConnection
|
return this is Failure.NetworkConnection
|
||||||
|
|| this is IOException
|
||||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,9 @@ interface CryptoService {
|
||||||
|
|
||||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||||
|
|
||||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||||
|
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||||
|
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||||
|
|
||||||
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
|
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,8 @@ interface CrossSigningService {
|
||||||
|
|
||||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||||
|
|
||||||
|
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
|
||||||
|
|
||||||
fun canCrossSign(): Boolean
|
fun canCrossSign(): Boolean
|
||||||
|
|
||||||
fun trustUser(otherUserId: String,
|
fun trustUser(otherUserId: String,
|
||||||
|
|
|
@ -251,15 +251,33 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
return myDeviceInfoHolder.get().myDevice
|
return myDeviceInfoHolder.get().myDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
override fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||||
getDevicesTask
|
getDevicesTask
|
||||||
.configureWith {
|
.configureWith {
|
||||||
// this.executionThread = TaskThread.CRYPTO
|
// this.executionThread = TaskThread.CRYPTO
|
||||||
this.callback = callback
|
this.callback = object : MatrixCallback<DevicesListResponse> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
callback.onFailure(failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: DevicesListResponse) {
|
||||||
|
// Save in local DB
|
||||||
|
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList())
|
||||||
|
callback.onSuccess(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
|
||||||
|
return cryptoStore.getLiveMyDevicesInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMyDevicesInfo(): List<DeviceInfo> {
|
||||||
|
return cryptoStore.getMyDevicesInfo()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
||||||
getDeviceInfoTask
|
getDeviceInfoTask
|
||||||
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||||
|
@ -318,6 +336,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
internalStart(isInitialSync)
|
internalStart(isInitialSync)
|
||||||
}
|
}
|
||||||
|
// Just update
|
||||||
|
fetchDevicesList(NoOpMatrixCallback())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun internalStart(isInitialSync: Boolean) {
|
private suspend fun internalStart(isInitialSync: Boolean) {
|
||||||
|
|
|
@ -470,6 +470,10 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||||
return cryptoStore.getCrossSigningPrivateKeys()
|
return cryptoStore.getCrossSigningPrivateKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
|
||||||
|
return cryptoStore.getLiveCrossSigningPrivateKeys()
|
||||||
|
}
|
||||||
|
|
||||||
override fun canCrossSign(): Boolean {
|
override fun canCrossSign(): Boolean {
|
||||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||||
|
|
|
@ -29,7 +29,8 @@ data class CryptoDeviceInfo(
|
||||||
override val signatures: Map<String, Map<String, String>>? = null,
|
override val signatures: Map<String, Map<String, String>>? = null,
|
||||||
val unsigned: JsonDict? = null,
|
val unsigned: JsonDict? = null,
|
||||||
var trustLevel: DeviceTrustLevel? = null,
|
var trustLevel: DeviceTrustLevel? = null,
|
||||||
var isBlocked: Boolean = false
|
var isBlocked: Boolean = false,
|
||||||
|
val firstTimeSeenLocalTs: Long? = null
|
||||||
) : CryptoInfo {
|
) : CryptoInfo {
|
||||||
|
|
||||||
val isVerified: Boolean
|
val isVerified: Boolean
|
||||||
|
|
|
@ -61,20 +61,4 @@ internal object CryptoInfoMapper {
|
||||||
signatures = keyInfo.signatures
|
signatures = keyInfo.signatures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RestDeviceInfo.toCryptoModel(): CryptoDeviceInfo {
|
|
||||||
return map(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun CryptoDeviceInfo.toRest(): RestDeviceInfo {
|
|
||||||
return map(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun RestKeyInfo.toCryptoModel(): CryptoCrossSigningKey {
|
|
||||||
// return map(this)
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun CryptoCrossSigningKey.toRest(): RestKeyInfo {
|
|
||||||
return map(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
import org.matrix.olm.OlmAccount
|
import org.matrix.olm.OlmAccount
|
||||||
|
@ -218,6 +219,9 @@ internal interface IMXCryptoStore {
|
||||||
// TODO temp
|
// TODO temp
|
||||||
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
|
||||||
|
|
||||||
|
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||||
|
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||||
|
fun saveMyDevicesInfo(info: List<DeviceInfo>)
|
||||||
/**
|
/**
|
||||||
* Store the crypto algorithm for a room.
|
* Store the crypto algorithm for a room.
|
||||||
*
|
*
|
||||||
|
@ -405,6 +409,7 @@ internal interface IMXCryptoStore {
|
||||||
fun storeUSKPrivateKey(usk: String?)
|
fun storeUSKPrivateKey(usk: String?)
|
||||||
|
|
||||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||||
|
fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
|
||||||
|
|
||||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||||
|
|
|
@ -40,6 +40,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
import im.vector.matrix.android.internal.crypto.model.toEntity
|
import im.vector.matrix.android.internal.crypto.model.toEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
|
@ -59,6 +60,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossiping
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
|
@ -287,10 +289,16 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
UserEntity.getOrCreate(realm, userId)
|
UserEntity.getOrCreate(realm, userId)
|
||||||
.let { u ->
|
.let { u ->
|
||||||
// Add the devices
|
// Add the devices
|
||||||
|
val currentKnownDevices = u.devices.toList()
|
||||||
|
val new = devices.map { entry -> entry.value.toEntity() }
|
||||||
|
new.forEach { entity ->
|
||||||
|
// Maintain first time seen
|
||||||
|
val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
|
||||||
|
entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
|
||||||
|
realm.insertOrUpdate(entity)
|
||||||
|
}
|
||||||
// Ensure all other devices are deleted
|
// Ensure all other devices are deleted
|
||||||
u.devices.deleteAllFromRealm()
|
u.devices.deleteAllFromRealm()
|
||||||
val new = devices.map { entry -> entry.value.toEntity() }
|
|
||||||
new.forEach { realm.insertOrUpdate(it) }
|
|
||||||
u.devices.addAll(new)
|
u.devices.addAll(new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -358,6 +366,25 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>> {
|
||||||
|
val liveData = monarchy.findAllMappedWithChanges(
|
||||||
|
{ realm: Realm ->
|
||||||
|
realm
|
||||||
|
.where<CryptoMetadataEntity>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PrivateKeysInfo(
|
||||||
|
master = it.xSignMasterPrivateKey,
|
||||||
|
selfSigned = it.xSignSelfSignedPrivateKey,
|
||||||
|
user = it.xSignUserPrivateKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Transformations.map(liveData) {
|
||||||
|
it.firstOrNull().toOptional()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
|
||||||
doRealmTransaction(realmConfiguration) { realm ->
|
doRealmTransaction(realmConfiguration) { realm ->
|
||||||
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
|
||||||
|
@ -482,6 +509,52 @@ internal class RealmCryptoStore @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMyDevicesInfo(): List<DeviceInfo> {
|
||||||
|
return monarchy.fetchAllCopiedSync {
|
||||||
|
it.where<MyDeviceLastSeenInfoEntity>()
|
||||||
|
}.map {
|
||||||
|
DeviceInfo(
|
||||||
|
deviceId = it.deviceId,
|
||||||
|
lastSeenIp = it.lastSeenIp,
|
||||||
|
lastSeenTs = it.lastSeenTs,
|
||||||
|
displayName = it.displayName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
|
||||||
|
return monarchy.findAllMappedWithChanges(
|
||||||
|
{ realm: Realm ->
|
||||||
|
realm.where<MyDeviceLastSeenInfoEntity>()
|
||||||
|
},
|
||||||
|
{ entity ->
|
||||||
|
DeviceInfo(
|
||||||
|
deviceId = entity.deviceId,
|
||||||
|
lastSeenIp = entity.lastSeenIp,
|
||||||
|
lastSeenTs = entity.lastSeenTs,
|
||||||
|
displayName = entity.displayName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveMyDevicesInfo(info: List<DeviceInfo>) {
|
||||||
|
val entities = info.map {
|
||||||
|
MyDeviceLastSeenInfoEntity(
|
||||||
|
lastSeenTs = it.lastSeenTs,
|
||||||
|
lastSeenIp = it.lastSeenIp,
|
||||||
|
displayName = it.displayName,
|
||||||
|
deviceId = it.deviceId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
monarchy.writeAsync { realm ->
|
||||||
|
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
|
||||||
|
entities.forEach {
|
||||||
|
realm.insertOrUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
|
override fun storeRoomAlgorithm(roomId: String, algorithm: String) {
|
||||||
doRealmTransaction(realmConfiguration) {
|
doRealmTransaction(realmConfiguration) {
|
||||||
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
|
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.store.db
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
import im.vector.matrix.android.api.util.JsonDict
|
import im.vector.matrix.android.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
import im.vector.matrix.android.internal.crypto.store.db.mapper.CrossSigningKeysMapper
|
||||||
|
@ -27,6 +28,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityF
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntityFields
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntityFields
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields
|
||||||
|
@ -40,7 +42,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
|
|
||||||
// Version 1L added Cross Signing info persistence
|
// Version 1L added Cross Signing info persistence
|
||||||
companion object {
|
companion object {
|
||||||
const val CRYPTO_STORE_SCHEMA_VERSION = 4L
|
const val CRYPTO_STORE_SCHEMA_VERSION = 5L
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||||
|
@ -50,6 +52,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
if (oldVersion <= 1) migrateTo2(realm)
|
if (oldVersion <= 1) migrateTo2(realm)
|
||||||
if (oldVersion <= 2) migrateTo3(realm)
|
if (oldVersion <= 2) migrateTo3(realm)
|
||||||
if (oldVersion <= 3) migrateTo4(realm)
|
if (oldVersion <= 3) migrateTo4(realm)
|
||||||
|
if (oldVersion <= 4) migrateTo5(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
|
@ -212,4 +215,24 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateTo5(realm: DynamicRealm) {
|
||||||
|
realm.schema.create("MyDeviceLastSeenInfoEntity")
|
||||||
|
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
|
||||||
|
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
|
||||||
|
.addField(MyDeviceLastSeenInfoEntityFields.DISPLAY_NAME, String::class.java)
|
||||||
|
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_IP, String::class.java)
|
||||||
|
.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, Long::class.java)
|
||||||
|
.setNullable(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_TS, true)
|
||||||
|
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
realm.schema.get("DeviceInfoEntity")
|
||||||
|
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
|
||||||
|
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
|
||||||
|
?.transform { deviceInfoEntity ->
|
||||||
|
tryThis {
|
||||||
|
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEnt
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||||
|
@ -48,6 +49,7 @@ import io.realm.annotations.RealmModule
|
||||||
TrustLevelEntity::class,
|
TrustLevelEntity::class,
|
||||||
GossipingEventEntity::class,
|
GossipingEventEntity::class,
|
||||||
IncomingGossipingRequestEntity::class,
|
IncomingGossipingRequestEntity::class,
|
||||||
OutgoingGossipingRequestEntity::class
|
OutgoingGossipingRequestEntity::class,
|
||||||
|
MyDeviceLastSeenInfoEntity::class
|
||||||
])
|
])
|
||||||
internal class RealmCryptoStoreModule
|
internal class RealmCryptoStoreModule
|
||||||
|
|
|
@ -104,7 +104,8 @@ object CryptoMapper {
|
||||||
Timber.e(failure)
|
Timber.e(failure)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
firstTimeSeenLocalTs = deviceInfoEntity.firstTimeSeenLocalTs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,12 @@ internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "",
|
||||||
var keysMapJson: String? = null,
|
var keysMapJson: String? = null,
|
||||||
var signatureMapJson: String? = null,
|
var signatureMapJson: String? = null,
|
||||||
var unsignedMapJson: String? = null,
|
var unsignedMapJson: String? = null,
|
||||||
var trustLevelEntity: TrustLevelEntity? = null
|
var trustLevelEntity: TrustLevelEntity? = null,
|
||||||
|
/**
|
||||||
|
* We use that to make distinction between old devices (there before mine)
|
||||||
|
* and new ones. Used for example to detect new unverified login
|
||||||
|
*/
|
||||||
|
var firstTimeSeenLocalTs: Long? = null
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
// // Deserialize data
|
// // Deserialize data
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -52,6 +52,8 @@ internal class DefaultSendToDeviceTask @Inject constructor(
|
||||||
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
|
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
|
||||||
sendToDeviceBody
|
sendToDeviceBody
|
||||||
)
|
)
|
||||||
|
isRetryable = true
|
||||||
|
maxRetryCount = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
|
|
||||||
override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
|
override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
|
||||||
Timber.v("## SAS O: onVerificationAccept id:$transactionId")
|
Timber.v("## SAS O: onVerificationAccept id:$transactionId")
|
||||||
if (state != VerificationTxState.Started) {
|
if (state != VerificationTxState.Started && state != VerificationTxState.SendingStart) {
|
||||||
Timber.e("## SAS O: received accept request from invalid state $state")
|
Timber.e("## SAS O: received accept request from invalid state $state")
|
||||||
cancel(CancelCode.UnexpectedMessage)
|
cancel(CancelCode.UnexpectedMessage)
|
||||||
return
|
return
|
||||||
|
@ -148,7 +148,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
||||||
|| !KNOWN_HASHES.contains(accept.hash)
|
|| !KNOWN_HASHES.contains(accept.hash)
|
||||||
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|
||||||
|| accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) {
|
|| accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) {
|
||||||
Timber.e("## SAS O: received accept request from invalid state")
|
Timber.e("## SAS O: received invalid accept")
|
||||||
cancel(CancelCode.UnknownMethod)
|
cancel(CancelCode.UnknownMethod)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@ internal class VerificationTransportToDevice(
|
||||||
onDone: (() -> Unit)?) {
|
onDone: (() -> Unit)?) {
|
||||||
Timber.d("## SAS sending msg type $type")
|
Timber.d("## SAS sending msg type $type")
|
||||||
Timber.v("## SAS sending msg info $verificationInfo")
|
Timber.v("## SAS sending msg info $verificationInfo")
|
||||||
|
val stateBeforeCall = tx?.state
|
||||||
val tx = tx ?: return
|
val tx = tx ?: return
|
||||||
val contentMap = MXUsersDevicesMap<Any>()
|
val contentMap = MXUsersDevicesMap<Any>()
|
||||||
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
|
val toSendToDeviceObject = verificationInfo.toSendToDeviceObject()
|
||||||
|
@ -132,9 +133,13 @@ internal class VerificationTransportToDevice(
|
||||||
if (onDone != null) {
|
if (onDone != null) {
|
||||||
onDone()
|
onDone()
|
||||||
} else {
|
} else {
|
||||||
|
// 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
|
tx.state = nextState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state")
|
Timber.e("## SAS verification [$tx.transactionId] failed to send toDevice in state : $tx.state")
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.network
|
package im.vector.matrix.android.internal.network
|
||||||
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.api.failure.shouldBeRetried
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
@ -46,7 +47,7 @@ internal class Request<DATA>(private val eventBus: EventBus?) {
|
||||||
throw response.toFailure(eventBus)
|
throw response.toFailure(eventBus)
|
||||||
}
|
}
|
||||||
} catch (exception: Throwable) {
|
} catch (exception: Throwable) {
|
||||||
if (isRetryable && currentRetryCount++ < maxRetryCount && exception is IOException) {
|
if (isRetryable && currentRetryCount++ < maxRetryCount && exception.shouldBeRetried()) {
|
||||||
delay(currentDelay)
|
delay(currentDelay)
|
||||||
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
currentDelay = (currentDelay * 2L).coerceAtMost(maxDelay)
|
||||||
return execute()
|
return execute()
|
||||||
|
|
|
@ -27,14 +27,13 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
import im.vector.matrix.android.internal.crypto.IncomingRequestCancellation
|
||||||
|
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
import im.vector.matrix.android.internal.crypto.IncomingSecretShareRequest
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.features.popup.DefaultVectorAlert
|
import im.vector.riotx.features.popup.DefaultVectorAlert
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
|
@ -75,7 +74,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
|
||||||
session = null
|
session = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSecretShareRequest(request: IncomingSecretShareRequest) : Boolean {
|
override fun onSecretShareRequest(request: IncomingSecretShareRequest): Boolean {
|
||||||
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
|
// By default riotX will not prompt if the SDK has decided that the request should not be fulfilled
|
||||||
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
Timber.v("## onSecretShareRequest() : Ignoring $request")
|
||||||
request.ignore?.run()
|
request.ignore?.run()
|
||||||
|
@ -124,19 +123,11 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
|
||||||
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
deviceInfo.trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
|
||||||
|
|
||||||
// can we get more info on this device?
|
// can we get more info on this device?
|
||||||
session?.cryptoService()?.getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
session?.cryptoService()?.getMyDevicesInfo()?.firstOrNull { it.deviceId == deviceId }?.let {
|
||||||
override fun onSuccess(data: DevicesListResponse) {
|
|
||||||
data.devices?.find { it.deviceId == deviceId }?.let {
|
|
||||||
postAlert(context, userId, deviceId, true, deviceInfo, it)
|
postAlert(context, userId, deviceId, true, deviceInfo, it)
|
||||||
} ?: run {
|
} ?: kotlin.run {
|
||||||
postAlert(context, userId, deviceId, true, deviceInfo)
|
postAlert(context, userId, deviceId, true, deviceInfo)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
postAlert(context, userId, deviceId, true, deviceInfo)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
postAlert(context, userId, deviceId, false, deviceInfo)
|
postAlert(context, userId, deviceId, false, deviceInfo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
|
||||||
uid,
|
uid,
|
||||||
context.getString(R.string.sas_incoming_request_notif_title),
|
context.getString(R.string.sas_incoming_request_notif_title),
|
||||||
context.getString(R.string.sas_incoming_request_notif_content, name),
|
context.getString(R.string.sas_incoming_request_notif_content, name),
|
||||||
R.drawable.shield,
|
R.drawable.ic_shield_black,
|
||||||
shouldBeDisplayedIn = { activity ->
|
shouldBeDisplayedIn = { activity ->
|
||||||
if (activity is VectorBaseActivity) {
|
if (activity is VectorBaseActivity) {
|
||||||
// TODO a bit too hugly :/
|
// TODO a bit too hugly :/
|
||||||
|
|
|
@ -423,7 +423,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
// Just ignore for now
|
// Just ignore for now
|
||||||
Timber.v("## Failed to restore backup after SSSS recovery")
|
Timber.e(failure, "## Failed to restore backup after SSSS recovery")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationMenuView
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.commitTransactionNow
|
import im.vector.riotx.core.extensions.commitTransactionNow
|
||||||
import im.vector.riotx.core.glide.GlideApp
|
import im.vector.riotx.core.glide.GlideApp
|
||||||
|
@ -42,6 +43,7 @@ import im.vector.riotx.features.home.room.list.RoomListParams
|
||||||
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
||||||
import im.vector.riotx.features.popup.PopupAlertManager
|
import im.vector.riotx.features.popup.PopupAlertManager
|
||||||
import im.vector.riotx.features.popup.VerificationVectorAlert
|
import im.vector.riotx.features.popup.VerificationVectorAlert
|
||||||
|
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -86,17 +88,27 @@ class HomeDetailFragment @Inject constructor(
|
||||||
switchDisplayMode(displayMode)
|
switchDisplayMode(displayMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
unknownDeviceDetectorSharedViewModel.subscribe {
|
unknownDeviceDetectorSharedViewModel.subscribe { state ->
|
||||||
it.unknownSessions.invoke()?.let { unknownDevices ->
|
state.unknownSessions.invoke()?.let { unknownDevices ->
|
||||||
Timber.v("## Detector - ${unknownDevices.size} Unknown sessions")
|
// Timber.v("## Detector Triggerred in fragment - ${unknownDevices.firstOrNull()}")
|
||||||
unknownDevices.forEachIndexed { index, deviceInfo ->
|
if (unknownDevices.firstOrNull()?.currentSessionTrust == true) {
|
||||||
Timber.v("## Detector - #$index deviceId:${deviceInfo.second.deviceId} lastSeenTs:${deviceInfo.second.lastSeenTs}")
|
val uid = "review_login"
|
||||||
}
|
|
||||||
val uid = "Newest_Device"
|
|
||||||
alertManager.cancelAlert(uid)
|
alertManager.cancelAlert(uid)
|
||||||
if (it.canCrossSign && unknownDevices.isNotEmpty()) {
|
val olderUnverified = unknownDevices.filter { !it.isNew }
|
||||||
val newest = unknownDevices.first().second
|
val newest = unknownDevices.firstOrNull { it.isNew }?.deviceInfo
|
||||||
val user = unknownDevices.first().first
|
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(
|
alertManager.postVectorAlert(
|
||||||
VerificationVectorAlert(
|
VerificationVectorAlert(
|
||||||
uid = uid,
|
uid = uid,
|
||||||
|
@ -110,17 +122,46 @@ class HomeDetailFragment @Inject constructor(
|
||||||
(weakCurrentActivity?.get() as? VectorBaseActivity)
|
(weakCurrentActivity?.get() as? VectorBaseActivity)
|
||||||
?.navigator
|
?.navigator
|
||||||
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
|
||||||
|
unknownDeviceDetectorSharedViewModel.handle(
|
||||||
|
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dismissedAction = Runnable {
|
dismissedAction = Runnable {
|
||||||
unknownDeviceDetectorSharedViewModel.handle(
|
unknownDeviceDetectorSharedViewModel.handle(
|
||||||
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId ?: "")
|
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun promptToReviewChanges(uid: String, state: UnknownDevicesState, oldUnverified: List<DeviceInfo>) {
|
||||||
|
val user = state.myMatrixItem
|
||||||
|
alertManager.postVectorAlert(
|
||||||
|
VerificationVectorAlert(
|
||||||
|
uid = uid,
|
||||||
|
title = getString(R.string.review_logins),
|
||||||
|
description = getString(R.string.verify_other_sessions),
|
||||||
|
iconId = R.drawable.ic_shield_warning
|
||||||
|
).apply {
|
||||||
|
matrixItem = user
|
||||||
|
colorInt = ContextCompat.getColor(requireActivity(), R.color.riotx_accent)
|
||||||
|
contentAction = Runnable {
|
||||||
|
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||||
|
// mark as ignored to avoid showing it again
|
||||||
|
unknownDeviceDetectorSharedViewModel.handle(
|
||||||
|
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
|
||||||
|
)
|
||||||
|
it.navigator.openSettings(it, EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dismissedAction = Runnable {
|
||||||
|
unknownDeviceDetectorSharedViewModel.handle(
|
||||||
|
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(oldUnverified.mapNotNull { it.deviceId })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onGroupChange(groupSummary: GroupSummary?) {
|
private fun onGroupChange(groupSummary: GroupSummary?) {
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.home
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.Optional
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
|
import im.vector.riotx.core.di.HasScreenInjector
|
||||||
|
import im.vector.riotx.core.platform.EmptyViewEvents
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.functions.Function3
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
data class UnknownDevicesState(
|
||||||
|
val myMatrixItem: MatrixItem.UserItem? = null,
|
||||||
|
val unknownSessions: Async<List<DeviceDetectionInfo>> = Uninitialized
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
data class DeviceDetectionInfo(
|
||||||
|
val deviceInfo: DeviceInfo,
|
||||||
|
val isNew: Boolean,
|
||||||
|
val currentSessionTrust: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
class UnknownDeviceDetectorSharedViewModel(
|
||||||
|
session: Session,
|
||||||
|
private val vectorPreferences: VectorPreferences,
|
||||||
|
initialState: UnknownDevicesState)
|
||||||
|
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
|
sealed class Action : VectorViewModelAction {
|
||||||
|
data class IgnoreDevice(val deviceIds: List<String>) : Action()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ignoredDeviceList = ArrayList<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId).firstOrNull {
|
||||||
|
it.deviceId == session.sessionParams.credentials.deviceId
|
||||||
|
}?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
|
||||||
|
Timber.v("## Detector - Current Session first time seen $currentSessionTs")
|
||||||
|
|
||||||
|
ignoredDeviceList.addAll(
|
||||||
|
vectorPreferences.getUnknownDeviceDismissedList().also {
|
||||||
|
Timber.v("## Detector - Remembered ignored list $it")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, Optional<PrivateKeysInfo>, List<DeviceDetectionInfo>>(
|
||||||
|
session.rx().liveUserCryptoDevices(session.myUserId),
|
||||||
|
session.rx().liveMyDeviceInfo(),
|
||||||
|
session.rx().liveCrossSigningPrivateKeys(),
|
||||||
|
Function3 { cryptoList, infoList, pInfo ->
|
||||||
|
// Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}")
|
||||||
|
// Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}")
|
||||||
|
infoList
|
||||||
|
.filter { info ->
|
||||||
|
// filter verified session, by checking the crypto device info
|
||||||
|
cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse()
|
||||||
|
}
|
||||||
|
// filter out ignored devices
|
||||||
|
.filter { !ignoredDeviceList.contains(it.deviceId) }
|
||||||
|
.sortedByDescending { it.lastSeenTs }
|
||||||
|
.map { deviceInfo ->
|
||||||
|
val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0
|
||||||
|
DeviceDetectionInfo(
|
||||||
|
deviceInfo,
|
||||||
|
deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive,
|
||||||
|
pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.execute { async ->
|
||||||
|
// Timber.v("## Detector trigger passed distinct")
|
||||||
|
copy(
|
||||||
|
myMatrixItem = session.getUser(session.myUserId)?.toMatrixItem(),
|
||||||
|
unknownSessions = async
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||||
|
.distinct()
|
||||||
|
.throttleLast(5_000, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribe {
|
||||||
|
// If we have a new crypto device change, we might want to trigger refresh of device info
|
||||||
|
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||||
|
}.disposeOnClear()
|
||||||
|
|
||||||
|
// trigger a refresh of lastSeen / last Ip
|
||||||
|
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: Action) {
|
||||||
|
when (action) {
|
||||||
|
is Action.IgnoreDevice -> {
|
||||||
|
ignoredDeviceList.addAll(action.deviceIds)
|
||||||
|
// local echo
|
||||||
|
withState { state ->
|
||||||
|
state.unknownSessions.invoke()?.let { detectedSessions ->
|
||||||
|
val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) }
|
||||||
|
setState {
|
||||||
|
copy(unknownSessions = Success(updated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
|
||||||
|
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
|
||||||
|
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||||
|
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,124 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package im.vector.riotx.features.home
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
|
||||||
import com.airbnb.mvrx.MvRxState
|
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
|
||||||
import im.vector.matrix.android.api.session.Session
|
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
|
||||||
import im.vector.matrix.android.api.util.NoOpCancellable
|
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
|
||||||
import im.vector.matrix.rx.rx
|
|
||||||
import im.vector.matrix.rx.singleBuilder
|
|
||||||
import im.vector.riotx.core.di.HasScreenInjector
|
|
||||||
import im.vector.riotx.core.platform.EmptyViewEvents
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
|
||||||
import im.vector.riotx.features.settings.VectorPreferences
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
data class UnknownDevicesState(
|
|
||||||
val unknownSessions: Async<List<Pair<MatrixItem?, DeviceInfo>>> = Uninitialized,
|
|
||||||
val canCrossSign: Boolean = false
|
|
||||||
) : MvRxState
|
|
||||||
|
|
||||||
class UnknownDeviceDetectorSharedViewModel(
|
|
||||||
session: Session,
|
|
||||||
private val vectorPreferences: VectorPreferences,
|
|
||||||
initialState: UnknownDevicesState)
|
|
||||||
: VectorViewModel<UnknownDevicesState, UnknownDeviceDetectorSharedViewModel.Action, EmptyViewEvents>(initialState) {
|
|
||||||
|
|
||||||
sealed class Action : VectorViewModelAction {
|
|
||||||
data class IgnoreDevice(val deviceId: String) : Action()
|
|
||||||
}
|
|
||||||
|
|
||||||
val ignoredDeviceList = ArrayList<String>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
|
|
||||||
ignoredDeviceList.addAll(
|
|
||||||
vectorPreferences.getUnknownDeviceDismissedList().also {
|
|
||||||
Timber.v("## Detector - Remembered ignored list $it")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
session.rx().liveUserCryptoDevices(session.myUserId)
|
|
||||||
.debounce(600, TimeUnit.MILLISECONDS)
|
|
||||||
.distinct()
|
|
||||||
.switchMap { deviceList ->
|
|
||||||
Timber.v("## Detector - ============================")
|
|
||||||
Timber.v("## Detector - Crypto device update ${deviceList.map { "${it.deviceId} : ${it.isVerified}" }}")
|
|
||||||
singleBuilder<DevicesListResponse> {
|
|
||||||
session.cryptoService().getDevicesList(it)
|
|
||||||
NoOpCancellable
|
|
||||||
}.map { resp ->
|
|
||||||
// Timber.v("## Detector - Device Infos ${resp.devices?.map { "${it.deviceId} : lastSeen:${it.lastSeenTs}" }}")
|
|
||||||
resp.devices?.filter { info ->
|
|
||||||
deviceList.firstOrNull { info.deviceId == it.deviceId }?.let {
|
|
||||||
!it.isVerified
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
?.sortedByDescending { it.lastSeenTs }
|
|
||||||
?.map {
|
|
||||||
session.getUser(it.user_id ?: "")?.toMatrixItem() to it
|
|
||||||
}
|
|
||||||
?.filter { !ignoredDeviceList.contains(it.second.deviceId) }
|
|
||||||
?: emptyList()
|
|
||||||
}
|
|
||||||
.toObservable()
|
|
||||||
}
|
|
||||||
.execute { async ->
|
|
||||||
copy(unknownSessions = async)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.rx().liveCrossSigningInfo(session.myUserId)
|
|
||||||
.execute {
|
|
||||||
copy(canCrossSign = session.cryptoService().crossSigningService().canCrossSign())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handle(action: Action) {
|
|
||||||
when (action) {
|
|
||||||
is Action.IgnoreDevice -> {
|
|
||||||
// local echo
|
|
||||||
withState { state ->
|
|
||||||
state.unknownSessions.invoke()?.let {
|
|
||||||
val updated = it.filter { it.second.deviceId != action.deviceId }
|
|
||||||
setState {
|
|
||||||
copy(unknownSessions = Success(updated))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ignoredDeviceList.add(action.deviceId)
|
|
||||||
vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<UnknownDeviceDetectorSharedViewModel, UnknownDevicesState> {
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: UnknownDevicesState): UnknownDeviceDetectorSharedViewModel? {
|
|
||||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
|
||||||
return UnknownDeviceDetectorSharedViewModel(session, VectorPreferences(viewModelContext.activity()), state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -96,7 +96,7 @@ class DefaultNavigator @Inject constructor(
|
||||||
roomId = null,
|
roomId = null,
|
||||||
otherUserId = session.myUserId,
|
otherUserId = session.myUserId,
|
||||||
transactionId = pr.transactionId
|
transactionId = pr.transactionId
|
||||||
).show(context.supportFragmentManager, "REQPOP")
|
).show(context.supportFragmentManager, VerificationBottomSheet.WAITING_SELF_VERIF_TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.replaceFragment
|
import im.vector.riotx.core.extensions.replaceFragment
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||||
import kotlinx.android.synthetic.main.activity_vector_settings.*
|
import kotlinx.android.synthetic.main.activity_vector_settings.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -62,6 +63,11 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
||||||
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
|
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
||||||
|
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS ->
|
||||||
|
replaceFragment(R.id.vector_settings_page,
|
||||||
|
VectorSettingsDevicesFragment::class.java,
|
||||||
|
null,
|
||||||
|
FRAGMENT_TAG)
|
||||||
else ->
|
else ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
||||||
}
|
}
|
||||||
|
@ -130,6 +136,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
const val EXTRA_DIRECT_ACCESS_ROOT = 0
|
const val EXTRA_DIRECT_ACCESS_ROOT = 0
|
||||||
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
|
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
|
||||||
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
|
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY = 2
|
||||||
|
const val EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS = 3
|
||||||
|
|
||||||
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
|
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,7 +412,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
refreshCryptographyPreference(it)
|
refreshCryptographyPreference(it)
|
||||||
}
|
}
|
||||||
// TODO Move to a ViewModel...
|
// TODO Move to a ViewModel...
|
||||||
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
session.cryptoService().fetchDevicesList(object : MatrixCallback<DevicesListResponse> {
|
||||||
override fun onSuccess(data: DevicesListResponse) {
|
override fun onSuccess(data: DevicesListResponse) {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
refreshCryptographyPreference(data.devices ?: emptyList())
|
refreshCryptographyPreference(data.devices ?: emptyList())
|
||||||
|
|
|
@ -57,6 +57,9 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var trusted: DeviceTrustLevel? = null
|
var trusted: DeviceTrustLevel? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var e2eCapable: Boolean = true
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var legacyMode: Boolean = false
|
var legacyMode: Boolean = false
|
||||||
|
|
||||||
|
@ -79,7 +82,11 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
|
||||||
trusted
|
trusted
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (e2eCapable) {
|
||||||
holder.trustIcon.setImageResource(shield)
|
holder.trustIcon.setImageResource(shield)
|
||||||
|
} else {
|
||||||
|
holder.trustIcon.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
|
||||||
val detailedModeLabels = listOf(
|
val detailedModeLabels = listOf(
|
||||||
holder.displayNameLabelText,
|
holder.displayNameLabelText,
|
||||||
|
|
|
@ -16,17 +16,14 @@
|
||||||
package im.vector.riotx.features.settings.devices
|
package im.vector.riotx.features.settings.devices
|
||||||
|
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.Fail
|
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.MvRxState
|
import com.airbnb.mvrx.MvRxState
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
@ -40,7 +37,7 @@ data class DeviceVerificationInfoBottomSheetViewState(
|
||||||
val deviceInfo: Async<DeviceInfo> = Uninitialized,
|
val deviceInfo: Async<DeviceInfo> = Uninitialized,
|
||||||
val hasAccountCrossSigning: Boolean = false,
|
val hasAccountCrossSigning: Boolean = false,
|
||||||
val accountCrossSigningIsTrusted: Boolean = false,
|
val accountCrossSigningIsTrusted: Boolean = false,
|
||||||
val isMine : Boolean = false
|
val isMine: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
|
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
|
||||||
|
@ -79,22 +76,18 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
|
||||||
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
|
isMine = it.invoke()?.deviceId == session.sessionParams.credentials.deviceId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(deviceInfo = Loading())
|
copy(deviceInfo = Loading())
|
||||||
}
|
}
|
||||||
session.cryptoService().getDeviceInfo(deviceId, object : MatrixCallback<DeviceInfo> {
|
|
||||||
override fun onSuccess(data: DeviceInfo) {
|
|
||||||
setState {
|
|
||||||
copy(deviceInfo = Success(data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
session.rx().liveMyDeviceInfo()
|
||||||
setState {
|
.map { devices ->
|
||||||
copy(deviceInfo = Fail(failure))
|
devices.firstOrNull { it.deviceId == deviceId } ?: DeviceInfo(deviceId = deviceId)
|
||||||
}
|
}
|
||||||
|
.execute {
|
||||||
|
copy(deviceInfo = it)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {
|
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {
|
||||||
|
|
|
@ -20,7 +20,7 @@ import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class DevicesAction : VectorViewModelAction {
|
sealed class DevicesAction : VectorViewModelAction {
|
||||||
object Retry : DevicesAction()
|
object Refresh : DevicesAction()
|
||||||
data class Delete(val deviceId: String) : DevicesAction()
|
data class Delete(val deviceId: String) : DevicesAction()
|
||||||
data class Password(val password: String) : DevicesAction()
|
data class Password(val password: String) : DevicesAction()
|
||||||
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
data class Rename(val deviceId: String, val newName: String) : DevicesAction()
|
||||||
|
|
|
@ -21,9 +21,7 @@ import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.api.extensions.sortByLastSeen
|
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
||||||
|
@ -73,20 +71,19 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||||
listener { callback?.retry() }
|
listener { callback?.retry() }
|
||||||
}
|
}
|
||||||
is Success ->
|
is Success ->
|
||||||
buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
|
buildDevicesList(devices(), state.myDeviceId, !state.hasAccountCrossSigning, state.accountCrossSigningIsTrusted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildDevicesList(devices: List<DeviceInfo>,
|
private fun buildDevicesList(devices: List<DeviceFullInfo>,
|
||||||
cryptoDevices: List<CryptoDeviceInfo>?,
|
|
||||||
myDeviceId: String,
|
myDeviceId: String,
|
||||||
legacyMode: Boolean,
|
legacyMode: Boolean,
|
||||||
currentSessionCrossTrusted: Boolean) {
|
currentSessionCrossTrusted: Boolean) {
|
||||||
devices
|
devices
|
||||||
.firstOrNull() {
|
.firstOrNull {
|
||||||
it.deviceId == myDeviceId
|
it.deviceInfo.deviceId == myDeviceId
|
||||||
}?.let { deviceInfo ->
|
}?.let { fullInfo ->
|
||||||
|
val deviceInfo = fullInfo.deviceInfo
|
||||||
// Current device
|
// Current device
|
||||||
genericItemHeader {
|
genericItemHeader {
|
||||||
id("current")
|
id("current")
|
||||||
|
@ -102,6 +99,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||||
detailedMode(vectorPreferences.developerMode())
|
detailedMode(vectorPreferences.developerMode())
|
||||||
deviceInfo(deviceInfo)
|
deviceInfo(deviceInfo)
|
||||||
currentDevice(true)
|
currentDevice(true)
|
||||||
|
e2eCapable(true)
|
||||||
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
||||||
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
|
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
|
||||||
}
|
}
|
||||||
|
@ -129,12 +127,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||||
|
|
||||||
devices
|
devices
|
||||||
.filter {
|
.filter {
|
||||||
it.deviceId != myDeviceId
|
it.deviceInfo.deviceId != myDeviceId
|
||||||
}
|
}
|
||||||
// sort before display: most recent first
|
.forEachIndexed { idx, deviceInfoPair ->
|
||||||
.sortByLastSeen()
|
val deviceInfo = deviceInfoPair.deviceInfo
|
||||||
.forEachIndexed { idx, deviceInfo ->
|
val cryptoInfo = deviceInfoPair.cryptoDeviceInfo
|
||||||
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
|
|
||||||
deviceItem {
|
deviceItem {
|
||||||
id("device$idx")
|
id("device$idx")
|
||||||
legacyMode(legacyMode)
|
legacyMode(legacyMode)
|
||||||
|
@ -143,9 +140,10 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
|
||||||
colorProvider(colorProvider)
|
colorProvider(colorProvider)
|
||||||
detailedMode(vectorPreferences.developerMode())
|
detailedMode(vectorPreferences.developerMode())
|
||||||
deviceInfo(deviceInfo)
|
deviceInfo(deviceInfo)
|
||||||
currentDevice(isCurrentDevice)
|
currentDevice(false)
|
||||||
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
|
||||||
trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.trustLevel)
|
e2eCapable(cryptoInfo != null)
|
||||||
|
trusted(cryptoInfo?.trustLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import com.airbnb.mvrx.ViewModelContext
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
import im.vector.matrix.android.api.extensions.tryThis
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
@ -39,30 +40,35 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTxSt
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
|
||||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
|
||||||
import im.vector.matrix.android.internal.util.awaitCallback
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.matrix.rx.rx
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.functions.BiFunction
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
data class DevicesViewState(
|
data class DevicesViewState(
|
||||||
val myDeviceId: String = "",
|
val myDeviceId: String = "",
|
||||||
val devices: Async<List<DeviceInfo>> = Uninitialized,
|
// val devices: Async<List<DeviceInfo>> = Uninitialized,
|
||||||
val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
|
// val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
|
||||||
|
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
|
||||||
// TODO Replace by isLoading boolean
|
// TODO Replace by isLoading boolean
|
||||||
val request: Async<Unit> = Uninitialized,
|
val request: Async<Unit> = Uninitialized,
|
||||||
val hasAccountCrossSigning: Boolean = false,
|
val hasAccountCrossSigning: Boolean = false,
|
||||||
val accountCrossSigningIsTrusted: Boolean = false
|
val accountCrossSigningIsTrusted: Boolean = false
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
|
data class DeviceFullInfo(
|
||||||
|
val deviceInfo: DeviceInfo,
|
||||||
|
val cryptoDeviceInfo: CryptoDeviceInfo?
|
||||||
|
)
|
||||||
class DevicesViewModel @AssistedInject constructor(
|
class DevicesViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: DevicesViewState,
|
@Assisted initialState: DevicesViewState,
|
||||||
private val session: Session,
|
private val session: Session
|
||||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider)
|
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
||||||
: VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
|
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
@ -82,14 +88,37 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
private var _currentDeviceId: String? = null
|
private var _currentDeviceId: String? = null
|
||||||
private var _currentSession: String? = null
|
private var _currentSession: String? = null
|
||||||
|
|
||||||
|
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
|
||||||
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
|
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
|
||||||
|
myDeviceId = session.sessionParams.credentials.deviceId ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Observable.combineLatest<List<CryptoDeviceInfo>, List<DeviceInfo>, List<DeviceFullInfo>>(
|
||||||
|
session.rx().liveUserCryptoDevices(session.myUserId),
|
||||||
|
session.rx().liveMyDeviceInfo(),
|
||||||
|
BiFunction { cryptoList, infoList ->
|
||||||
|
infoList
|
||||||
|
.sortedByDescending { it.lastSeenTs }
|
||||||
|
.map { deviceInfo ->
|
||||||
|
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
|
||||||
|
DeviceFullInfo(deviceInfo, cryptoDeviceInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.execute { async ->
|
||||||
|
copy(
|
||||||
|
devices = async
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
session.rx().liveCrossSigningInfo(session.myUserId)
|
session.rx().liveCrossSigningInfo(session.myUserId)
|
||||||
.execute {
|
.execute {
|
||||||
copy(
|
copy(
|
||||||
|
@ -97,16 +126,38 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
|
accountCrossSigningIsTrusted = it.invoke()?.get()?.isTrusted() == true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshDevicesList()
|
|
||||||
session.cryptoService().verificationService().addListener(this)
|
session.cryptoService().verificationService().addListener(this)
|
||||||
|
|
||||||
|
// session.rx().liveMyDeviceInfo()
|
||||||
|
// .execute {
|
||||||
|
// copy(
|
||||||
|
// devices = it
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
session.rx().liveUserCryptoDevices(session.myUserId)
|
session.rx().liveUserCryptoDevices(session.myUserId)
|
||||||
.execute {
|
.distinct()
|
||||||
copy(
|
.throttleLast(5_000, TimeUnit.MILLISECONDS)
|
||||||
cryptoDevices = it
|
.subscribe {
|
||||||
)
|
// If we have a new crypto device change, we might want to trigger refresh of device info
|
||||||
|
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||||
|
}.disposeOnClear()
|
||||||
|
|
||||||
|
// session.rx().liveUserCryptoDevices(session.myUserId)
|
||||||
|
// .execute {
|
||||||
|
// copy(
|
||||||
|
// cryptoDevices = it
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
refreshPublisher.throttleFirst(4_000, TimeUnit.MILLISECONDS)
|
||||||
|
.subscribe {
|
||||||
|
session.cryptoService().fetchDevicesList(NoOpMatrixCallback())
|
||||||
|
session.cryptoService().downloadKeys(listOf(session.myUserId), true, NoOpMatrixCallback())
|
||||||
}
|
}
|
||||||
|
.disposeOnClear()
|
||||||
|
// then force download
|
||||||
|
queryRefreshDevicesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -116,7 +167,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||||
if (tx.state == VerificationTxState.Verified) {
|
if (tx.state == VerificationTxState.Verified) {
|
||||||
refreshDevicesList()
|
queryRefreshDevicesList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,69 +176,13 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
* The devices list is the list of the devices where the user is logged in.
|
* The devices list is the list of the devices where the user is logged in.
|
||||||
* It can be any mobile devices, and any browsers.
|
* It can be any mobile devices, and any browsers.
|
||||||
*/
|
*/
|
||||||
private fun refreshDevicesList() {
|
private fun queryRefreshDevicesList() {
|
||||||
if (!session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
|
refreshPublisher.onNext(Unit)
|
||||||
// display something asap
|
|
||||||
val localKnown = session.cryptoService().getUserDevices(session.myUserId).map {
|
|
||||||
DeviceInfo(
|
|
||||||
user_id = session.myUserId,
|
|
||||||
deviceId = it.deviceId,
|
|
||||||
displayName = it.displayName()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
// Keep known list if we have it, and let refresh go in backgroung
|
|
||||||
devices = this.devices.takeIf { it is Success } ?: Success(localKnown)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.cryptoService().getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
|
||||||
override fun onSuccess(data: DevicesListResponse) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
|
|
||||||
devices = Success(data.devices.orEmpty())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
devices = Fail(failure)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Put cached state
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
|
|
||||||
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// then force download
|
|
||||||
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
|
||||||
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
cryptoDevices = Success(session.cryptoService().getUserDevices(session.myUserId))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Should not happen
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: DevicesAction) {
|
override fun handle(action: DevicesAction) {
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is DevicesAction.Retry -> refreshDevicesList()
|
is DevicesAction.Refresh -> queryRefreshDevicesList()
|
||||||
is DevicesAction.Delete -> handleDelete(action)
|
is DevicesAction.Delete -> handleDelete(action)
|
||||||
is DevicesAction.Password -> handlePassword(action)
|
is DevicesAction.Password -> handlePassword(action)
|
||||||
is DevicesAction.Rename -> handleRename(action)
|
is DevicesAction.Rename -> handleRename(action)
|
||||||
|
@ -210,10 +205,10 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
|
private fun handleShowDeviceCryptoInfo(action: DevicesAction.VerifyMyDeviceManually) = withState { state ->
|
||||||
state.cryptoDevices.invoke()
|
state.devices.invoke()
|
||||||
?.firstOrNull { it.deviceId == action.deviceId }
|
?.firstOrNull { it.cryptoDeviceInfo?.deviceId == action.deviceId }
|
||||||
?.let {
|
?.let {
|
||||||
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it))
|
_viewEvents.post(DevicesViewEvents.ShowManuallyVerify(it.cryptoDeviceInfo!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,9 +233,9 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
|
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
|
||||||
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
|
val info = state.devices.invoke()?.firstOrNull { it.deviceInfo.deviceId == action.deviceId }
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info))
|
_viewEvents.post(DevicesViewEvents.PromptRenameDevice(info.deviceInfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +248,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// force settings update
|
// force settings update
|
||||||
refreshDevicesList()
|
queryRefreshDevicesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
|
@ -324,7 +319,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// force settings update
|
// force settings update
|
||||||
refreshDevicesList()
|
queryRefreshDevicesList()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -353,7 +348,7 @@ class DevicesViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// force settings update
|
// force settings update
|
||||||
refreshDevicesList()
|
queryRefreshDevicesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
|
|
|
@ -102,8 +102,8 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
|
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
|
||||||
|
viewModel.handle(DevicesAction.Refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
|
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
|
||||||
|
@ -122,7 +122,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
|
||||||
// }
|
// }
|
||||||
|
|
||||||
override fun retry() {
|
override fun retry() {
|
||||||
viewModel.handle(DevicesAction.Retry)
|
viewModel.handle(DevicesAction.Refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2201,7 +2201,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
|
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
|
|
||||||
<string name="new_session">Unverified login. Was this you?</string>
|
<string name="new_session">New login. Was this you?</string>
|
||||||
<string name="new_session_review">Tap to review & verify</string>
|
<string name="new_session_review">Tap to review & verify</string>
|
||||||
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
|
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
|
||||||
<string name="verify_new_session_was_not_me">This wasn’t me</string>
|
<string name="verify_new_session_was_not_me">This wasn’t me</string>
|
||||||
|
|
|
@ -6,7 +6,8 @@
|
||||||
<!-- Sections has been created to limit merge conflicts. -->
|
<!-- Sections has been created to limit merge conflicts. -->
|
||||||
|
|
||||||
<!-- BEGIN Strings added by Valere -->
|
<!-- BEGIN Strings added by Valere -->
|
||||||
|
<string name="review_logins">Review where you’re logged in</string>
|
||||||
|
<string name="verify_other_sessions">Verify all your sessions to ensure your account & messages are safe</string>
|
||||||
<!-- END Strings added by Valere -->
|
<!-- END Strings added by Valere -->
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue