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:
commit
e81f02f433
|
@ -0,0 +1 @@
|
|||
[Device Manager] Render Security Recommendations
|
|
@ -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 don’t use anymore.</item>
|
||||
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue