diff --git a/changelog.d/6964.wip b/changelog.d/6964.wip
new file mode 100644
index 0000000000..f96dfe41ed
--- /dev/null
+++ b/changelog.d/6964.wip
@@ -0,0 +1 @@
+[Device Manager] Render Security Recommendations
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index f8eb4b8de0..df0e10627a 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3225,5 +3225,19 @@
View All (%1$d)
Verified · Last activity %1$s
Unverified · Last activity %1$s
+
+
+ - Inactive for %1$d+ day (%2$s)
+ - Inactive for %1$d+ days (%2$s)
+
+ Security recommendations
+ Improve your account security by following these recommendations.
+ Unverified sessions
+ Verify or sign out from unverified sessions.
+ Inactive sessions
+
+ - Consider signing out from old sessions (%1$d day or more) that you don’t use anymore.
+ - Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.
+
diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml
index e72d02f51e..01af740d43 100644
--- a/library/ui-styles/src/main/res/values/colors.xml
+++ b/library/ui-styles/src/main/res/values/colors.xml
@@ -143,6 +143,7 @@
#0DBD8B
#17191C
#FF4B55
+ #0FFF4B55
diff --git a/library/ui-styles/src/main/res/values/stylable_security_recommendation_view.xml b/library/ui-styles/src/main/res/values/stylable_security_recommendation_view.xml
new file mode 100644
index 0000000000..4283c8da8a
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_security_recommendation_view.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/java/im/vector/app/core/date/DateFormatKind.kt b/vector/src/main/java/im/vector/app/core/date/DateFormatKind.kt
index b08a1dc725..a66066ac8b 100644
--- a/vector/src/main/java/im/vector/app/core/date/DateFormatKind.kt
+++ b/vector/src/main/java/im/vector/app/core/date/DateFormatKind.kt
@@ -27,7 +27,7 @@ enum class DateFormatKind {
// Will show hour or date relative (9:30am or yesterday or Sep 7 or 09/07/2020)
ROOM_LIST,
- // Will show full date (Sep 7 2020)
+ // Will show full date (Sep 7, 2020)
TIMELINE_DAY_DIVIDER,
// Will show full date and time (Mon, Sep 7 2020, 9:30am)
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..3b5bcb61d9 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
@@ -34,6 +34,7 @@ import im.vector.app.core.resources.StringProvider
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.CheckIfSessionIsInactiveUseCase
import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
@@ -52,6 +53,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
@@ -81,12 +83,15 @@ 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(
val deviceInfo: DeviceInfo,
val cryptoDeviceInfo: CryptoDeviceInfo?,
val trustLevelForShield: RoomEncryptionTrustLevel,
+ val isInactive: Boolean,
)
class DevicesViewModel @AssistedInject constructor(
@@ -95,6 +100,7 @@ class DevicesViewModel @AssistedInject constructor(
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider,
private val matrix: Matrix,
+ private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
) : VectorViewModel(initialState), VerificationService.Listener {
var uiaContinuation: Continuation? = null
@@ -125,6 +131,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 { checkIfSessionIsInactiveUseCase.execute(it.date) }
+ setState {
+ copy(
+ unverifiedSessionsCount = unverifiedSessionsCount,
+ inactiveSessionsCount = inactiveSessionsCount
+ )
+ }
infoList
.sortedByDescending { it.lastSeenTs }
.map { deviceInfo ->
@@ -135,7 +149,8 @@ class DevicesViewModel @AssistedInject constructor(
deviceTrustLevel = cryptoDeviceInfo?.trustLevel,
isCurrentDevice = deviceInfo.deviceId == session.sessionParams.deviceId
)
- DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield)
+ val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
+ DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield, isInactive)
}
}
.distinctUntilChanged()
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..78b8c66f9c 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
@@ -40,6 +40,8 @@ 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.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
+import im.vector.app.features.settings.devices.v2.list.SecurityRecommendationViewState
/**
* Display the list of the user's devices and sessions.
@@ -131,9 +133,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 +145,38 @@ 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
+ val unverifiedSessionsViewState = SecurityRecommendationViewState(
+ description = getString(R.string.device_manager_unverified_sessions_description),
+ sessionsCount = unverifiedSessionsCount,
+ )
+ views.deviceListUnverifiedSessionsRecommendation.render(unverifiedSessionsViewState)
+ val inactiveSessionsViewState = SecurityRecommendationViewState(
+ description = resources.getQuantityString(
+ R.plurals.device_manager_inactive_sessions_description,
+ SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
+ SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
+ ),
+ sessionsCount = inactiveSessionsCount,
+ )
+ views.deviceListInactiveSessionsRecommendation.render(inactiveSessionsViewState)
+ }
+ }
+
+ 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 +205,7 @@ class VectorSettingsDevicesFragment :
private fun hideCurrentSessionView() {
views.deviceListHeaderCurrentSession.isVisible = false
views.deviceListCurrentSession.isVisible = false
+ views.deviceListDividerCurrentSession.isVisible = false
}
private fun handleRequestStatus(unIgnoreRequest: Async) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt
new file mode 100644
index 0000000000..8991ad1e3d
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCase.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.core.time.Clock
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+class CheckIfSessionIsInactiveUseCase @Inject constructor(
+ private val clock: Clock,
+) {
+
+ fun execute(lastSeenTs: Long): Boolean {
+ // In case of the server doesn't send the last seen date.
+ if (lastSeenTs == 0L) return true
+
+ val diffMilliseconds = clock.epochMillis() - lastSeenTs
+ return diffMilliseconds >= TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong())
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
index 2a62100994..e9376953e0 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2.list
+import android.graphics.drawable.Drawable
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
@@ -42,6 +43,9 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la
@EpoxyAttribute
var sessionDescription: String? = null
+ @EpoxyAttribute
+ var sessionDescriptionDrawable: Drawable? = null
+
@EpoxyAttribute
lateinit var stringProvider: StringProvider
@@ -68,6 +72,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
holder.otherSessionNameTextView.text = sessionName
holder.otherSessionDescriptionTextView.text = sessionDescription
+ holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null)
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
index 44c73e6eb7..8a5ee05af7 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt
@@ -21,6 +21,8 @@ import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.noResultItem
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.settings.devices.DeviceFullInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@@ -29,6 +31,8 @@ import javax.inject.Inject
class OtherSessionsController @Inject constructor(
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter,
+ private val drawableProvider: DrawableProvider,
+ private val colorProvider: ColorProvider,
) : TypedEpoxyController>() {
override fun buildModels(data: List?) {
@@ -41,12 +45,22 @@ class OtherSessionsController @Inject constructor(
}
} else {
data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device ->
- val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
- val description = if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
+ 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 description = if (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
+ )
+ } else if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
} else {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
}
+ val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
+ val descriptionDrawable = if (device.isInactive) drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor) else null
otherSessionItem {
id(device.deviceInfo.deviceId)
@@ -54,6 +68,7 @@ class OtherSessionsController @Inject constructor(
roomEncryptionTrustLevel(device.trustLevelForShield)
sessionName(device.deviceInfo.displayName)
sessionDescription(description)
+ sessionDescriptionDrawable(descriptionDrawable)
stringProvider(this@OtherSessionsController.stringProvider)
}
}
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
new file mode 100644
index 0000000000..93cf3c0501
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationView.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.content.res.ColorStateList
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.res.use
+import im.vector.app.R
+import im.vector.app.databinding.ViewSecurityRecommendationBinding
+
+class SecurityRecommendationView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val views: ViewSecurityRecommendationBinding
+
+ init {
+ inflate(context, R.layout.view_security_recommendation, this)
+ views = ViewSecurityRecommendationBinding.bind(this)
+
+ context.obtainStyledAttributes(
+ attrs,
+ R.styleable.SecurityRecommendationView,
+ 0,
+ 0
+ ).use {
+ setTitle(it)
+ setDescription(it)
+ setImage(it)
+ }
+ }
+
+ private fun setTitle(typedArray: TypedArray) {
+ val title = typedArray.getString(R.styleable.SecurityRecommendationView_recommendationTitle)
+ views.recommendationTitleTextView.text = title
+ }
+
+ private fun setDescription(typedArray: TypedArray) {
+ val description = typedArray.getString(R.styleable.SecurityRecommendationView_recommendationDescription)
+ setDescription(description)
+ }
+
+ private fun setImage(typedArray: TypedArray) {
+ val imageResource = typedArray.getResourceId(R.styleable.SecurityRecommendationView_recommendationImageResource, 0)
+ val backgroundTint = typedArray.getColor(R.styleable.SecurityRecommendationView_recommendationImageBackgroundTint, 0)
+ views.recommendationShieldImageView.setImageResource(imageResource)
+ views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTint)
+ }
+
+ private fun setDescription(description: String?) {
+ views.recommendationDescriptionTextView.text = description
+ }
+
+ private fun setCount(sessionsCount: Int) {
+ views.recommendationViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, sessionsCount)
+ }
+
+ fun render(viewState: SecurityRecommendationViewState) {
+ setDescription(viewState.description)
+ setCount(viewState.sessionsCount)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationViewState.kt
new file mode 100644
index 0000000000..a5b30ea13b
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SecurityRecommendationViewState.kt
@@ -0,0 +1,22 @@
+/*
+ * 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
+
+data class SecurityRecommendationViewState(
+ val description: String,
+ val sessionsCount: Int,
+)
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/drawable/bg_security_recommendation_shield.xml b/vector/src/main/res/drawable/bg_security_recommendation_shield.xml
new file mode 100644
index 0000000000..3d46fd4ff6
--- /dev/null
+++ b/vector/src/main/res/drawable/bg_security_recommendation_shield.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_inactive_sessions.xml b/vector/src/main/res/drawable/ic_inactive_sessions.xml
new file mode 100644
index 0000000000..30ee2b3b3c
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_inactive_sessions.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml
index 1367835d2c..6710f345ce 100644
--- a/vector/src/main/res/layout/fragment_settings_devices.xml
+++ b/vector/src/main/res/layout/fragment_settings_devices.xml
@@ -8,6 +8,54 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider" />
diff --git a/vector/src/main/res/layout/view_current_session.xml b/vector/src/main/res/layout/view_current_session.xml
index 31ad3cce2c..91977eba40 100644
--- a/vector/src/main/res/layout/view_current_session.xml
+++ b/vector/src/main/res/layout/view_current_session.xml
@@ -12,12 +12,12 @@
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
+ android:background="@drawable/bg_device_type"
android:contentDescription="@string/a11y_device_manager_device_type_mobile"
android:padding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
- tools:background="@drawable/bg_device_type"
tools:src="@drawable/ic_device_type_mobile" />
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt
new file mode 100644
index 0000000000..b7d56a88bf
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/list/CheckIfSessionIsInactiveUseCaseTest.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.test.fakes.FakeClock
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+private const val A_TIMESTAMP = 1654689143L
+
+class CheckIfSessionIsInactiveUseCaseTest {
+
+ private val clock = FakeClock().apply { givenEpoch(A_TIMESTAMP) }
+ private val checkIfSessionIsInactiveUseCase = CheckIfSessionIsInactiveUseCase(clock)
+
+ @Test
+ fun `given an old last seen date then session is inactive`() {
+ val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) - 1
+
+ checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true
+ }
+
+ @Test
+ fun `given a last seen date equal to the threshold then session is inactive`() {
+ val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong())
+
+ checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true
+ }
+
+ @Test
+ fun `given a recent last seen date then session is active`() {
+ val lastSeenDate = A_TIMESTAMP - TimeUnit.DAYS.toMillis(SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS.toLong()) + 1
+
+ checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo false
+ }
+
+ @Test
+ fun `given a last seen date as zero then session is inactive`() {
+ // In case of the server doesn't send the last seen date.
+ val lastSeenDate = 0L
+
+ checkIfSessionIsInactiveUseCase.execute(lastSeenDate) shouldBeEqualTo true
+ }
+}