diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 11fc6ecd64..7355465aa0 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -30,10 +30,13 @@ import im.vector.app.R import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.resources.DateProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.resources.toTimestamp import im.vector.app.core.utils.PublishDataSource import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.login.ReAuthHelper +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine @@ -52,6 +55,7 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel @@ -67,6 +71,7 @@ import org.matrix.android.sdk.api.util.awaitCallback import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.flow.flow import timber.log.Timber +import java.util.concurrent.TimeUnit import javax.net.ssl.HttpsURLConnection import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -81,6 +86,8 @@ data class DevicesViewState( val request: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, val accountCrossSigningIsTrusted: Boolean = false, + val unverifiedSessionsCount: Int = 0, + val inactiveSessionsCount: Int = 0, ) : MavericksState data class DeviceFullInfo( @@ -125,6 +132,14 @@ class DevicesViewModel @AssistedInject constructor( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() ) { cryptoList, infoList -> + val unverifiedSessionsCount = cryptoList.count { !it.trustLevel?.isVerified().orFalse() } + val inactiveSessionsCount = infoList.count { isInactiveSession(it.date) } + setState { + copy( + unverifiedSessionsCount = unverifiedSessionsCount, + inactiveSessionsCount = inactiveSessionsCount + ) + } infoList .sortedByDescending { it.lastSeenTs } .map { deviceInfo -> @@ -188,6 +203,14 @@ class DevicesViewModel @AssistedInject constructor( queryRefreshDevicesList() } + private fun isInactiveSession(lastSeenTs: Long): Boolean { + val lastSeenDate = DateProvider.toLocalDateTime(lastSeenTs) + val currentDate = DateProvider.currentLocalDateTime() + val diffMilliseconds = currentDate.toTimestamp() - lastSeenDate.toTimestamp() + val diffDays = TimeUnit.MILLISECONDS.toDays(diffMilliseconds) + return diffDays > SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + } + override fun onCleared() { session.cryptoService().verificationService().removeListener(this) super.onCleared() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 80dfe25c77..245cfe9809 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -131,9 +131,11 @@ class VectorSettingsDevicesFragment : } val otherDevices = devices?.filter { it.deviceInfo.deviceId != state.myDeviceId } + renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount) renderCurrentDevice(currentDeviceInfo) renderOtherSessionsView(otherDevices) } else { + hideSecurityRecommendations() hideCurrentSessionView() hideOtherSessionsView() } @@ -141,6 +143,26 @@ class VectorSettingsDevicesFragment : handleRequestStatus(state.request) } + private fun renderSecurityRecommendations(inactiveSessionsCount: Int, unverifiedSessionsCount: Int) { + if (unverifiedSessionsCount == 0 && inactiveSessionsCount == 0) { + hideSecurityRecommendations() + } else { + views.deviceListHeaderSectionSecurityRecommendations.isVisible = true + views.deviceListSecurityRecommendationsDivider.isVisible = true + views.deviceListUnverifiedSessionsRecommendation.isVisible = unverifiedSessionsCount > 0 + views.deviceListInactiveSessionsRecommendation.isVisible = inactiveSessionsCount > 0 + views.deviceListUnverifiedSessionsRecommendation.setCount(unverifiedSessionsCount) + views.deviceListInactiveSessionsRecommendation.setCount(inactiveSessionsCount) + } + } + + private fun hideSecurityRecommendations() { + views.deviceListHeaderSectionSecurityRecommendations.isVisible = false + views.deviceListUnverifiedSessionsRecommendation.isVisible = false + views.deviceListInactiveSessionsRecommendation.isVisible = false + views.deviceListSecurityRecommendationsDivider.isVisible = false + } + private fun renderOtherSessionsView(otherDevices: List?) { if (otherDevices.isNullOrEmpty()) { hideOtherSessionsView() @@ -169,6 +191,7 @@ class VectorSettingsDevicesFragment : private fun hideCurrentSessionView() { views.deviceListHeaderCurrentSession.isVisible = false views.deviceListCurrentSession.isVisible = false + views.deviceListCurrentSessionDivider.isVisible = false } private fun handleRequestStatus(unIgnoreRequest: Async) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt index f2f653183c..73d8d74bfd 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt @@ -65,4 +65,8 @@ class SecurityRecommendationView @JvmOverloads constructor( views.recommendationShieldImageView.setImageResource(imageResource) views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTint) } + + fun setCount(sessionsCount: Int) { + views.recommendationViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, sessionsCount) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt index c1dbbdff4f..662ce536e7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt @@ -17,3 +17,4 @@ package im.vector.app.features.settings.devices.v2.list internal const val NUMBER_OF_OTHER_DEVICES_TO_RENDER = 5 +internal const val SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS = 90 diff --git a/vector/src/main/res/layout/view_security_recommendation.xml b/vector/src/main/res/layout/view_security_recommendation.xml index 1fe62761a6..b5e7fb9beb 100644 --- a/vector/src/main/res/layout/view_security_recommendation.xml +++ b/vector/src/main/res/layout/view_security_recommendation.xml @@ -32,10 +32,11 @@