mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
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:
commit
6c2bf35d60
1
changelog.d/6961.wip
Normal file
1
changelog.d/6961.wip
Normal file
@ -0,0 +1 @@
|
||||
[Devices Management] Session overview screen
|
@ -3227,13 +3227,22 @@
|
||||
<string name="a11y_device_manager_device_type_unknown">Unknown device type</string>
|
||||
<string name="device_manager_verification_status_verified">Verified session</string>
|
||||
<string name="device_manager_verification_status_unverified">Unverified session</string>
|
||||
<string name="device_manager_verification_status_detail_verified">Your current session is ready for secure messaging.</string>
|
||||
<string name="device_manager_verification_status_detail_unverified">Verify your current session for enhanced secure messaging.</string>
|
||||
<!-- TODO TO BE REMOVED: replaced by device_manager_verification_status_detail_current_session_verified -->
|
||||
<string name="device_manager_verification_status_detail_verified" tools:ignore="UnusedResources">Your current session is ready for secure messaging.</string>
|
||||
<!-- TODO TO BE REMOVED: replaced by device_manager_verification_status_detail_current_session_unverified -->
|
||||
<string name="device_manager_verification_status_detail_unverified" tools:ignore="UnusedResources">Verify your current session for enhanced secure messaging.</string>
|
||||
<string name="device_manager_verification_status_detail_current_session_verified">Your current session is ready for secure messaging.</string>
|
||||
<string name="device_manager_verification_status_detail_other_session_verified">This session is ready for secure messaging.</string>
|
||||
<string name="device_manager_verification_status_detail_current_session_unverified">Verify your current session for enhanced secure messaging.</string>
|
||||
<string name="device_manager_verification_status_detail_other_session_unverified">Verify or sign out from this session for best security and reliability.</string>
|
||||
<string name="device_manager_verify_session">Verify Session</string>
|
||||
<string name="device_manager_view_details">View Details</string>
|
||||
<string name="device_manager_header_section_current_session">Current Session</string>
|
||||
<!-- TODO TO BE REMOVED: replaced by device_manager_current_session_title -->
|
||||
<string name="device_manager_header_section_current_session" tools:ignore="UnusedResources">Current Session</string>
|
||||
<string name="device_manager_other_sessions_view_all">View All (%1$d)</string>
|
||||
<!-- Examples: Verified · Last activity Yesterday at 6PM, Verified · Last activity Aug 31 at 5:47PM -->
|
||||
<string name="device_manager_other_sessions_description_verified">Verified · Last activity %1$s</string>
|
||||
<!-- Examples: Unverified · Last activity Yesterday at 6PM, Unverified · Last activity Aug 31 at 5:47PM -->
|
||||
<string name="device_manager_other_sessions_description_unverified">Unverified · Last activity %1$s</string>
|
||||
<!-- Example: Inactive for 90+ days (Dec 25, 2021) -->
|
||||
<plurals name="device_manager_other_sessions_description_inactive">
|
||||
@ -3249,6 +3258,10 @@
|
||||
<item quantity="one">Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.</item>
|
||||
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.</item>
|
||||
</plurals>
|
||||
<string name="device_manager_current_session_title">Current Session</string>
|
||||
<string name="device_manager_session_title">Session</string>
|
||||
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
|
||||
<string name="device_manager_session_last_activity">Last activity %1$s</string>
|
||||
|
||||
<!-- Note to translators: %s will be replaces with selected space name -->
|
||||
<string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>)
|
||||
|
||||
/**
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 -->
|
||||
|
||||
|
@ -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<*, *>
|
||||
}
|
||||
|
@ -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 -> {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
)
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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>) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
20
vector/src/main/res/layout/fragment_session_overview.xml
Normal file
20
vector/src/main/res/layout/fragment_session_overview.xml
Normal 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>
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -7,7 +7,7 @@
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/devices_list_header_title"
|
||||
android:id="@+id/sessions_list_header_title"
|
||||
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
@ -19,14 +19,14 @@
|
||||
tools:text="Other sessions" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/devices_list_header_description"
|
||||
android:id="@+id/sessions_list_header_description"
|
||||
style="@style/TextAppearance.Vector.Body.DevicesManagement"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18.5dp"
|
||||
android:layout_marginEnd="40dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/devices_list_header_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/devices_list_header_title"
|
||||
app:layout_constraintStart_toStartOf="@id/sessions_list_header_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
|
||||
tools:text="For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Learn More." />
|
||||
</merge>
|
@ -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()
|
||||
|
||||
|
@ -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 = ""),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -33,4 +33,8 @@ class FakeActiveSessionHolder(
|
||||
fun expectSetsActiveSession(session: Session) {
|
||||
justRun { instance.setActiveSession(session) }
|
||||
}
|
||||
|
||||
fun givenGetSafeActiveSessionReturns(session: Session?) {
|
||||
every { instance.getSafeActiveSession() } returns session
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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!!)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user