Merge pull request #7005 from vector-im/feature/mna/session-overview-screen

[Devices Management] Session overview screen (PSG-691, PSG-693)
This commit is contained in:
Maxime NATUREL 2022-09-08 09:18:31 +02:00 committed by GitHub
commit 6c2bf35d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 1836 additions and 186 deletions

1
changelog.d/6961.wip Normal file
View File

@ -0,0 +1 @@
[Devices Management] Session overview screen

View File

@ -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 dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont 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>

View File

@ -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>

View File

@ -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>

View File

@ -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()
}

View File

@ -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()
)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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>)
/**

View File

@ -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(

View File

@ -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
)
}
}

View File

@ -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
}
}

View File

@ -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 -->

View File

@ -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<*, *>
}

View File

@ -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 -> {

View File

@ -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) {

View File

@ -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,
)

View File

@ -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()

View File

@ -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
)
}
}

View File

@ -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
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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(

View File

@ -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>) {

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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) } }
}
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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,
)

View File

@ -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

View File

@ -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()
}
}

View File

@ -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

View File

@ -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))
}
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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")
}
}

View File

@ -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
)
}

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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 dont recognize or use anymore. Learn More." />
</merge>

View File

@ -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()

View File

@ -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 = ""),

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
)
}
}

View File

@ -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
)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -33,4 +33,8 @@ class FakeActiveSessionHolder(
fun expectSetsActiveSession(session: Session) {
justRun { instance.setActiveSession(session) }
}
fun givenGetSafeActiveSessionReturns(session: Session?) {
every { instance.getSafeActiveSession() } returns session
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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!!)
}

View File

@ -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 {