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 @@ <string name="a11y_device_manager_device_type_unknown">Unknown device type</string> <string name="device_manager_verification_status_verified">Verified session</string> <string name="device_manager_verification_status_unverified">Unverified session</string> - <string name="device_manager_verification_status_detail_verified">Your current session is ready for secure messaging.</string> - <string name="device_manager_verification_status_detail_unverified">Verify your current session for enhanced secure messaging.</string> + <!-- TODO TO BE REMOVED: replaced by device_manager_verification_status_detail_current_session_verified --> + <string name="device_manager_verification_status_detail_verified" tools:ignore="UnusedResources">Your current session is ready for secure messaging.</string> + <!-- TODO TO BE REMOVED: replaced by device_manager_verification_status_detail_current_session_unverified --> + <string name="device_manager_verification_status_detail_unverified" tools:ignore="UnusedResources">Verify your current session for enhanced secure messaging.</string> + <string name="device_manager_verification_status_detail_current_session_verified">Your current session is ready for secure messaging.</string> + <string name="device_manager_verification_status_detail_other_session_verified">This session is ready for secure messaging.</string> + <string name="device_manager_verification_status_detail_current_session_unverified">Verify your current session for enhanced secure messaging.</string> + <string name="device_manager_verification_status_detail_other_session_unverified">Verify or sign out from this session for best security and reliability.</string> <string name="device_manager_verify_session">Verify Session</string> <string name="device_manager_view_details">View Details</string> - <string name="device_manager_header_section_current_session">Current Session</string> + <!-- TODO TO BE REMOVED: replaced by device_manager_current_session_title --> + <string name="device_manager_header_section_current_session" tools:ignore="UnusedResources">Current Session</string> <string name="device_manager_other_sessions_view_all">View All (%1$d)</string> + <!-- Examples: Verified · Last activity Yesterday at 6PM, Verified · Last activity Aug 31 at 5:47PM --> <string name="device_manager_other_sessions_description_verified">Verified · Last activity %1$s</string> + <!-- Examples: Unverified · Last activity Yesterday at 6PM, Unverified · Last activity Aug 31 at 5:47PM --> <string name="device_manager_other_sessions_description_unverified">Unverified · Last activity %1$s</string> <!-- Example: Inactive for 90+ days (Dec 25, 2021) --> <plurals name="device_manager_other_sessions_description_inactive"> @@ -3249,6 +3258,10 @@ <item quantity="one">Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.</item> <item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.</item> </plurals> + <string name="device_manager_current_session_title">Current Session</string> + <string name="device_manager_session_title">Session</string> + <!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> + <string name="device_manager_session_last_activity">Last activity %1$s</string> <!-- Note to translators: %s will be replaces with selected space name --> <string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string> 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 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <declare-styleable name="DevicesListHeaderView"> + <declare-styleable name="SessionsListHeaderView"> <attr name="devicesListHeaderTitle" format="string" /> <attr name="devicesListHeaderDescription" format="string" /> </declare-styleable> 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 @@ <style name="TextAppearance.Vector.Body.DevicesManagement"> <item name="android:textColor">?vctr_content_secondary</item> + <item name="android:drawablePadding">12dp</item> </style> </resources> 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<List<DeviceInfo>> { - 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<DeviceInfo>) + + fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> + + fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> + + fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> + + fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> + + fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> fun requestRoomKeyForEvent(event: Event) @@ -127,9 +140,9 @@ interface CryptoService { fun getMyDevicesInfo(): List<DeviceInfo> - fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> + fun getMyDevicesInfoLive(): LiveData<List<DeviceInfo>> - fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) + fun getMyDevicesInfoLive(deviceId: String): LiveData<Optional<DeviceInfo>> fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int @@ -156,14 +169,6 @@ interface CryptoService { fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>) - fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> - - fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> - - fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> - - fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>> - 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<List<DeviceInfo>> { + override fun getMyDevicesInfoLive(): LiveData<List<DeviceInfo>> { return cryptoStore.getLiveMyDevicesInfo() } + override fun getMyDevicesInfoLive(deviceId: String): LiveData<Optional<DeviceInfo>> { + return cryptoStore.getLiveMyDevicesInfo(deviceId) + } + override fun getMyDevicesInfo(): List<DeviceInfo> { return cryptoStore.getMyDevicesInfo() } - override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) { - 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<DeviceInfo>) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.executionThread = TaskThread.CRYPTO + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { return cryptoStore.getUserDeviceList(userId).orEmpty() } @@ -529,6 +534,10 @@ internal class DefaultCryptoService @Inject constructor( return cryptoStore.getLiveDeviceList() } + override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> { + return cryptoStore.getLiveDeviceWithId(deviceId) + } + override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> { 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<List<CryptoDeviceInfo>> + fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> + fun getMyDevicesInfo(): List<DeviceInfo> fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> + fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>> + fun saveMyDevicesInfo(info: List<DeviceInfo>) /** 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<Optional<CryptoDeviceInfo>> { + return Transformations.map(getLiveDeviceList()) { devices -> + devices.firstOrNull { it.deviceId == deviceId }.toOptional() + } + } + override fun getMyDevicesInfo(): List<DeviceInfo> { return monarchy.fetchAllCopiedSync { it.where<MyDeviceLastSeenInfoEntity>() @@ -596,17 +605,24 @@ internal class RealmCryptoStore @Inject constructor( { realm: Realm -> realm.where<MyDeviceLastSeenInfoEntity>() }, - { entity -> - DeviceInfo( - deviceId = entity.deviceId, - lastSeenIp = entity.lastSeenIp, - lastSeenTs = entity.lastSeenTs, - displayName = entity.displayName - ) - } + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } ) } + override fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>> { + val liveData = monarchy.findAllMappedWithChanges( + { realm: Realm -> + realm.where<MyDeviceLastSeenInfoEntity>() + .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId) + }, + { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) } + ) + + return Transformations.map(liveData) { + it.firstOrNull().toOptional() + } + } + override fun saveMyDevicesInfo(info: List<DeviceInfo>) { 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 @@ <activity android:name=".features.call.dialpad.PstnDialActivity" /> <activity android:name=".features.home.room.list.home.invites.InvitesActivity"/> <activity android:name=".features.home.room.list.home.release.ReleaseNotesActivity"/> + <activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity"/> <!-- Services --> 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<EncryptedEventContent>() ?.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<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener { var uiaContinuation: Continuation<UIABaseAuth>? = null @@ -116,8 +118,9 @@ class DevicesViewModel @AssistedInject constructor( private val refreshSource = PublishDataSource<Unit>() 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<FragmentSettingsDevicesBinding>() { + @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<Unit>) { 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<OtherSessionItem.Holder>(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<List<DeviceFullInfo>>() { + var callback: Callback? = null + + interface Callback { + fun onItemClicked(deviceId: String) + } + override fun buildModels(data: List<DeviceFullInfo>?) { 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<Optional<DeviceFullInfo>> { + 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<FragmentSessionOverviewBinding>() { + + @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<SessionOverviewViewState, SessionOverviewAction, EmptyViewEvents>(initialState) { + + companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> by hiltMavericksViewModelFactory() + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> { + 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<DeviceFullInfo> = 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <im.vector.app.features.settings.devices.v2.list.SessionInfoView + android:id="@+id/sessionOverviewInfo" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="24dp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + +</androidx.constraintlayout.widget.ConstraintLayout> 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"> - <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView + <im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView android:id="@+id/deviceListHeaderSectionSecurityRecommendations" android:layout_width="0dp" android:layout_height="wrap_content" @@ -56,17 +56,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/deviceListInactiveSessionsRecommendation" /> - <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView + <im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView android:id="@+id/deviceListHeaderCurrentSession" android:layout_width="0dp" android:layout_height="wrap_content" app:devicesListHeaderDescription="" - app:devicesListHeaderTitle="@string/device_manager_header_section_current_session" + app:devicesListHeaderTitle="@string/device_manager_current_session_title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider" /> - <im.vector.app.features.settings.devices.v2.list.CurrentSessionView + <im.vector.app.features.settings.devices.v2.list.SessionInfoView android:id="@+id/deviceListCurrentSession" android:layout_width="0dp" android:layout_height="wrap_content" @@ -86,7 +86,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/deviceListCurrentSession" /> - <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView + <im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView android:id="@+id/deviceListHeaderOtherSessions" android:layout_width="0dp" android:layout_height="wrap_content" diff --git a/vector/src/main/res/layout/item_other_session.xml b/vector/src/main/res/layout/item_other_session.xml index 2c41ce6a56..2f93c2be5d 100644 --- a/vector/src/main/res/layout/item_other_session.xml +++ b/vector/src/main/res/layout/item_other_session.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:foreground="?selectableItemBackground" android:paddingTop="16dp"> <ImageView diff --git a/vector/src/main/res/layout/view_current_session.xml b/vector/src/main/res/layout/view_session_info.xml similarity index 60% rename from vector/src/main/res/layout/view_current_session.xml rename to vector/src/main/res/layout/view_session_info.xml index 91977eba40..18daae825a 100644 --- a/vector/src/main/res/layout/view_current_session.xml +++ b/vector/src/main/res/layout/view_session_info.xml @@ -8,7 +8,7 @@ android:paddingBottom="16dp"> <ImageView - android:id="@+id/currentSessionDeviceTypeImageView" + android:id="@+id/sessionInfoDeviceTypeImageView" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginTop="16dp" @@ -21,18 +21,18 @@ tools:src="@drawable/ic_device_type_mobile" /> <TextView - android:id="@+id/currentSessionNameTextView" + android:id="@+id/sessionInfoNameTextView" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionDeviceTypeImageView" + app:layout_constraintTop_toBottomOf="@id/sessionInfoDeviceTypeImageView" tools:text="Element Mobile: Android" /> <LinearLayout - android:id="@+id/currentSessionVerificationStatusContainer" + android:id="@+id/sessionInfoVerificationStatusContainer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" @@ -40,17 +40,17 @@ android:orientation="horizontal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionNameTextView"> + app:layout_constraintTop_toBottomOf="@id/sessionInfoNameTextView"> <im.vector.app.core.ui.views.ShieldImageView - android:id="@+id/currentSessionVerificationStatusImageView" + android:id="@+id/sessionInfoVerificationStatusImageView" android:layout_width="16dp" android:layout_height="16dp" android:importantForAccessibility="no" tools:src="@drawable/ic_shield_trusted" /> <TextView - android:id="@+id/currentSessionVerificationStatusTextView" + android:id="@+id/sessionInfoVerificationStatusTextView" style="@style/TextAppearance.Vector.Body" android:layout_width="wrap_content" android:layout_height="wrap_content" @@ -60,7 +60,7 @@ </LinearLayout> <TextView - android:id="@+id/currentSessionVerificationStatusDetailTextView" + android:id="@+id/sessionInfoVerificationStatusDetailTextView" style="@style/TextAppearance.Vector.Body.DevicesManagement" android:layout_width="0dp" android:layout_height="wrap_content" @@ -69,11 +69,40 @@ android:gravity="center" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionVerificationStatusContainer" - tools:text="@string/device_manager_verification_status_detail_verified" /> + app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusContainer" + tools:text="@string/device_manager_verification_status_detail_current_session_verified" /> + + <TextView + android:id="@+id/sessionInfoLastActivityTextView" + style="@style/TextAppearance.Vector.Body.DevicesManagement" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="32dp" + android:layout_marginTop="12dp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusDetailTextView" + app:layout_constraintWidth="wrap_content_constrained" + tools:text="Last activity Fri 14:59" + tools:visibility="visible" /> + + <TextView + android:id="@+id/sessionInfoLastIPAddressTextView" + style="@style/TextAppearance.Vector.Body.DevicesManagement" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="32dp" + android:gravity="center" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/sessionInfoLastActivityTextView" + tools:text="81.235.41.100 (United Kingdom)" + tools:visibility="visible" /> <Button - android:id="@+id/currentSessionVerifySessionButton" + android:id="@+id/sessionInfoVerifySessionButton" android:layout_width="0dp" android:layout_height="52dp" android:layout_marginHorizontal="24dp" @@ -81,10 +110,10 @@ android:text="@string/device_manager_verify_session" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionVerificationStatusDetailTextView" /> + app:layout_constraintTop_toBottomOf="@id/sessionInfoLastIPAddressTextView" /> <Button - android:id="@+id/currentSessionViewDetailsButton" + android:id="@+id/sessionInfoViewDetailsButton" style="@style/Widget.Vector.Button.Text" android:layout_width="0dp" android:layout_height="wrap_content" @@ -93,6 +122,6 @@ android:text="@string/device_manager_view_details" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionVerifySessionButton" /> + app:layout_constraintTop_toBottomOf="@id/sessionInfoVerifySessionButton" /> </androidx.constraintlayout.widget.ConstraintLayout> 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"> <TextView - android:id="@+id/devices_list_header_title" + android:id="@+id/sessions_list_header_title" style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" android:layout_width="0dp" android:layout_height="wrap_content" @@ -19,14 +19,14 @@ tools:text="Other sessions" /> <TextView - android:id="@+id/devices_list_header_description" + android:id="@+id/sessions_list_header_description" style="@style/TextAppearance.Vector.Body.DevicesManagement" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="18.5dp" android:layout_marginEnd="40dp" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/devices_list_header_title" - app:layout_constraintTop_toBottomOf="@id/devices_list_header_title" + app:layout_constraintStart_toStartOf="@id/sessions_list_header_title" + app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title" tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." /> </merge> 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<SessionParams>() + 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<GetEncryptionTrustLevelForCurrentDeviceUseCase>() + private val getEncryptionTrustLevelForOtherDeviceUseCase = mockk<GetEncryptionTrustLevelForOtherDeviceUseCase>() + + 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<Intent>() + 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<GetCurrentSessionCrossSigningInfoUseCase>() + private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>() + private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>() + 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<GetDeviceFullInfoUseCase>() + + 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<DeviceFullInfo>() + 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<SessionParams>() + 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<String, CryptoDeviceInfo>() + var cryptoDeviceInfoWithIdLiveData: MutableLiveData<Optional<CryptoDeviceInfo>> = MutableLiveData() + var myDevicesInfoWithIdLiveData: MutableLiveData<Optional<DeviceInfo>> = 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<String>) = 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 <T> LiveData<T>.givenAsFlowReturns(value: T) { - every { asFlow() } returns flowOf(value) +fun <T> LiveData<T>.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 {