Render security recommendation sessions.
This commit is contained in:
parent
740b69d48c
commit
7db222af0c
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue