Merge pull request #6965 from vector-im/feature/ons/device_manager_security_recommendations

[Device Manager] Render security recommendations (PSG-681)
This commit is contained in:
Maxime NATUREL 2022-09-05 09:04:34 +02:00 committed by GitHub
commit e81f02f433
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 425 additions and 6 deletions

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

@ -0,0 +1 @@
[Device Manager] Render Security Recommendations

View File

@ -3225,5 +3225,19 @@
<string name="device_manager_other_sessions_view_all">View All (%1$d)</string>
<string name="device_manager_other_sessions_description_verified">Verified · Last activity %1$s</string>
<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">
<item quantity="one">Inactive for %1$d+ day (%2$s)</item>
<item quantity="other">Inactive for %1$d+ days (%2$s)</item>
</plurals>
<string name="device_manager_header_section_security_recommendations_title">Security recommendations</string>
<string name="device_manager_header_section_security_recommendations_description">Improve your account security by following these recommendations.</string>
<string name="device_manager_unverified_sessions_title">Unverified sessions</string>
<string name="device_manager_unverified_sessions_description">Verify or sign out from unverified sessions.</string>
<string name="device_manager_inactive_sessions_title">Inactive sessions</string>
<plurals name="device_manager_inactive_sessions_description">
<item quantity="one">Consider signing out from old sessions (%1$d day or more) that you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you dont use anymore.</item>
</plurals>
</resources>

View File

@ -143,6 +143,7 @@
<color name="shield_color_trust">#0DBD8B</color>
<color name="shield_color_black">#17191C</color>
<color name="shield_color_warning">#FF4B55</color>
<color name="shield_color_warning_background">#0FFF4B55</color>
<!-- Badge Colors -->
<attr name="vctr_badge_color_border" format="color" />

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SecurityRecommendationView">
<attr name="recommendationTitle" format="string" />
<attr name="recommendationDescription" format="string" />
<attr name="recommendationImageResource" format="reference" />
<attr name="recommendationImageBackgroundTint" format="color" />
</declare-styleable>
</resources>

View File

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

View File

@ -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<Unit> = 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<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
var uiaContinuation: Continuation<UIABaseAuth>? = 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()

View File

@ -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<DeviceFullInfo>?) {
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<Unit>) {

View File

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

View File

@ -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<OtherSessionItem.Holder>(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<OtherSessionItem.Holder>(R.la
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
holder.otherSessionNameTextView.text = sessionName
holder.otherSessionDescriptionTextView.text = sessionDescription
holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null)
}
class Holder : VectorEpoxyHolder() {

View File

@ -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<List<DeviceFullInfo>>() {
override fun buildModels(data: List<DeviceFullInfo>?) {
@ -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)
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?android:colorBackground" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="14dp"
android:viewportWidth="8"
android:viewportHeight="14">
<path
android:pathData="M1.333,0.333C0.6,0.333 0,0.933 0,1.666L0.007,3.786C0.007,4.14 0.147,4.473 0.393,4.726L2.667,7L0.393,9.286C0.147,9.533 0.007,9.873 0.007,10.226L0,12.333C0,13.066 0.6,13.666 1.333,13.666H6.667C7.4,13.666 8,13.066 8,12.333V10.226C8,9.873 7.86,9.533 7.613,9.286L5.333,7L7.607,4.733C7.86,4.48 8,4.14 8,3.786V1.666C8,0.933 7.4,0.333 6.667,0.333H1.333ZM6.667,10.273V11.666C6.667,12.033 6.367,12.333 6,12.333H2C1.633,12.333 1.333,12.033 1.333,11.666V10.273C1.333,10.093 1.407,9.926 1.527,9.8L4,7.333L6.473,9.806C6.593,9.926 6.667,10.1 6.667,10.273Z"
android:fillColor="#737D8C"/>
</vector>

View File

@ -8,6 +8,54 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView
android:id="@+id/deviceListHeaderSectionSecurityRecommendations"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:devicesListHeaderDescription="@string/device_manager_header_section_security_recommendations_description"
app:devicesListHeaderTitle="@string/device_manager_header_section_security_recommendations_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView
android:id="@+id/deviceListUnverifiedSessionsRecommendation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
app:recommendationTitle="@string/device_manager_unverified_sessions_title"
app:recommendationDescription="@string/device_manager_unverified_sessions_description"
app:recommendationImageResource="@drawable/ic_shield_warning_no_border"
app:recommendationImageBackgroundTint="@color/shield_color_warning_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderSectionSecurityRecommendations"/>
<im.vector.app.features.settings.devices.v2.list.SecurityRecommendationView
android:id="@+id/deviceListInactiveSessionsRecommendation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
app:recommendationTitle="@string/device_manager_inactive_sessions_title"
app:recommendationDescription="@plurals/device_manager_inactive_sessions_description"
app:recommendationImageResource="@drawable/ic_inactive_sessions"
app:recommendationImageBackgroundTint="?vctr_system"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListUnverifiedSessionsRecommendation"/>
<View
android:id="@+id/deviceListSecurityRecommendationsDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="24dp"
android:background="@drawable/divider_horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListInactiveSessionsRecommendation" />
<im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView
android:id="@+id/deviceListHeaderCurrentSession"
android:layout_width="0dp"
@ -16,7 +64,7 @@
app:devicesListHeaderTitle="@string/device_manager_header_section_current_session"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/deviceListSecurityRecommendationsDivider" />
<im.vector.app.features.settings.devices.v2.list.CurrentSessionView
android:id="@+id/deviceListCurrentSession"

View File

@ -50,6 +50,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:drawablePadding="8dp"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"
app:layout_constraintTop_toBottomOf="@id/otherSessionNameTextView"
tools:text="@string/device_manager_verification_status_verified" />

View File

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

View File

@ -0,0 +1,56 @@
<?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="wrap_content"
android:background="@drawable/bg_current_session"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/recommendationShieldImageView"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/bg_security_recommendation_shield"
android:importantForAccessibility="no"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="@color/shield_color_warning_background"
tools:src="@drawable/ic_shield_warning_no_border" />
<TextView
android:id="@+id/recommendationTitleTextView"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/recommendationShieldImageView"
app:layout_constraintTop_toTopOf="@id/recommendationShieldImageView"
tools:text="@string/device_manager_unverified_sessions_title" />
<TextView
android:id="@+id/recommendationDescriptionTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/recommendationTitleTextView"
app:layout_constraintTop_toBottomOf="@id/recommendationTitleTextView"
tools:text="@string/device_manager_unverified_sessions_description" />
<Button
android:id="@+id/recommendationViewAllButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:padding="0dp"
android:text="@string/device_manager_other_sessions_view_all"
app:layout_constraintStart_toStartOf="@id/recommendationTitleTextView"
app:layout_constraintTop_toBottomOf="@id/recommendationDescriptionTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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