diff --git a/changelog.d/6961.wip b/changelog.d/6961.wip
new file mode 100644
index 0000000000..2d271da8c1
--- /dev/null
+++ b/changelog.d/6961.wip
@@ -0,0 +1 @@
+[Devices Management] Session overview screen
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index cbd56dc7ea..07ea6a74cf 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3227,13 +3227,22 @@
Unknown device type
Verified session
Unverified session
- Your current session is ready for secure messaging.
- Verify your current session for enhanced secure messaging.
+
+ Your current session is ready for secure messaging.
+
+ Verify your current session for enhanced secure messaging.
+ Your current session is ready for secure messaging.
+ This session is ready for secure messaging.
+ Verify your current session for enhanced secure messaging.
+ Verify or sign out from this session for best security and reliability.
Verify Session
View Details
- Current Session
+
+ Current Session
View All (%1$d)
+
Verified · Last activity %1$s
+
Unverified · Last activity %1$s
@@ -3249,6 +3258,10 @@
- Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.
- Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.
+ Current Session
+ Session
+
+ Last activity %1$s
%s\nis looking a little empty.
diff --git a/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml
index f0807f89c6..97e0290815 100644
--- a/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml
+++ b/library/ui-styles/src/main/res/values/stylable_devices_list_header_view.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/library/ui-styles/src/main/res/values/styles_devices_management.xml b/library/ui-styles/src/main/res/values/styles_devices_management.xml
index 2a63c2ed36..6fb236d3e6 100644
--- a/library/ui-styles/src/main/res/values/styles_devices_management.xml
+++ b/library/ui-styles/src/main/res/values/styles_devices_management.xml
@@ -7,6 +7,7 @@
diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
index f22cfa369a..80ed311901 100644
--- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
+++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt
@@ -72,7 +72,7 @@ class FlowSession(private val session: Session) {
}
fun liveMyDevicesInfo(): Flow> {
- return session.cryptoService().getLiveMyDevicesInfo().asFlow()
+ return session.cryptoService().getMyDevicesInfoLive().asFlow()
.startWith(session.coroutineDispatchers.io) {
session.cryptoService().getMyDevicesInfo()
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
index ba1afd4758..48cfbebe5b 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
@@ -21,6 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.time.DefaultClock
import kotlin.random.Random
@@ -37,6 +38,7 @@ internal class CryptoStoreHelper {
userId = "userId_" + Random.nextInt(),
deviceId = "deviceId_sample",
clock = DefaultClock(),
+ myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
)
}
}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
index 251c13ccbf..f883295495 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -676,8 +676,8 @@ class E2eeSanityTests : InstrumentedTest {
assertEquals("Decimal code should have matched", oldCode, newCode)
// Assert that devices are verified
- val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
- val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
+ val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
+ val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
index 8cb38ddc87..ef3fdfeeda 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
@@ -193,7 +193,7 @@ class XSigningTest : InstrumentedTest {
fail("Bob should see the new device")
}
- val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId)
+ val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobSecondDeviceId)
assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
// Manually mark it as trusted from first session
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
index c2e74abc59..1bffbeeeaa 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
@@ -521,9 +521,9 @@ class SASTest : InstrumentedTest {
testHelper.await(bobSASLatch)
// Assert that devices are verified
- val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId)
+ val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
- bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId)
+ bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId)
assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index a5e05f69e0..e0e662c789 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
+import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.model.SessionInfo
interface CryptoService {
@@ -113,7 +114,19 @@ interface CryptoService {
fun setRoomBlacklistUnverifiedDevices(roomId: String)
- fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
+ fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
+
+ fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback)
+
+ fun getCryptoDeviceInfo(userId: String): List
+
+ fun getLiveCryptoDeviceInfo(): LiveData>
+
+ fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData>
+
+ fun getLiveCryptoDeviceInfo(userId: String): LiveData>
+
+ fun getLiveCryptoDeviceInfo(userIds: List): LiveData>
fun requestRoomKeyForEvent(event: Event)
@@ -127,9 +140,9 @@ interface CryptoService {
fun getMyDevicesInfo(): List
- fun getLiveMyDevicesInfo(): LiveData>
+ fun getMyDevicesInfoLive(): LiveData>
- fun getDeviceInfo(deviceId: String, callback: MatrixCallback)
+ fun getMyDevicesInfoLive(deviceId: String): LiveData>
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
@@ -156,14 +169,6 @@ interface CryptoService {
fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>)
- fun getCryptoDeviceInfo(userId: String): List
-
- fun getLiveCryptoDeviceInfo(): LiveData>
-
- fun getLiveCryptoDeviceInfo(userId: String): LiveData>
-
- fun getLiveCryptoDeviceInfo(userIds: List): LiveData>
-
fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
index 35c066dea8..8dd7c309c6 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt
@@ -73,6 +73,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
import org.matrix.android.sdk.api.session.sync.model.SyncResponse
+import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
@@ -273,23 +274,18 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor)
}
- override fun getLiveMyDevicesInfo(): LiveData> {
+ override fun getMyDevicesInfoLive(): LiveData> {
return cryptoStore.getLiveMyDevicesInfo()
}
+ override fun getMyDevicesInfoLive(deviceId: String): LiveData> {
+ return cryptoStore.getLiveMyDevicesInfo(deviceId)
+ }
+
override fun getMyDevicesInfo(): List {
return cryptoStore.getMyDevicesInfo()
}
- override fun getDeviceInfo(deviceId: String, callback: MatrixCallback) {
- getDeviceInfoTask
- .configureWith(GetDeviceInfoTask.Params(deviceId)) {
- this.executionThread = TaskThread.CRYPTO
- this.callback = callback
- }
- .executeBy(taskExecutor)
- }
-
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}
@@ -513,7 +509,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param userId the user id
* @param deviceId the device id
*/
- override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
+ override fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
cryptoStore.getUserDevice(userId, deviceId)
} else {
@@ -521,6 +517,15 @@ internal class DefaultCryptoService @Inject constructor(
}
}
+ override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback) {
+ getDeviceInfoTask
+ .configureWith(GetDeviceInfoTask.Params(deviceId)) {
+ this.executionThread = TaskThread.CRYPTO
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
+
override fun getCryptoDeviceInfo(userId: String): List {
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
@@ -529,6 +534,10 @@ internal class DefaultCryptoService @Inject constructor(
return cryptoStore.getLiveDeviceList()
}
+ override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData> {
+ return cryptoStore.getLiveDeviceWithId(deviceId)
+ }
+
override fun getLiveCryptoDeviceInfo(userId: String): LiveData> {
return cryptoStore.getLiveDeviceList(userId)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 0413fc730c..56eba25249 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -238,10 +238,14 @@ internal interface IMXCryptoStore {
// TODO temp
fun getLiveDeviceList(): LiveData>
+ fun getLiveDeviceWithId(deviceId: String): LiveData>
+
fun getMyDevicesInfo(): List
fun getLiveMyDevicesInfo(): LiveData>
+ fun getLiveMyDevicesInfo(deviceId: String): LiveData>
+
fun saveMyDevicesInfo(info: List)
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index f5468634cb..3b8fa4cacd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
@@ -68,6 +69,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
@@ -112,6 +114,7 @@ internal class RealmCryptoStore @Inject constructor(
@UserId private val userId: String,
@DeviceId private val deviceId: String?,
private val clock: Clock,
+ private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper,
) : IMXCryptoStore {
/* ==========================================================================================
@@ -578,6 +581,12 @@ internal class RealmCryptoStore @Inject constructor(
}
}
+ override fun getLiveDeviceWithId(deviceId: String): LiveData> {
+ return Transformations.map(getLiveDeviceList()) { devices ->
+ devices.firstOrNull { it.deviceId == deviceId }.toOptional()
+ }
+ }
+
override fun getMyDevicesInfo(): List {
return monarchy.fetchAllCopiedSync {
it.where()
@@ -596,17 +605,24 @@ internal class RealmCryptoStore @Inject constructor(
{ realm: Realm ->
realm.where()
},
- { entity ->
- DeviceInfo(
- deviceId = entity.deviceId,
- lastSeenIp = entity.lastSeenIp,
- lastSeenTs = entity.lastSeenTs,
- displayName = entity.displayName
- )
- }
+ { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) }
)
}
+ override fun getLiveMyDevicesInfo(deviceId: String): LiveData> {
+ val liveData = monarchy.findAllMappedWithChanges(
+ { realm: Realm ->
+ realm.where()
+ .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId)
+ },
+ { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) }
+ )
+
+ return Transformations.map(liveData) {
+ it.firstOrNull().toOptional()
+ }
+ }
+
override fun saveMyDevicesInfo(info: List) {
val entities = info.map {
MyDeviceLastSeenInfoEntity(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt
new file mode 100644
index 0000000000..38a7569aab
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.crypto.store.db.mapper
+
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+import javax.inject.Inject
+
+internal class MyDeviceLastSeenInfoEntityMapper @Inject constructor() {
+
+ fun map(entity: MyDeviceLastSeenInfoEntity): DeviceInfo {
+ return DeviceInfo(
+ deviceId = entity.deviceId,
+ lastSeenIp = entity.lastSeenIp,
+ lastSeenTs = entity.lastSeenTs,
+ displayName = entity.displayName
+ )
+ }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt
new file mode 100644
index 0000000000..a27f430edc
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * 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 org.matrix.android.sdk.internal.crypto.store.db.mapper
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+
+private const val A_DEVICE_ID = "device-id"
+private const val AN_IP_ADDRESS = "ip-address"
+private const val A_TIMESTAMP = 123L
+private const val A_DISPLAY_NAME = "display-name"
+
+class MyDeviceLastSeenInfoEntityMapperTest {
+
+ private val myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
+
+ @Test
+ fun `given an entity when mapping to model then all fields are correctly mapped`() {
+ val entity = MyDeviceLastSeenInfoEntity(
+ deviceId = A_DEVICE_ID,
+ lastSeenIp = AN_IP_ADDRESS,
+ lastSeenTs = A_TIMESTAMP,
+ displayName = A_DISPLAY_NAME
+ )
+ val expectedDeviceInfo = DeviceInfo(
+ deviceId = A_DEVICE_ID,
+ lastSeenIp = AN_IP_ADDRESS,
+ lastSeenTs = A_TIMESTAMP,
+ displayName = A_DISPLAY_NAME
+ )
+
+ val deviceInfo = myDeviceLastSeenInfoEntityMapper.map(entity)
+
+ deviceInfo shouldBeEqualTo expectedDeviceInfo
+ }
+}
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index c4022576c3..cd2fd52b32 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -339,6 +339,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
index a40aeaaa15..40484f57e8 100644
--- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt
@@ -88,6 +88,7 @@ import im.vector.app.features.settings.account.deactivation.DeactivateAccountVie
import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewModel
import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel
import im.vector.app.features.settings.devices.DevicesViewModel
+import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel
import im.vector.app.features.settings.devtools.AccountDataViewModel
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel
import im.vector.app.features.settings.devtools.KeyRequestListViewModel
@@ -630,4 +631,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(ReleaseNotesViewModel::class)
fun releaseNotesViewModel(factory: ReleaseNotesViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
+
+ @Binds
+ @IntoMap
+ @MavericksViewModelKey(SessionOverviewViewModel::class)
+ fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 6d94837f88..b711bf37bd 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -162,7 +162,7 @@ class MessageInformationDataFactory @Inject constructor(
.toModel()
?.deviceId
?.let { deviceId ->
- session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId)
+ session.cryptoService().getCryptoDeviceInfo(event.root.senderId ?: "", deviceId)
}
when {
sendingDevice == null -> {
diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
index 2b4d376f55..ecb1779a4a 100644
--- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt
@@ -585,7 +585,7 @@ class VectorSettingsSecurityPrivacyFragment :
}
// crypto section: device key (fingerprint)
- val deviceInfo = session.cryptoService().getDeviceInfo(userId, deviceId)
+ val deviceInfo = session.cryptoService().getCryptoDeviceInfo(userId, deviceId)
val fingerprint = deviceInfo?.fingerprint()
if (fingerprint?.isNotEmpty() == true) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/CurrentSessionCrossSigningInfo.kt b/vector/src/main/java/im/vector/app/features/settings/devices/CurrentSessionCrossSigningInfo.kt
new file mode 100644
index 0000000000..790de08823
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/CurrentSessionCrossSigningInfo.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+/**
+ * Used to hold some info about the cross signing of the current Session.
+ */
+data class CurrentSessionCrossSigningInfo(
+ val deviceId: String?,
+ val isCrossSigningInitialized: Boolean,
+ val isCrossSigningVerified: Boolean,
+)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
index 3b5bcb61d9..82c346b09c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt
@@ -101,6 +101,8 @@ class DevicesViewModel @AssistedInject constructor(
private val stringProvider: StringProvider,
private val matrix: Matrix,
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
+ getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
+ private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
) : VectorViewModel(initialState), VerificationService.Listener {
var uiaContinuation: Continuation? = null
@@ -116,8 +118,9 @@ class DevicesViewModel @AssistedInject constructor(
private val refreshSource = PublishDataSource()
init {
- val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized()
- val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
+ val currentSessionCrossSigningInfo = getCurrentSessionCrossSigningInfoUseCase.execute()
+ val hasAccountCrossSigning = currentSessionCrossSigningInfo.isCrossSigningInitialized
+ val accountCrossSigningIsTrusted = currentSessionCrossSigningInfo.isCrossSigningVerified
setState {
copy(
@@ -143,12 +146,7 @@ class DevicesViewModel @AssistedInject constructor(
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
- val trustLevelForShield = computeTrustLevelForShield(
- currentSessionCrossTrusted = accountCrossSigningIsTrusted,
- legacyMode = !hasAccountCrossSigning,
- deviceTrustLevel = cryptoDeviceInfo?.trustLevel,
- isCurrentDevice = deviceInfo.deviceId == session.sessionParams.deviceId
- )
+ val trustLevelForShield = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield, isInactive)
}
@@ -268,20 +266,6 @@ class DevicesViewModel @AssistedInject constructor(
}
}
- private fun computeTrustLevelForShield(
- currentSessionCrossTrusted: Boolean,
- legacyMode: Boolean,
- deviceTrustLevel: DeviceTrustLevel?,
- isCurrentDevice: Boolean,
- ): RoomEncryptionTrustLevel {
- return TrustUtils.shieldForTrust(
- currentDevice = isCurrentDevice,
- trustMSK = currentSessionCrossTrusted,
- legacyMode = legacyMode,
- deviceTrustLevel = deviceTrustLevel
- )
- }
-
private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) {
val txID = session.cryptoService()
.verificationService()
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt
new file mode 100644
index 0000000000..d07bd5daae
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCase.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import im.vector.app.core.di.ActiveSessionHolder
+import javax.inject.Inject
+
+class GetCurrentSessionCrossSigningInfoUseCase @Inject constructor(
+ private val activeSessionHolder: ActiveSessionHolder,
+) {
+
+ fun execute(): CurrentSessionCrossSigningInfo {
+ val session = activeSessionHolder.getActiveSession()
+ val isCrossSigningInitialized = session.cryptoService().crossSigningService().isCrossSigningInitialized()
+ val isCrossSigningVerified = session.cryptoService().crossSigningService().isCrossSigningVerified()
+ return CurrentSessionCrossSigningInfo(
+ deviceId = session.sessionParams.deviceId,
+ isCrossSigningInitialized = isCrossSigningInitialized,
+ isCrossSigningVerified = isCrossSigningVerified
+ )
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt
new file mode 100644
index 0000000000..0d30aba318
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCase.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import javax.inject.Inject
+
+class GetEncryptionTrustLevelForCurrentDeviceUseCase @Inject constructor() {
+
+ fun execute(trustMSK: Boolean, legacyMode: Boolean): RoomEncryptionTrustLevel {
+ return if (legacyMode) {
+ // In legacy, current session is always trusted
+ RoomEncryptionTrustLevel.Trusted
+ } else {
+ // If current session doesn't trust MSK, show red shield for current device
+ if (trustMSK) {
+ RoomEncryptionTrustLevel.Trusted
+ } else {
+ RoomEncryptionTrustLevel.Warning
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCase.kt
new file mode 100644
index 0000000000..e5ef4b446b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCase.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import javax.inject.Inject
+
+class GetEncryptionTrustLevelForDeviceUseCase @Inject constructor(
+ private val getEncryptionTrustLevelForCurrentDeviceUseCase: GetEncryptionTrustLevelForCurrentDeviceUseCase,
+ private val getEncryptionTrustLevelForOtherDeviceUseCase: GetEncryptionTrustLevelForOtherDeviceUseCase,
+) {
+
+ fun execute(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel {
+ val legacyMode = !currentSessionCrossSigningInfo.isCrossSigningInitialized
+ val trustMSK = currentSessionCrossSigningInfo.isCrossSigningVerified
+ val isCurrentDevice = !cryptoDeviceInfo?.deviceId.isNullOrEmpty() && cryptoDeviceInfo?.deviceId == currentSessionCrossSigningInfo.deviceId
+ val deviceTrustLevel = cryptoDeviceInfo?.trustLevel
+
+ return when {
+ isCurrentDevice -> getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK, legacyMode)
+ else -> getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK, legacyMode, deviceTrustLevel)
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCase.kt
new file mode 100644
index 0000000000..11bc3a8ede
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCase.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import javax.inject.Inject
+
+class GetEncryptionTrustLevelForOtherDeviceUseCase @Inject constructor() {
+
+ fun execute(trustMSK: Boolean, legacyMode: Boolean, deviceTrustLevel: DeviceTrustLevel?): RoomEncryptionTrustLevel {
+ return if (legacyMode) {
+ // use local trust
+ if (deviceTrustLevel?.locallyVerified == true) {
+ RoomEncryptionTrustLevel.Trusted
+ } else {
+ RoomEncryptionTrustLevel.Warning
+ }
+ } else {
+ if (trustMSK) {
+ // use cross sign trust, put locally trusted in black
+ when {
+ deviceTrustLevel?.crossSigningVerified == true -> RoomEncryptionTrustLevel.Trusted
+ deviceTrustLevel?.locallyVerified == true -> RoomEncryptionTrustLevel.Default
+ else -> RoomEncryptionTrustLevel.Warning
+ }
+ } else {
+ // The current session is untrusted, so displays others in black
+ // as we can't know the cross-signing state
+ RoomEncryptionTrustLevel.Default
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/TrustUtils.kt b/vector/src/main/java/im/vector/app/features/settings/devices/TrustUtils.kt
index da18154ea1..7709a63344 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/TrustUtils.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/TrustUtils.kt
@@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+// TODO Replace usage by the use case GetEncryptionTrustLevelForDeviceUseCase
object TrustUtils {
fun shieldForTrust(
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
index 4bb1d1131b..dc72d4fe9c 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt
@@ -31,8 +31,11 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
+import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.dialogs.ManuallyVerifyDialog
import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet
@@ -40,8 +43,11 @@ import im.vector.app.features.settings.devices.DeviceFullInfo
import im.vector.app.features.settings.devices.DevicesAction
import im.vector.app.features.settings.devices.DevicesViewEvents
import im.vector.app.features.settings.devices.DevicesViewModel
+import im.vector.app.features.settings.devices.v2.list.OtherSessionsController
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
+import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
+import javax.inject.Inject
/**
* Display the list of the user's devices and sessions.
@@ -50,6 +56,14 @@ import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationVie
class VectorSettingsDevicesFragment :
VectorBaseFragment() {
+ @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator
+
+ @Inject lateinit var dateFormatter: VectorDateFormatter
+
+ @Inject lateinit var drawableProvider: DrawableProvider
+
+ @Inject lateinit var colorProvider: ColorProvider
+
private val viewModel: DevicesViewModel by fragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding {
@@ -72,10 +86,11 @@ class VectorSettingsDevicesFragment :
initLearnMoreButtons()
initWaitingView()
- observerViewEvents()
+ initOtherSessionsView()
+ observeViewEvents()
}
- private fun observerViewEvents() {
+ private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
@@ -110,6 +125,14 @@ class VectorSettingsDevicesFragment :
views.waitingView.waitingStatusText.isVisible = true
}
+ private fun initOtherSessionsView() {
+ views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback {
+ override fun onItemClicked(deviceId: String) {
+ navigateToSessionOverview(deviceId)
+ }
+ })
+ }
+
override fun onDestroyView() {
cleanUpLearnMoreButtonsListeners()
super.onDestroyView()
@@ -196,16 +219,39 @@ class VectorSettingsDevicesFragment :
currentDeviceInfo?.let {
views.deviceListHeaderCurrentSession.isVisible = true
views.deviceListCurrentSession.isVisible = true
- views.deviceListCurrentSession.render(it)
+ val viewState = SessionInfoViewState(
+ isCurrentSession = true,
+ deviceFullInfo = it
+ )
+ views.deviceListCurrentSession.render(viewState, dateFormatter, drawableProvider, colorProvider)
+ views.deviceListCurrentSession.debouncedClicks {
+ currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
+ }
+ views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
+ currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
+ }
} ?: run {
hideCurrentSessionView()
}
}
+ private fun navigateToSessionOverview(deviceId: String) {
+ viewNavigator.navigateToSessionOverview(
+ context = requireActivity(),
+ deviceId = deviceId
+ )
+ }
+
private fun hideCurrentSessionView() {
views.deviceListHeaderCurrentSession.isVisible = false
views.deviceListCurrentSession.isVisible = false
views.deviceListDividerCurrentSession.isVisible = false
+ views.deviceListCurrentSession.debouncedClicks {
+ // do nothing
+ }
+ views.deviceListCurrentSession.viewDetailsButton.debouncedClicks {
+ // do nothing
+ }
}
private fun handleRequestStatus(unIgnoreRequest: Async) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
new file mode 100644
index 0000000000..54eed3bc14
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigator.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2
+
+import android.content.Context
+import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
+import javax.inject.Inject
+
+class VectorSettingsDevicesViewNavigator @Inject constructor() {
+
+ fun navigateToSessionOverview(context: Context, deviceId: String) {
+ context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt
deleted file mode 100644
index d6f81f4f79..0000000000
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2022 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.app.features.settings.devices.v2.list
-
-import android.content.Context
-import android.util.AttributeSet
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.view.isVisible
-import im.vector.app.R
-import im.vector.app.databinding.ViewCurrentSessionBinding
-import im.vector.app.features.settings.devices.DeviceFullInfo
-import im.vector.app.features.themes.ThemeUtils
-import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
-
-class CurrentSessionView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : ConstraintLayout(context, attrs, defStyleAttr) {
-
- private val views: ViewCurrentSessionBinding
-
- init {
- inflate(context, R.layout.view_current_session, this)
- views = ViewCurrentSessionBinding.bind(this)
- }
-
- fun render(currentDeviceInfo: DeviceFullInfo) {
- renderDeviceInfo(currentDeviceInfo.deviceInfo.displayName.orEmpty())
- renderVerificationStatus(currentDeviceInfo.trustLevelForShield)
- }
-
- private fun renderVerificationStatus(trustLevelForShield: RoomEncryptionTrustLevel) {
- views.currentSessionVerificationStatusImageView.render(trustLevelForShield)
- if (trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
- renderCrossSigningVerified()
- } else {
- renderCrossSigningUnverified()
- }
- }
-
- private fun renderCrossSigningVerified() {
- views.currentSessionVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_verified)
- views.currentSessionVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
- views.currentSessionVerificationStatusDetailTextView.text = context.getString(R.string.device_manager_verification_status_detail_verified)
- views.currentSessionVerifySessionButton.isVisible = false
- }
-
- private fun renderCrossSigningUnverified() {
- views.currentSessionVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified)
- views.currentSessionVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError))
- views.currentSessionVerificationStatusDetailTextView.text = context.getString(R.string.device_manager_verification_status_detail_unverified)
- views.currentSessionVerifySessionButton.isVisible = true
- }
-
- // TODO. We don't have this info yet. Update later accordingly.
- private fun renderDeviceInfo(sessionName: String) {
- views.currentSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile)
- views.currentSessionDeviceTypeImageView.contentDescription = context.getString(R.string.a11y_device_manager_device_type_mobile)
- views.currentSessionNameTextView.text = sessionName
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
index e9376953e0..c73389d775 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
@@ -22,8 +22,10 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
+import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.epoxy.onClick
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.views.ShieldImageView
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@@ -49,8 +51,16 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la
@EpoxyAttribute
lateinit var stringProvider: StringProvider
+ @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
+ var clickListener: ClickListener? = null
+
override fun bind(holder: Holder) {
super.bind(holder)
+ holder.view.onClick(clickListener)
+ if (clickListener == null) {
+ holder.view.isClickable = false
+ }
+
when (deviceType) {
DeviceType.MOBILE -> {
holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
index 8a5ee05af7..6419d02fc9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
@@ -35,6 +35,12 @@ class OtherSessionsController @Inject constructor(
private val colorProvider: ColorProvider,
) : TypedEpoxyController>() {
+ var callback: Callback? = null
+
+ interface Callback {
+ fun onItemClicked(deviceId: String)
+ }
+
override fun buildModels(data: List?) {
val host = this
@@ -70,6 +76,7 @@ class OtherSessionsController @Inject constructor(
sessionDescription(description)
sessionDescriptionDrawable(descriptionDrawable)
stringProvider(this@OtherSessionsController.stringProvider)
+ clickListener { device.deviceInfo.deviceId?.let { host.callback?.onItemClicked(it) } }
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt
index 55978e61fd..682a9c6e64 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt
@@ -49,7 +49,12 @@ class OtherSessionsView @JvmOverloads constructor(
otherSessionsController.setData(devices)
}
+ fun setCallback(callback: OtherSessionsController.Callback) {
+ otherSessionsController.callback = callback
+ }
+
override fun onDetachedFromWindow() {
+ otherSessionsController.callback = null
views.otherSessionsRecyclerView.cleanup()
super.onDetachedFromWindow()
}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
new file mode 100644
index 0000000000..767f09482b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.list
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import im.vector.app.R
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.extensions.setTextWithColoredPart
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
+import im.vector.app.databinding.ViewSessionInfoBinding
+import im.vector.app.features.themes.ThemeUtils
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+
+class SessionInfoView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val views: ViewSessionInfoBinding
+
+ var onLearnMoreClickListener: (() -> Unit)? = null
+
+ init {
+ inflate(context, R.layout.view_session_info, this)
+ views = ViewSessionInfoBinding.bind(this)
+ }
+
+ val viewDetailsButton = views.sessionInfoViewDetailsButton
+
+ fun render(
+ sessionInfoViewState: SessionInfoViewState,
+ dateFormatter: VectorDateFormatter,
+ drawableProvider: DrawableProvider,
+ colorProvider: ColorProvider,
+ ) {
+ renderDeviceInfo(sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty())
+ renderVerificationStatus(
+ sessionInfoViewState.deviceFullInfo.trustLevelForShield,
+ sessionInfoViewState.isCurrentSession,
+ sessionInfoViewState.isLearnMoreLinkVisible,
+ )
+ renderDeviceLastSeenDetails(
+ sessionInfoViewState.deviceFullInfo.isInactive,
+ sessionInfoViewState.deviceFullInfo.deviceInfo,
+ sessionInfoViewState.isLastSeenDetailsVisible,
+ dateFormatter,
+ drawableProvider,
+ colorProvider,
+ )
+ renderDetailsButton(sessionInfoViewState.isDetailsButtonVisible)
+ }
+
+ private fun renderVerificationStatus(
+ encryptionTrustLevel: RoomEncryptionTrustLevel,
+ isCurrentSession: Boolean,
+ hasLearnMoreLink: Boolean,
+ ) {
+ views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel)
+ if (encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
+ renderCrossSigningVerified(isCurrentSession)
+ } else {
+ renderCrossSigningUnverified(isCurrentSession)
+ }
+ if (hasLearnMoreLink) {
+ appendLearnMoreToVerificationStatus()
+ }
+ }
+
+ private fun appendLearnMoreToVerificationStatus() {
+ val status = views.sessionInfoVerificationStatusDetailTextView.text
+ val learnMore = context.getString(R.string.action_learn_more)
+ val stringBuilder = StringBuilder()
+ stringBuilder.append(status)
+ stringBuilder.append(" ")
+ stringBuilder.append(learnMore)
+
+ views.sessionInfoVerificationStatusDetailTextView.setTextWithColoredPart(
+ fullText = stringBuilder.toString(),
+ coloredPart = learnMore,
+ underline = false
+ ) {
+ onLearnMoreClickListener?.invoke()
+ }
+ }
+
+ private fun renderCrossSigningVerified(isCurrentSession: Boolean) {
+ views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_verified)
+ views.sessionInfoVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
+ val statusResId = if (isCurrentSession) {
+ R.string.device_manager_verification_status_detail_current_session_verified
+ } else {
+ R.string.device_manager_verification_status_detail_other_session_verified
+ }
+ views.sessionInfoVerificationStatusDetailTextView.text = context.getString(statusResId)
+ views.sessionInfoVerifySessionButton.isVisible = false
+ }
+
+ private fun renderCrossSigningUnverified(isCurrentSession: Boolean) {
+ views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified)
+ views.sessionInfoVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError))
+ val statusResId = if (isCurrentSession) {
+ R.string.device_manager_verification_status_detail_current_session_unverified
+ } else {
+ R.string.device_manager_verification_status_detail_other_session_unverified
+ }
+ views.sessionInfoVerificationStatusDetailTextView.text = context.getString(statusResId)
+ views.sessionInfoVerifySessionButton.isVisible = true
+ }
+
+ // TODO. We don't have this info yet. Update later accordingly.
+ private fun renderDeviceInfo(sessionName: String) {
+ views.sessionInfoDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile)
+ views.sessionInfoDeviceTypeImageView.contentDescription = context.getString(R.string.a11y_device_manager_device_type_mobile)
+ views.sessionInfoNameTextView.text = sessionName
+ }
+
+ private fun renderDeviceLastSeenDetails(
+ isInactive: Boolean,
+ deviceInfo: DeviceInfo,
+ isLastSeenDetailsVisible: Boolean,
+ dateFormatter: VectorDateFormatter,
+ drawableProvider: DrawableProvider,
+ colorProvider: ColorProvider,
+ ) {
+ deviceInfo.lastSeenTs
+ ?.takeIf { isLastSeenDetailsVisible }
+ ?.let { timestamp ->
+ views.sessionInfoLastActivityTextView.isVisible = true
+ views.sessionInfoLastActivityTextView.text = if (isInactive) {
+ val formattedTs = dateFormatter.format(timestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)
+ context.resources.getQuantityString(
+ R.plurals.device_manager_other_sessions_description_inactive,
+ SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
+ SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
+ formattedTs
+ )
+ } else {
+ val formattedTs = dateFormatter.format(timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
+ context.getString(R.string.device_manager_session_last_activity, formattedTs)
+ }
+ val drawable = if (isInactive) {
+ val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
+ drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor)
+ } else {
+ null
+ }
+ views.sessionInfoLastActivityTextView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
+ }
+ ?: run {
+ views.sessionInfoLastActivityTextView.isGone = true
+ }
+
+ deviceInfo.lastSeenIp
+ ?.takeIf { isLastSeenDetailsVisible }
+ ?.let { ipAddress ->
+ views.sessionInfoLastIPAddressTextView.isVisible = true
+ views.sessionInfoLastIPAddressTextView.text = ipAddress
+ }
+ ?: run {
+ views.sessionInfoLastIPAddressTextView.isGone = true
+ }
+ }
+
+ private fun renderDetailsButton(isDetailsButtonVisible: Boolean) {
+ views.sessionInfoViewDetailsButton.isVisible = isDetailsButtonVisible
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt
new file mode 100644
index 0000000000..22ad710676
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.list
+
+import im.vector.app.features.settings.devices.DeviceFullInfo
+
+data class SessionInfoViewState(
+ val isCurrentSession: Boolean,
+ val deviceFullInfo: DeviceFullInfo,
+ val isDetailsButtonVisible: Boolean = true,
+ val isLearnMoreLinkVisible: Boolean = false,
+ val isLastSeenDetailsVisible: Boolean = false,
+)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
similarity index 74%
rename from vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt
rename to vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
index d6c7dbe273..547ed93f24 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt
@@ -25,15 +25,15 @@ import androidx.core.content.res.use
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.extensions.setTextWithColoredPart
-import im.vector.app.databinding.ViewDevicesListHeaderBinding
+import im.vector.app.databinding.ViewSessionsListHeaderBinding
-class DevicesListHeaderView @JvmOverloads constructor(
+class SessionsListHeaderView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
- private val binding = ViewDevicesListHeaderBinding.inflate(
+ private val binding = ViewSessionsListHeaderBinding.inflate(
LayoutInflater.from(context),
this
)
@@ -43,7 +43,7 @@ class DevicesListHeaderView @JvmOverloads constructor(
init {
context.obtainStyledAttributes(
attrs,
- R.styleable.DevicesListHeaderView,
+ R.styleable.SessionsListHeaderView,
0,
0
).use {
@@ -53,14 +53,14 @@ class DevicesListHeaderView @JvmOverloads constructor(
}
private fun setTitle(typedArray: TypedArray) {
- val title = typedArray.getString(R.styleable.DevicesListHeaderView_devicesListHeaderTitle)
- binding.devicesListHeaderTitle.text = title
+ val title = typedArray.getString(R.styleable.SessionsListHeaderView_devicesListHeaderTitle)
+ binding.sessionsListHeaderTitle.text = title
}
private fun setDescription(typedArray: TypedArray) {
- val description = typedArray.getString(R.styleable.DevicesListHeaderView_devicesListHeaderDescription)
+ val description = typedArray.getString(R.styleable.SessionsListHeaderView_devicesListHeaderDescription)
if (description.isNullOrEmpty()) {
- binding.devicesListHeaderDescription.isVisible = false
+ binding.sessionsListHeaderDescription.isVisible = false
return
}
@@ -70,8 +70,8 @@ class DevicesListHeaderView @JvmOverloads constructor(
stringBuilder.append(" ")
stringBuilder.append(learnMore)
- binding.devicesListHeaderDescription.isVisible = true
- binding.devicesListHeaderDescription.setTextWithColoredPart(
+ binding.sessionsListHeaderDescription.isVisible = true
+ binding.sessionsListHeaderDescription.setTextWithColoredPart(
fullText = stringBuilder.toString(),
coloredPart = learnMore,
underline = false
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt
new file mode 100644
index 0000000000..c3579b68c3
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import androidx.lifecycle.asFlow
+import im.vector.app.core.di.ActiveSessionHolder
+import im.vector.app.features.settings.devices.DeviceFullInfo
+import im.vector.app.features.settings.devices.GetCurrentSessionCrossSigningInfoUseCase
+import im.vector.app.features.settings.devices.GetEncryptionTrustLevelForDeviceUseCase
+import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import javax.inject.Inject
+
+class GetDeviceFullInfoUseCase @Inject constructor(
+ private val activeSessionHolder: ActiveSessionHolder,
+ private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
+ private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
+ private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
+) {
+
+ fun execute(deviceId: String): Flow> {
+ return activeSessionHolder.getSafeActiveSession()?.let { session ->
+ val currentSessionCrossSigningInfo = getCurrentSessionCrossSigningInfoUseCase.execute()
+ combine(
+ session.cryptoService().getMyDevicesInfoLive(deviceId).asFlow(),
+ session.cryptoService().getLiveCryptoDeviceInfoWithId(deviceId).asFlow()
+ ) { deviceInfo, cryptoDeviceInfo ->
+ val info = deviceInfo.getOrNull()
+ val cryptoInfo = cryptoDeviceInfo.getOrNull()
+ val fullInfo = if (info != null && cryptoInfo != null) {
+ val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo)
+ val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0)
+ DeviceFullInfo(
+ deviceInfo = info,
+ cryptoDeviceInfo = cryptoInfo,
+ trustLevelForShield = roomEncryptionTrustLevel,
+ isInactive = isInactive
+ )
+ } else {
+ null
+ }
+ fullInfo.toOptional()
+ }
+ } ?: emptyFlow()
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt
new file mode 100644
index 0000000000..c028c08ec4
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.app.features.settings.devices.v2.overview
+
+import im.vector.app.core.platform.VectorViewModelAction
+
+sealed class SessionOverviewAction : VectorViewModelAction
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewActivity.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewActivity.kt
new file mode 100644
index 0000000000..015fcccf51
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.app.features.settings.devices.v2.overview
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.airbnb.mvrx.Mavericks
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.core.extensions.addFragment
+import im.vector.app.core.platform.SimpleFragmentActivity
+
+/**
+ * Display the overview info about a Session.
+ */
+@AndroidEntryPoint
+class SessionOverviewActivity : SimpleFragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (isFirstCreation()) {
+ addFragment(
+ container = views.container,
+ fragmentClass = SessionOverviewFragment::class.java,
+ params = intent.getParcelableExtra(Mavericks.KEY_ARG)
+ )
+ }
+ }
+
+ companion object {
+ fun newIntent(context: Context, deviceId: String): Intent {
+ return Intent(context, SessionOverviewActivity::class.java).apply {
+ putExtra(Mavericks.KEY_ARG, SessionOverviewArgs(deviceId))
+ }
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewArgs.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewArgs.kt
new file mode 100644
index 0000000000..27c8d6fb2e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewArgs.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class SessionOverviewArgs(
+ val deviceId: String
+) : Parcelable
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
new file mode 100644
index 0000000000..a6bac6087b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
+import dagger.hilt.android.AndroidEntryPoint
+import im.vector.app.R
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
+import im.vector.app.databinding.FragmentSessionOverviewBinding
+import im.vector.app.features.settings.devices.DeviceFullInfo
+import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
+import javax.inject.Inject
+
+/**
+ * Display the overview info about a Session.
+ */
+@AndroidEntryPoint
+class SessionOverviewFragment :
+ VectorBaseFragment() {
+
+ @Inject lateinit var dateFormatter: VectorDateFormatter
+
+ @Inject lateinit var drawableProvider: DrawableProvider
+
+ @Inject lateinit var colorProvider: ColorProvider
+
+ private val viewModel: SessionOverviewViewModel by fragmentViewModel()
+
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding {
+ return FragmentSessionOverviewBinding.inflate(inflater, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initSessionInfoView()
+ }
+
+ private fun initSessionInfoView() {
+ views.sessionOverviewInfo.onLearnMoreClickListener = {
+ Toast.makeText(context, "Learn more verification status", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ override fun onDestroyView() {
+ cleanUpSessionInfoView()
+ super.onDestroyView()
+ }
+
+ private fun cleanUpSessionInfoView() {
+ views.sessionOverviewInfo.onLearnMoreClickListener = null
+ }
+
+ override fun invalidate() = withState(viewModel) { state ->
+ updateToolbar(state.isCurrentSession)
+ if (state.deviceInfo is Success) {
+ renderSessionInfo(state.isCurrentSession, state.deviceInfo.invoke())
+ } else {
+ hideSessionInfo()
+ }
+ }
+
+ private fun updateToolbar(isCurrentSession: Boolean) {
+ val titleResId = if (isCurrentSession) R.string.device_manager_current_session_title else R.string.device_manager_session_title
+ (activity as? AppCompatActivity)
+ ?.supportActionBar
+ ?.setTitle(titleResId)
+ }
+
+ private fun renderSessionInfo(isCurrentSession: Boolean, deviceFullInfo: DeviceFullInfo) {
+ views.sessionOverviewInfo.isVisible = true
+ val viewState = SessionInfoViewState(
+ isCurrentSession = isCurrentSession,
+ deviceFullInfo = deviceFullInfo,
+ isDetailsButtonVisible = false,
+ isLearnMoreLinkVisible = true,
+ isLastSeenDetailsVisible = true,
+ )
+ views.sessionOverviewInfo.render(viewState, dateFormatter, drawableProvider, colorProvider)
+ }
+
+ private fun hideSessionInfo() {
+ views.sessionOverviewInfo.isGone = true
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
new file mode 100644
index 0000000000..1a1d3640a2
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import com.airbnb.mvrx.MavericksViewModelFactory
+import com.airbnb.mvrx.Success
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import im.vector.app.core.di.MavericksAssistedViewModelFactory
+import im.vector.app.core.di.hiltMavericksViewModelFactory
+import im.vector.app.core.platform.EmptyViewEvents
+import im.vector.app.core.platform.VectorViewModel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import org.matrix.android.sdk.api.session.Session
+
+class SessionOverviewViewModel @AssistedInject constructor(
+ @Assisted val initialState: SessionOverviewViewState,
+ session: Session,
+ private val getDeviceFullInfoUseCase: GetDeviceFullInfoUseCase,
+) : VectorViewModel(initialState) {
+
+ companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory()
+
+ @AssistedFactory
+ interface Factory : MavericksAssistedViewModelFactory {
+ override fun create(initialState: SessionOverviewViewState): SessionOverviewViewModel
+ }
+
+ init {
+ val currentDeviceId = session.sessionParams.deviceId.orEmpty()
+ setState {
+ copy(isCurrentSession = deviceId.isNotEmpty() && deviceId == currentDeviceId)
+ }
+
+ observeSessionInfo(initialState.deviceId)
+ }
+
+ private fun observeSessionInfo(deviceId: String) {
+ getDeviceFullInfoUseCase.execute(deviceId)
+ .mapNotNull { it.getOrNull() }
+ .onEach { setState { copy(deviceInfo = Success(it)) } }
+ .launchIn(viewModelScope)
+ }
+
+ override fun handle(action: SessionOverviewAction) {
+ TODO("Implement when adding the first action")
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
new file mode 100644
index 0000000000..c9f5635cbd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.MavericksState
+import com.airbnb.mvrx.Uninitialized
+import im.vector.app.features.settings.devices.DeviceFullInfo
+
+data class SessionOverviewViewState(
+ val deviceId: String,
+ val isCurrentSession: Boolean = false,
+ val deviceInfo: Async = Uninitialized,
+) : MavericksState {
+ constructor(args: SessionOverviewArgs) : this(
+ deviceId = args.deviceId
+ )
+}
diff --git a/vector/src/main/res/layout/fragment_session_overview.xml b/vector/src/main/res/layout/fragment_session_overview.xml
new file mode 100644
index 0000000000..156e61673b
--- /dev/null
+++ b/vector/src/main/res/layout/fragment_session_overview.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml
index 6710f345ce..9cefd6aa24 100644
--- a/vector/src/main/res/layout/fragment_settings_devices.xml
+++ b/vector/src/main/res/layout/fragment_settings_devices.xml
@@ -8,7 +8,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
-
-
-
+ app:layout_constraintTop_toBottomOf="@id/sessionInfoNameTextView">
+ app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusContainer"
+ tools:text="@string/device_manager_verification_status_detail_current_session_verified" />
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/sessionInfoLastIPAddressTextView" />
+ app:layout_constraintTop_toBottomOf="@id/sessionInfoVerifySessionButton" />
diff --git a/vector/src/main/res/layout/view_devices_list_header.xml b/vector/src/main/res/layout/view_sessions_list_header.xml
similarity index 83%
rename from vector/src/main/res/layout/view_devices_list_header.xml
rename to vector/src/main/res/layout/view_sessions_list_header.xml
index 492c3e7a12..d690ee4c87 100644
--- a/vector/src/main/res/layout/view_devices_list_header.xml
+++ b/vector/src/main/res/layout/view_sessions_list_header.xml
@@ -7,7 +7,7 @@
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
diff --git a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt
index ed1bcebf16..89966b5317 100644
--- a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt
@@ -18,7 +18,7 @@ package im.vector.app.features.location.live
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession
-import im.vector.app.test.fakes.givenAsFlowReturns
+import im.vector.app.test.fakes.givenAsFlow
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
@@ -28,7 +28,6 @@ import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
-import org.matrix.android.sdk.api.util.Optional
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
@@ -64,7 +63,7 @@ class GetLiveLocationShareSummaryUseCaseTest {
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenLiveLocationShareSummaryReturns(AN_EVENT_ID, summary)
- .givenAsFlowReturns(Optional(summary))
+ .givenAsFlow()
val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first()
@@ -77,7 +76,7 @@ class GetLiveLocationShareSummaryUseCaseTest {
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenLiveLocationShareSummaryReturns(AN_EVENT_ID, null)
- .givenAsFlowReturns(Optional(null))
+ .givenAsFlow()
val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first()
diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt
index 420b8e6a06..6d24858915 100644
--- a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt
+++ b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt
@@ -19,7 +19,7 @@ package im.vector.app.features.location.live.map
import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession
-import im.vector.app.test.fakes.givenAsFlowReturns
+import im.vector.app.test.fakes.givenAsFlow
import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.unmockkAll
@@ -81,7 +81,7 @@ class GetListOfUserLiveLocationUseCaseTest {
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenRunningLiveLocationShareSummariesReturns(summaries)
- .givenAsFlowReturns(summaries)
+ .givenAsFlow()
val viewState1 = UserLiveLocationViewState(
matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCaseTest.kt
new file mode 100644
index 0000000000..7c8ee008eb
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/GetCurrentSessionCrossSigningInfoUseCaseTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.auth.data.SessionParams
+
+private const val A_DEVICE_ID = "device-id"
+
+class GetCurrentSessionCrossSigningInfoUseCaseTest {
+
+ private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+
+ private val getCurrentSessionCrossSigningInfoUseCase = GetCurrentSessionCrossSigningInfoUseCase(
+ activeSessionHolder = fakeActiveSessionHolder.instance
+ )
+
+ @Test
+ fun `given the active session when getting cross signing info then the result is correct`() {
+ val sessionParams = mockk()
+ every { sessionParams.deviceId } returns A_DEVICE_ID
+ fakeActiveSessionHolder.fakeSession.givenSessionParams(sessionParams)
+ val isCrossSigningInitialized = true
+ fakeActiveSessionHolder.fakeSession
+ .fakeCryptoService
+ .fakeCrossSigningService
+ .givenIsCrossSigningInitializedReturns(isCrossSigningInitialized)
+ val isCrossSigningVerified = true
+ fakeActiveSessionHolder.fakeSession
+ .fakeCryptoService
+ .fakeCrossSigningService
+ .givenIsCrossSigningVerifiedReturns(isCrossSigningVerified)
+ val expectedResult = CurrentSessionCrossSigningInfo(
+ deviceId = A_DEVICE_ID,
+ isCrossSigningInitialized = isCrossSigningInitialized,
+ isCrossSigningVerified = isCrossSigningVerified
+ )
+
+ val result = getCurrentSessionCrossSigningInfoUseCase.execute()
+
+ result shouldBeEqualTo expectedResult
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCaseTest.kt
new file mode 100644
index 0000000000..830eab5dcb
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForCurrentDeviceUseCaseTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+
+class GetEncryptionTrustLevelForCurrentDeviceUseCaseTest {
+
+ private val getEncryptionTrustLevelForCurrentDeviceUseCase = GetEncryptionTrustLevelForCurrentDeviceUseCase()
+
+ @Test
+ fun `given in legacy mode when computing trust level then device is trusted`() {
+ val trustMSK = false
+ val legacyMode = true
+
+ val result = getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Trusted
+ }
+
+ @Test
+ fun `given trustMSK is true and not in legacy mode when computing trust level then device is trusted`() {
+ val trustMSK = true
+ val legacyMode = false
+
+ val result = getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Trusted
+ }
+
+ @Test
+ fun `given trustMSK is false and not in legacy mode when computing trust level then device is unverified`() {
+ val trustMSK = false
+ val legacyMode = false
+
+ val result = getEncryptionTrustLevelForCurrentDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Warning
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCaseTest.kt
new file mode 100644
index 0000000000..8d54b31ab4
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForDeviceUseCaseTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+
+private const val A_DEVICE_ID = "device-id"
+private const val A_DEVICE_ID_2 = "device-id-2"
+
+class GetEncryptionTrustLevelForDeviceUseCaseTest {
+
+ private val getEncryptionTrustLevelForCurrentDeviceUseCase = mockk()
+ private val getEncryptionTrustLevelForOtherDeviceUseCase = mockk()
+
+ private val getEncryptionTrustLevelForDeviceUseCase = GetEncryptionTrustLevelForDeviceUseCase(
+ getEncryptionTrustLevelForCurrentDeviceUseCase = getEncryptionTrustLevelForCurrentDeviceUseCase,
+ getEncryptionTrustLevelForOtherDeviceUseCase = getEncryptionTrustLevelForOtherDeviceUseCase,
+ )
+
+ @Test
+ fun `given is current device when computing trust level then the correct sub use case result is returned`() {
+ val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo(
+ deviceId = A_DEVICE_ID,
+ isCrossSigningInitialized = true,
+ isCrossSigningVerified = false
+ )
+ val cryptoDeviceInfo = givenCryptoDeviceInfo(
+ deviceId = A_DEVICE_ID,
+ trustLevel = null
+ )
+ val trustLevel = RoomEncryptionTrustLevel.Trusted
+ every { getEncryptionTrustLevelForCurrentDeviceUseCase.execute(any(), any()) } returns trustLevel
+
+ val result = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
+
+ result shouldBeEqualTo trustLevel
+ verify {
+ getEncryptionTrustLevelForCurrentDeviceUseCase.execute(
+ trustMSK = currentSessionCrossSigningInfo.isCrossSigningVerified,
+ legacyMode = !currentSessionCrossSigningInfo.isCrossSigningInitialized
+ )
+ }
+ }
+
+ @Test
+ fun `given is not current device when computing trust level then the correct sub use case result is returned`() {
+ val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo(
+ deviceId = A_DEVICE_ID,
+ isCrossSigningInitialized = true,
+ isCrossSigningVerified = false
+ )
+ val cryptoDeviceInfo = givenCryptoDeviceInfo(
+ deviceId = A_DEVICE_ID_2,
+ trustLevel = null
+ )
+ val trustLevel = RoomEncryptionTrustLevel.Trusted
+ every { getEncryptionTrustLevelForOtherDeviceUseCase.execute(any(), any(), any()) } returns trustLevel
+
+ val result = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
+
+ result shouldBeEqualTo trustLevel
+ verify {
+ getEncryptionTrustLevelForOtherDeviceUseCase.execute(
+ trustMSK = currentSessionCrossSigningInfo.isCrossSigningVerified,
+ legacyMode = !currentSessionCrossSigningInfo.isCrossSigningInitialized,
+ deviceTrustLevel = cryptoDeviceInfo.trustLevel
+ )
+ }
+ }
+
+ private fun givenCurrentSessionCrossSigningInfo(
+ deviceId: String?,
+ isCrossSigningInitialized: Boolean,
+ isCrossSigningVerified: Boolean
+ ): CurrentSessionCrossSigningInfo {
+ return CurrentSessionCrossSigningInfo(
+ deviceId = deviceId,
+ isCrossSigningInitialized = isCrossSigningInitialized,
+ isCrossSigningVerified = isCrossSigningVerified
+ )
+ }
+
+ private fun givenCryptoDeviceInfo(
+ deviceId: String,
+ trustLevel: DeviceTrustLevel?
+ ): CryptoDeviceInfo {
+ return CryptoDeviceInfo(
+ userId = "",
+ deviceId = deviceId,
+ trustLevel = trustLevel
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCaseTest.kt
new file mode 100644
index 0000000000..9dc87c2a16
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/GetEncryptionTrustLevelForOtherDeviceUseCaseTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+
+class GetEncryptionTrustLevelForOtherDeviceUseCaseTest {
+
+ private val getEncryptionTrustLevelForOtherDeviceUseCase = GetEncryptionTrustLevelForOtherDeviceUseCase()
+
+ @Test
+ fun `given in legacy mode and device locally verified when computing trust level then device is trusted`() {
+ val trustMSK = false
+ val legacyMode = true
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = true, crossSigningVerified = false)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Trusted
+ }
+
+ @Test
+ fun `given in legacy mode and device not locally verified when computing trust level then device is unverified`() {
+ val trustMSK = false
+ val legacyMode = true
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = false, crossSigningVerified = false)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Warning
+ }
+
+ @Test
+ fun `given trustMSK is true and not in legacy mode and device cross signing verified when computing trust level then device is trusted`() {
+ val trustMSK = true
+ val legacyMode = false
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = false, crossSigningVerified = true)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Trusted
+ }
+
+ @Test
+ fun `given trustMSK is true and not in legacy mode and device locally verified when computing trust level then device has default trust level`() {
+ val trustMSK = true
+ val legacyMode = false
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = true, crossSigningVerified = false)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Default
+ }
+
+ @Test
+ fun `given trustMSK is true and not in legacy mode and device not verified when computing trust level then device is unverified`() {
+ val trustMSK = true
+ val legacyMode = false
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = false, crossSigningVerified = false)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Warning
+ }
+
+ @Test
+ fun `given trustMSK is false and not in legacy mode when computing trust level then device has default trust level`() {
+ val trustMSK = false
+ val legacyMode = false
+ val deviceTrustLevel = givenDeviceTrustLevel(locallyVerified = false, crossSigningVerified = false)
+
+ val result = getEncryptionTrustLevelForOtherDeviceUseCase.execute(trustMSK = trustMSK, legacyMode = legacyMode, deviceTrustLevel = deviceTrustLevel)
+
+ result shouldBeEqualTo RoomEncryptionTrustLevel.Default
+ }
+
+ private fun givenDeviceTrustLevel(locallyVerified: Boolean?, crossSigningVerified: Boolean): DeviceTrustLevel {
+ return DeviceTrustLevel(
+ crossSigningVerified = crossSigningVerified,
+ locallyVerified = locallyVerified
+ )
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
new file mode 100644
index 0000000000..2a4c53f34f
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesViewNavigatorTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2
+
+import android.content.Intent
+import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
+import im.vector.app.test.fakes.FakeContext
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import io.mockk.verify
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+private const val A_SESSION_ID = "session_id"
+
+class VectorSettingsDevicesViewNavigatorTest {
+
+ private val context = FakeContext()
+ private val vectorSettingsDevicesViewNavigator = VectorSettingsDevicesViewNavigator()
+
+ @Before
+ fun setUp() {
+ mockkObject(SessionOverviewActivity.Companion)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given a session id when navigating to overview then it starts the correct activity`() {
+ val intent = givenIntentForSessionOverview(A_SESSION_ID)
+ context.givenStartActivity(intent)
+
+ vectorSettingsDevicesViewNavigator.navigateToSessionOverview(context.instance, A_SESSION_ID)
+
+ verify {
+ context.instance.startActivity(intent)
+ }
+ }
+
+ private fun givenIntentForSessionOverview(sessionId: String): Intent {
+ val intent = mockk()
+ every { SessionOverviewActivity.newIntent(context.instance, sessionId) } returns intent
+ return intent
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt
new file mode 100644
index 0000000000..e3d62961a7
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asFlow
+import im.vector.app.features.settings.devices.CurrentSessionCrossSigningInfo
+import im.vector.app.features.settings.devices.DeviceFullInfo
+import im.vector.app.features.settings.devices.GetCurrentSessionCrossSigningInfoUseCase
+import im.vector.app.features.settings.devices.GetEncryptionTrustLevelForDeviceUseCase
+import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
+import im.vector.app.test.fakes.FakeActiveSessionHolder
+import im.vector.app.test.fakes.FakeFlowLiveDataConversions
+import im.vector.app.test.fakes.givenAsFlow
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import io.mockk.verify
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.util.Optional
+
+private const val A_DEVICE_ID = "device-id"
+private const val A_TIMESTAMP = 123L
+
+class GetDeviceFullInfoUseCaseTest {
+
+ private val fakeActiveSessionHolder = FakeActiveSessionHolder()
+ private val getCurrentSessionCrossSigningInfoUseCase = mockk()
+ private val getEncryptionTrustLevelForDeviceUseCase = mockk()
+ private val checkIfSessionIsInactiveUseCase = mockk()
+ private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
+
+ private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase(
+ activeSessionHolder = fakeActiveSessionHolder.instance,
+ getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
+ getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
+ checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
+ )
+
+ @Before
+ fun setUp() {
+ fakeFlowLiveDataConversions.setup()
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `given current session and info for device when getting device info then the result is correct`() = runTest {
+ val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
+ val deviceInfo = DeviceInfo(
+ lastSeenTs = A_TIMESTAMP
+ )
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo))
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
+ val cryptoDeviceInfo = CryptoDeviceInfo(deviceId = A_DEVICE_ID, userId = "")
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData = MutableLiveData(Optional(cryptoDeviceInfo))
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow()
+ val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo)
+ val isInactive = false
+ every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive
+
+ val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
+
+ deviceFullInfo shouldBeEqualTo Optional(
+ DeviceFullInfo(
+ deviceInfo = deviceInfo,
+ cryptoDeviceInfo = cryptoDeviceInfo,
+ trustLevelForShield = trustLevel,
+ isInactive = isInactive,
+ )
+ )
+ verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
+ verify { getCurrentSessionCrossSigningInfoUseCase.execute() }
+ verify { getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) }
+ verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() }
+ verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() }
+ verify { checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP) }
+ }
+
+ @Test
+ fun `given current session and no info for device when getting device info then the result is null`() = runTest {
+ givenCurrentSessionCrossSigningInfo()
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(null))
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData = MutableLiveData(Optional(null))
+ fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow()
+
+ val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
+
+ deviceFullInfo shouldBeEqualTo Optional(null)
+ verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
+ verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() }
+ verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() }
+ }
+
+ @Test
+ fun `given no current session when getting device info then the result is empty`() = runTest {
+ fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
+
+ val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
+
+ deviceFullInfo shouldBeEqualTo null
+ verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
+ }
+
+ private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
+ val currentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(
+ deviceId = A_DEVICE_ID,
+ isCrossSigningInitialized = true,
+ isCrossSigningVerified = false
+ )
+ every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns currentSessionCrossSigningInfo
+ return currentSessionCrossSigningInfo
+ }
+
+ private fun givenTrustLevel(currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, cryptoDeviceInfo: CryptoDeviceInfo?): RoomEncryptionTrustLevel {
+ val trustLevel = RoomEncryptionTrustLevel.Trusted
+ every { getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) } returns trustLevel
+ return trustLevel
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
new file mode 100644
index 0000000000..735c553808
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2022 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.app.features.settings.devices.v2.overview
+
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.test.MvRxTestRule
+import im.vector.app.features.settings.devices.DeviceFullInfo
+import im.vector.app.test.fakes.FakeSession
+import im.vector.app.test.test
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Rule
+import org.junit.Test
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.util.Optional
+
+private const val A_SESSION_ID = "session-id"
+
+class SessionOverviewViewModelTest {
+
+ @get:Rule
+ val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())
+
+ private val args = SessionOverviewArgs(
+ deviceId = A_SESSION_ID
+ )
+ private val fakeSession = FakeSession()
+ private val getDeviceFullInfoUseCase = mockk()
+
+ private fun createViewModel() = SessionOverviewViewModel(
+ initialState = SessionOverviewViewState(args),
+ session = fakeSession,
+ getDeviceFullInfoUseCase = getDeviceFullInfoUseCase
+ )
+
+ @Test
+ fun `given the viewModel has been initialized then viewState is updated with session info`() {
+ val sessionParams = givenIdForSession(A_SESSION_ID)
+ val deviceFullInfo = mockk()
+ every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(Optional(deviceFullInfo))
+ val expectedState = SessionOverviewViewState(
+ deviceId = A_SESSION_ID,
+ isCurrentSession = true,
+ deviceInfo = Success(deviceFullInfo)
+ )
+
+ val viewModel = createViewModel()
+
+ viewModel.test()
+ .assertLatestState { state -> state == expectedState }
+ .finish()
+ verify { sessionParams.deviceId }
+ verify { getDeviceFullInfoUseCase.execute(A_SESSION_ID) }
+ }
+
+ private fun givenIdForSession(deviceId: String): SessionParams {
+ val sessionParams = mockk()
+ every { sessionParams.deviceId } returns deviceId
+ fakeSession.givenSessionParams(sessionParams)
+ return sessionParams
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/TestCoroutineDispatchers.kt b/vector/src/test/java/im/vector/app/test/TestCoroutineDispatchers.kt
index fb3c1bb70a..c4f4c2a19a 100644
--- a/vector/src/test/java/im/vector/app/test/TestCoroutineDispatchers.kt
+++ b/vector/src/test/java/im/vector/app/test/TestCoroutineDispatchers.kt
@@ -19,7 +19,7 @@ package im.vector.app.test
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-private val testDispatcher = UnconfinedTestDispatcher()
+internal val testDispatcher = UnconfinedTestDispatcher()
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(
io = testDispatcher,
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt
index 3065c18c30..bfc36ef06d 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt
@@ -33,4 +33,8 @@ class FakeActiveSessionHolder(
fun expectSetsActiveSession(session: Session) {
justRun { instance.setActiveSession(session) }
}
+
+ fun givenGetSafeActiveSessionReturns(session: Session?) {
+ every { instance.getSafeActiveSession() } returns session
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
index 329ac1bdae..d74ebcb678 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeContext.kt
@@ -18,11 +18,14 @@ package im.vector.app.test.fakes
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.net.ConnectivityManager
import android.net.Uri
import android.os.ParcelFileDescriptor
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
+import io.mockk.runs
import java.io.OutputStream
class FakeContext(
@@ -67,4 +70,8 @@ class FakeContext(
connectivityManager.givenHasActiveConnection()
givenService(Context.CONNECTIVITY_SERVICE, ConnectivityManager::class.java, connectivityManager.instance)
}
+
+ fun givenStartActivity(intent: Intent) {
+ every { instance.startActivity(intent) } just runs
+ }
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt
new file mode 100644
index 0000000000..e9a5365b1c
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCrossSigningService.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 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.app.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
+
+class FakeCrossSigningService : CrossSigningService by mockk() {
+
+ fun givenIsCrossSigningInitializedReturns(isInitialized: Boolean) {
+ every { isCrossSigningInitialized() } returns isInitialized
+ }
+
+ fun givenIsCrossSigningVerifiedReturns(isVerified: Boolean) {
+ every { isCrossSigningVerified() } returns isVerified
+ }
+}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
index ed571fc2f2..197ccf4cd2 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeCryptoService.kt
@@ -20,11 +20,19 @@ import androidx.lifecycle.MutableLiveData
import io.mockk.mockk
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.util.Optional
-class FakeCryptoService : CryptoService by mockk() {
+class FakeCryptoService(
+ val fakeCrossSigningService: FakeCrossSigningService = FakeCrossSigningService()
+) : CryptoService by mockk() {
var roomKeysExport = ByteArray(size = 1)
var cryptoDeviceInfos = mutableMapOf()
+ var cryptoDeviceInfoWithIdLiveData: MutableLiveData> = MutableLiveData()
+ var myDevicesInfoWithIdLiveData: MutableLiveData> = MutableLiveData()
+
+ override fun crossSigningService() = fakeCrossSigningService
override suspend fun exportRoomKeys(password: String) = roomKeysExport
@@ -35,4 +43,8 @@ class FakeCryptoService : CryptoService by mockk() {
override fun getLiveCryptoDeviceInfo(userIds: List) = MutableLiveData(
cryptoDeviceInfos.filterKeys { userIds.contains(it) }.values.toList()
)
+
+ override fun getLiveCryptoDeviceInfoWithId(deviceId: String) = cryptoDeviceInfoWithIdLiveData
+
+ override fun getMyDevicesInfoLive(deviceId: String) = myDevicesInfoWithIdLiveData
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt
index 9abbcc174d..956a86f32e 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeFlowLiveDataConversions.kt
@@ -28,6 +28,6 @@ class FakeFlowLiveDataConversions {
}
}
-fun LiveData.givenAsFlowReturns(value: T) {
- every { asFlow() } returns flowOf(value)
+fun LiveData.givenAsFlow() {
+ every { asFlow() } returns flowOf(value!!)
}
diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
index ee016ecae3..71bcde5807 100644
--- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
+++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt
@@ -26,6 +26,7 @@ import io.mockk.coJustRun
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
+import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
@@ -71,6 +72,10 @@ class FakeSession(
}
}
+ fun givenSessionParams(sessionParams: SessionParams) {
+ every { this@FakeSession.sessionParams } returns sessionParams
+ }
+
companion object {
fun withRoomSummary(roomSummary: RoomSummary) = FakeSession().apply {