Merge pull request #7171 from vector-im/feature/ons/device_manager_security_sessions

[Device Manager] Unverified and inactive sessions list (PSG-698, PSG-696)
This commit is contained in:
Onuray Sahin 2022-09-23 17:22:44 +03:00 committed by GitHub
commit 6c79aae3aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 210 additions and 42 deletions

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

@ -0,0 +1 @@
[Device Manager] Unverified and inactive sessions list

View File

@ -3244,6 +3244,7 @@
<string name="device_manager_other_sessions_description_verified">Verified · Last activity %1$s</string> <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 --> <!-- 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> <string name="device_manager_other_sessions_description_unverified">Unverified · Last activity %1$s</string>
<string name="device_manager_other_sessions_description_unverified_current_session">Unverified · Your current session</string>
<!-- Example: Inactive for 90+ days (Dec 25, 2021) --> <!-- Example: Inactive for 90+ days (Dec 25, 2021) -->
<plurals name="device_manager_other_sessions_description_inactive"> <plurals name="device_manager_other_sessions_description_inactive">
<item quantity="one">Inactive for %1$d+ day (%2$s)</item> <item quantity="one">Inactive for %1$d+ day (%2$s)</item>

View File

@ -25,4 +25,5 @@ data class DeviceFullInfo(
val cryptoDeviceInfo: CryptoDeviceInfo?, val cryptoDeviceInfo: CryptoDeviceInfo?,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
val isInactive: Boolean, val isInactive: Boolean,
val isCurrentDevice: Boolean,
) )

View File

@ -74,7 +74,7 @@ class DevicesViewModel @AssistedInject constructor(
.execute { async -> .execute { async ->
if (async is Success) { if (async is Success) {
val deviceFullInfoList = async.invoke() val deviceFullInfoList = async.invoke()
val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.isVerified.orFalse() } val unverifiedSessionsCount = deviceFullInfoList.count { !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse() }
val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive } val inactiveSessionsCount = deviceFullInfoList.count { it.isInactive }
copy( copy(
devices = async, devices = async,

View File

@ -71,7 +71,8 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive) val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice)
} }
} }
} }

View File

@ -37,9 +37,11 @@ import im.vector.app.core.resources.DrawableProvider
import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.databinding.FragmentSettingsDevicesBinding
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
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.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView
import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import javax.inject.Inject import javax.inject.Inject
@ -83,6 +85,7 @@ class VectorSettingsDevicesFragment :
initLearnMoreButtons() initLearnMoreButtons()
initWaitingView() initWaitingView()
initOtherSessionsView() initOtherSessionsView()
initSecurityRecommendationsView()
observeViewEvents() observeViewEvents()
} }
@ -124,6 +127,29 @@ class VectorSettingsDevicesFragment :
views.deviceListOtherSessions.callback = this views.deviceListOtherSessions.callback = this
} }
private fun initSecurityRecommendationsView() {
views.deviceListUnverifiedSessionsRecommendation.callback = object : SecurityRecommendationView.Callback {
override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions(
requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.UNVERIFIED,
excludeCurrentDevice = false
)
}
}
views.deviceListInactiveSessionsRecommendation.callback = object : SecurityRecommendationView.Callback {
override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions(
requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.INACTIVE,
excludeCurrentDevice = false
)
}
}
}
override fun onDestroyView() { override fun onDestroyView() {
cleanUpLearnMoreButtonsListeners() cleanUpLearnMoreButtonsListeners()
super.onDestroyView() super.onDestroyView()
@ -262,6 +288,11 @@ class VectorSettingsDevicesFragment :
} }
override fun onViewAllOtherSessionsClicked() { override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions(requireActivity()) viewNavigator.navigateToOtherSessions(
context = requireActivity(),
titleResourceId = R.string.device_manager_sessions_other_title,
defaultFilter = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = true
)
} }
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import android.content.Context import android.content.Context
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +28,14 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
} }
fun navigateToOtherSessions(context: Context) { fun navigateToOtherSessions(
context.startActivity(OtherSessionsActivity.newIntent(context)) context: Context,
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
) {
context.startActivity(
OtherSessionsActivity.newIntent(context, titleResourceId, defaultFilter, excludeCurrentDevice)
)
} }
} }

View File

@ -31,8 +31,8 @@ class FilterDevicesUseCase @Inject constructor() {
.filter { .filter {
when (filterType) { when (filterType) {
DeviceManagerFilterType.ALL_SESSIONS -> true DeviceManagerFilterType.ALL_SESSIONS -> true
DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse() DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse() DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
DeviceManagerFilterType.INACTIVE -> it.isInactive DeviceManagerFilterType.INACTIVE -> it.isInactive
} }
} }

View File

@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.list
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorInt
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
@ -45,6 +46,10 @@ abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.la
@EpoxyAttribute @EpoxyAttribute
var sessionDescription: String? = null var sessionDescription: String? = null
@EpoxyAttribute
@ColorInt
var sessionDescriptionColor: Int? = null
@EpoxyAttribute @EpoxyAttribute
var sessionDescriptionDrawable: Drawable? = null var sessionDescriptionDrawable: Drawable? = null
@ -82,6 +87,9 @@ abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.la
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
holder.otherSessionNameTextView.text = sessionName holder.otherSessionNameTextView.text = sessionName
holder.otherSessionDescriptionTextView.text = sessionDescription holder.otherSessionDescriptionTextView.text = sessionDescription
sessionDescriptionColor?.let {
holder.otherSessionDescriptionTextView.setTextColor(it)
}
holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null) holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null)
} }

View File

@ -53,20 +53,14 @@ class OtherSessionsController @Inject constructor(
data.forEach { device -> data.forEach { device ->
val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME
val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind) val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind)
val description = if (device.isInactive) { val description = calculateDescription(device, formattedLastActivityDate)
stringProvider.getQuantityString( val descriptionColor = if (device.isCurrentDevice) {
R.plurals.device_manager_other_sessions_description_inactive, host.colorProvider.getColorFromAttribute(R.attr.colorError)
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedLastActivityDate
)
} else if (device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
} else { } else {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate) host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
} }
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) val drawableColor = host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
val descriptionDrawable = if (device.isInactive) drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null val descriptionDrawable = if (device.isInactive) host.drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null
otherSessionItem { otherSessionItem {
id(device.deviceInfo.deviceId) id(device.deviceInfo.deviceId)
@ -75,10 +69,33 @@ class OtherSessionsController @Inject constructor(
sessionName(device.deviceInfo.displayName) sessionName(device.deviceInfo.displayName)
sessionDescription(description) sessionDescription(description)
sessionDescriptionDrawable(descriptionDrawable) sessionDescriptionDrawable(descriptionDrawable)
sessionDescriptionColor(descriptionColor)
stringProvider(this@OtherSessionsController.stringProvider) stringProvider(this@OtherSessionsController.stringProvider)
clickListener { device.deviceInfo.deviceId?.let { host.callback?.onItemClicked(it) } } clickListener { device.deviceInfo.deviceId?.let { host.callback?.onItemClicked(it) } }
} }
} }
} }
} }
private fun calculateDescription(device: DeviceFullInfo, formattedLastActivityDate: String): String {
return when {
device.isInactive -> {
stringProvider.getQuantityString(
R.plurals.device_manager_other_sessions_description_inactive,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedLastActivityDate
)
}
device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
}
device.isCurrentDevice -> {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified_current_session)
}
else -> {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
}
}
}
} }

View File

@ -31,7 +31,12 @@ class SecurityRecommendationView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) { ) : ConstraintLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onViewAllClicked()
}
private val views: ViewSecurityRecommendationBinding private val views: ViewSecurityRecommendationBinding
var callback: Callback? = null
init { init {
inflate(context, R.layout.view_security_recommendation, this) inflate(context, R.layout.view_security_recommendation, this)
@ -47,6 +52,10 @@ class SecurityRecommendationView @JvmOverloads constructor(
setDescription(it) setDescription(it)
setImage(it) setImage(it)
} }
views.recommendationViewAllButton.setOnClickListener {
callback?.onViewAllClicked()
}
} }
private fun setTitle(typedArray: TypedArray) { private fun setTitle(typedArray: TypedArray) {
@ -78,4 +87,9 @@ class SecurityRecommendationView @JvmOverloads constructor(
setDescription(viewState.description) setDescription(viewState.description)
setCount(viewState.sessionsCount) setCount(viewState.sessionsCount)
} }
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
callback = null
}
} }

View File

@ -20,9 +20,12 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.StringRes
import com.airbnb.mvrx.Mavericks
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
@AndroidEntryPoint @AndroidEntryPoint
class OtherSessionsActivity : SimpleFragmentActivity() { class OtherSessionsActivity : SimpleFragmentActivity() {
@ -35,14 +38,23 @@ class OtherSessionsActivity : SimpleFragmentActivity() {
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment( addFragment(
container = views.container, container = views.container,
fragmentClass = OtherSessionsFragment::class.java fragmentClass = OtherSessionsFragment::class.java,
params = intent.getParcelableExtra(Mavericks.KEY_ARG)
) )
} }
} }
companion object { companion object {
fun newIntent(context: Context): Intent { fun newIntent(
return Intent(context, OtherSessionsActivity::class.java) context: Context,
@StringRes
titleResourceId: Int,
defaultFilter: DeviceManagerFilterType,
excludeCurrentDevice: Boolean,
): Intent {
return Intent(context, OtherSessionsActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, OtherSessionsArgs(titleResourceId, defaultFilter, excludeCurrentDevice))
}
} }
} }
} }

View File

@ -0,0 +1,30 @@
/*
* 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.othersessions
import android.os.Parcelable
import androidx.annotation.StringRes
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.parcelize.Parcelize
@Parcelize
data class OtherSessionsArgs(
@StringRes
val titleResourceId: Int,
val defaultFilter: DeviceManagerFilterType,
val excludeCurrentDevice: Boolean,
) : Parcelable

View File

@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -46,6 +47,7 @@ class OtherSessionsFragment :
OtherSessionsView.Callback { OtherSessionsView.Callback {
private val viewModel: OtherSessionsViewModel by fragmentViewModel() private val viewModel: OtherSessionsViewModel by fragmentViewModel()
private val args: OtherSessionsArgs by args()
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@ -57,7 +59,7 @@ class OtherSessionsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).allowBack() setupToolbar(views.otherSessionsToolbar).setTitle(args.titleResourceId).allowBack()
observeViewEvents() observeViewEvents()
initFilterView() initFilterView()
} }
@ -85,6 +87,10 @@ class OtherSessionsFragment :
} }
views.deviceListOtherSessions.callback = this views.deviceListOtherSessions.callback = this
if (args.defaultFilter != DeviceManagerFilterType.ALL_SESSIONS) {
viewModel.handle(OtherSessionsAction.FilterDevices(args.defaultFilter))
}
} }
override fun onBottomSheetResult(resultCode: Int, data: Any?) { override fun onBottomSheetResult(resultCode: Int, data: Any?) {

View File

@ -30,7 +30,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
class OtherSessionsViewModel @AssistedInject constructor( class OtherSessionsViewModel @AssistedInject constructor(
@Assisted initialState: OtherSessionsViewState, @Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder, activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
refreshDevicesUseCase: RefreshDevicesUseCase refreshDevicesUseCase: RefreshDevicesUseCase
@ -55,7 +55,7 @@ class OtherSessionsViewModel @AssistedInject constructor(
observeDevicesJob?.cancel() observeDevicesJob?.cancel()
observeDevicesJob = getDeviceFullInfoListUseCase.execute( observeDevicesJob = getDeviceFullInfoListUseCase.execute(
filterType = currentFilter, filterType = currentFilter,
excludeCurrentDevice = true excludeCurrentDevice = initialState.excludeCurrentDevice
) )
.execute { async -> .execute { async ->
copy( copy(

View File

@ -25,4 +25,8 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
data class OtherSessionsViewState( data class OtherSessionsViewState(
val devices: Async<List<DeviceFullInfo>> = Uninitialized, val devices: Async<List<DeviceFullInfo>> = Uninitialized,
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
) : MavericksState val excludeCurrentDevice: Boolean = false,
) : MavericksState {
constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)
}

View File

@ -48,11 +48,13 @@ class GetDeviceFullInfoUseCase @Inject constructor(
val fullInfo = if (info != null && cryptoInfo != null) { val fullInfo = if (info != null && cryptoInfo != null) {
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo) val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId
DeviceFullInfo( DeviceFullInfo(
deviceInfo = info, deviceInfo = info,
cryptoDeviceInfo = cryptoInfo, cryptoDeviceInfo = cryptoInfo,
roomEncryptionTrustLevel = roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive isInactive = isInactive,
isCurrentDevice = isCurrentDevice,
) )
} else { } else {
null null

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import android.os.SystemClock
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
@ -30,11 +31,16 @@ import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.After
import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test 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.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@ -62,6 +68,17 @@ class DevicesViewModelTest {
) )
} }
@Before
fun setup() {
mockkStatic(SystemClock::class)
every { SystemClock.elapsedRealtime() } returns 1234
}
@After
fun tearDown() {
unmockkAll()
}
@Test @Test
fun `given the viewModel when initializing it then verification listener is added`() { fun `given the viewModel when initializing it then verification listener is added`() {
// Given // Given
@ -216,21 +233,23 @@ class DevicesViewModelTest {
*/ */
private fun givenDeviceFullInfoList(): List<DeviceFullInfo> { private fun givenDeviceFullInfoList(): List<DeviceFullInfo> {
val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>() val verifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { verifiedCryptoDeviceInfo.isVerified } returns true every { verifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>() val unverifiedCryptoDeviceInfo = mockk<CryptoDeviceInfo>()
every { unverifiedCryptoDeviceInfo.isVerified } returns false every { unverifiedCryptoDeviceInfo.trustLevel } returns DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
val deviceFullInfo1 = DeviceFullInfo( val deviceFullInfo1 = DeviceFullInfo(
deviceInfo = mockk(), deviceInfo = mockk(),
cryptoDeviceInfo = verifiedCryptoDeviceInfo, cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false isInactive = false,
isCurrentDevice = true
) )
val deviceFullInfo2 = DeviceFullInfo( val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(), deviceInfo = mockk(),
cryptoDeviceInfo = unverifiedCryptoDeviceInfo, cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true isInactive = true,
isCurrentDevice = false
) )
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList) val deviceFullInfoListFlow = flowOf(deviceFullInfoList)

View File

@ -109,19 +109,22 @@ class GetDeviceFullInfoListUseCaseTest {
deviceInfo = deviceInfo1, deviceInfo = deviceInfo1,
cryptoDeviceInfo = cryptoDeviceInfo1, cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true isInactive = true,
isCurrentDevice = true
) )
val expectedResult2 = DeviceFullInfo( val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2, deviceInfo = deviceInfo2,
cryptoDeviceInfo = cryptoDeviceInfo2, cryptoDeviceInfo = cryptoDeviceInfo2,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false isInactive = false,
isCurrentDevice = false
) )
val expectedResult3 = DeviceFullInfo( val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3, deviceInfo = deviceInfo3,
cryptoDeviceInfo = cryptoDeviceInfo3, cryptoDeviceInfo = cryptoDeviceInfo3,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false isInactive = false,
isCurrentDevice = false
) )
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1) val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult
@ -163,6 +166,7 @@ class GetDeviceFullInfoListUseCaseTest {
private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo {
val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>() val currentSessionCrossSigningInfo = mockk<CurrentSessionCrossSigningInfo>()
every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo) every { getCurrentSessionCrossSigningInfoUseCase.execute() } returns flowOf(currentSessionCrossSigningInfo)
every { currentSessionCrossSigningInfo.deviceId } returns A_DEVICE_ID_1
return currentSessionCrossSigningInfo return currentSessionCrossSigningInfo
} }

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import android.content.Intent import android.content.Intent
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
@ -30,6 +31,8 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
private const val A_SESSION_ID = "session_id" private const val A_SESSION_ID = "session_id"
private const val A_TITLE_RESOURCE_ID = 1234
private val A_DEFAULT_FILTER = DeviceManagerFilterType.INACTIVE
class VectorSettingsDevicesViewNavigatorTest { class VectorSettingsDevicesViewNavigatorTest {
@ -61,10 +64,10 @@ class VectorSettingsDevicesViewNavigatorTest {
@Test @Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() { fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
val intent = givenIntentForOtherSessions() val intent = givenIntentForOtherSessions(A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
context.givenStartActivity(intent) context.givenStartActivity(intent)
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance) vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance, A_TITLE_RESOURCE_ID, A_DEFAULT_FILTER, true)
verify { verify {
context.instance.startActivity(intent) context.instance.startActivity(intent)
@ -77,9 +80,9 @@ class VectorSettingsDevicesViewNavigatorTest {
return intent return intent
} }
private fun givenIntentForOtherSessions(): Intent { private fun givenIntentForOtherSessions(titleResourceId: Int, defaultFilter: DeviceManagerFilterType, excludeCurrentDevice: Boolean): Intent {
val intent = mockk<Intent>() val intent = mockk<Intent>()
every { OtherSessionsActivity.newIntent(context.instance) } returns intent every { OtherSessionsActivity.newIntent(context.instance, titleResourceId, defaultFilter, excludeCurrentDevice) } returns intent
return intent return intent
} }
} }

View File

@ -33,7 +33,8 @@ private val activeVerifiedDevice = DeviceFullInfo(
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false isInactive = false,
isCurrentDevice = true
) )
private val inactiveVerifiedDevice = DeviceFullInfo( private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
@ -43,7 +44,8 @@ private val inactiveVerifiedDevice = DeviceFullInfo(
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true) trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true isInactive = true,
isCurrentDevice = false
) )
private val activeUnverifiedDevice = DeviceFullInfo( private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
@ -53,7 +55,8 @@ private val activeUnverifiedDevice = DeviceFullInfo(
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false isInactive = false,
isCurrentDevice = false
) )
private val inactiveUnverifiedDevice = DeviceFullInfo( private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
@ -63,7 +66,8 @@ private val inactiveUnverifiedDevice = DeviceFullInfo(
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false) trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true isInactive = true,
isCurrentDevice = false
) )
private val devices = listOf( private val devices = listOf(

View File

@ -85,6 +85,7 @@ class GetDeviceFullInfoUseCaseTest {
fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow() fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow()
val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo) val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = false val isInactive = false
val isCurrentDevice = true
every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive
// When // When
@ -96,6 +97,7 @@ class GetDeviceFullInfoUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo, cryptoDeviceInfo = cryptoDeviceInfo,
roomEncryptionTrustLevel = trustLevel, roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive, isInactive = isInactive,
isCurrentDevice = isCurrentDevice
) )
verify { fakeActiveSessionHolder.instance.getSafeActiveSession() } verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
verify { getCurrentSessionCrossSigningInfoUseCase.execute() } verify { getCurrentSessionCrossSigningInfoUseCase.execute() }