Render security recommendation sessions.

This commit is contained in:
Onuray Sahin 2022-09-19 15:37:13 +03:00
parent 740b69d48c
commit 7db222af0c
9 changed files with 78 additions and 16 deletions

View File

@ -3246,6 +3246,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

@ -41,6 +41,7 @@ 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
@ -84,6 +85,7 @@ class VectorSettingsDevicesFragment :
initLearnMoreButtons() initLearnMoreButtons()
initWaitingView() initWaitingView()
initOtherSessionsView() initOtherSessionsView()
initSecurityRecommendationsView()
observeViewEvents() observeViewEvents()
} }
@ -126,6 +128,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,
includeCurrentSession = true
)
}
}
views.deviceListInactiveSessionsRecommendation.callback = object : SecurityRecommendationView.Callback {
override fun onViewAllClicked() {
viewNavigator.navigateToOtherSessions(
requireActivity(),
R.string.device_manager_header_section_security_recommendations_title,
DeviceManagerFilterType.INACTIVE,
includeCurrentSession = true
)
}
}
}
override fun onDestroyView() { override fun onDestroyView() {
cleanUpLearnMoreButtonsListeners() cleanUpLearnMoreButtonsListeners()
super.onDestroyView() super.onDestroyView()

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,28 @@ 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 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.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
} else if (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) {

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.includeCurrentSession
) )
.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 includeCurrentSession: Boolean = false,
) : MavericksState {
constructor(args: OtherSessionsArgs) : this(includeCurrentSession = args.includeCurrentSession)
}

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