Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Device management] Update the unknown verification status icon (PSG-824) #7361

Merged
1 change: 1 addition & 0 deletions changelog.d/7327.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Device management] Update the unknown verification status icon
2 changes: 2 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3250,10 +3250,12 @@
<string name="a11y_device_manager_device_type_unknown">Unknown device type</string>
<string name="device_manager_verification_status_verified">Verified session</string>
<string name="device_manager_verification_status_unverified">Unverified session</string>
<string name="device_manager_verification_status_unknown">Unknown verification status</string>
<string name="device_manager_verification_status_detail_current_session_verified">Your current session is ready for secure messaging.</string>
<string name="device_manager_verification_status_detail_other_session_verified">This session is ready for secure messaging.</string>
<string name="device_manager_verification_status_detail_current_session_unverified">Verify your current session for enhanced secure messaging.</string>
<string name="device_manager_verification_status_detail_other_session_unverified">Verify or sign out from this session for best security and reliability.</string>
<string name="device_manager_verification_status_detail_other_session_unknown">Verify your current session to reveal this session\'s verification status.</string>
<string name="device_manager_verify_session">Verify Session</string>
<string name="device_manager_view_details">View Details</string>
<string name="device_manager_other_sessions_view_all">View All (%1$d)</string>
Expand Down
1 change: 1 addition & 0 deletions library/ui-styles/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<color name="shield_color_gray">#91A1C0</color>
<color name="shield_color_warning">#FF4B55</color>
<color name="shield_color_warning_background">#0FFF4B55</color>
<color name="shield_color_unknown">@color/palette_gray_200</color>

<!-- Badge Colors -->
<attr name="vctr_badge_color_border" format="color" />
Expand Down
2 changes: 2 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ android {
// "pm clear" command after each test invocation. This command ensures
// that the app's state is completely cleared between tests.
testInstrumentationRunnerArguments clearPackageData: 'true'

vectorDrawables.useSupportLibrary = true
}

testOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class ShieldImageView @JvmOverloads constructor(
RoomEncryptionTrustLevel.Default -> {
contentDescription = context.getString(R.string.a11y_trust_level_default)
setImageResource(
if (borderLess) R.drawable.ic_shield_black_no_border
else R.drawable.ic_shield_black
if (borderLess) R.drawable.ic_shield_unknown_no_border
else R.drawable.ic_shield_unknown
)
}
RoomEncryptionTrustLevel.Warning -> {
Expand Down Expand Up @@ -137,7 +137,7 @@ class ShieldImageView @JvmOverloads constructor(
@DrawableRes
fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
return when (this) {
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_unknown
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
} else {
emptyList()
}
filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds)
filterDevicesUseCase.execute(currentSessionCrossSigningInfo, deviceFullInfoList, filterType, excludedDeviceIds)
}

deviceFullInfoFlow.distinctUntilChanged()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INAC
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.SessionInfoViewState
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
import javax.inject.Inject

/**
Expand Down Expand Up @@ -164,12 +165,11 @@ class VectorSettingsDevicesFragment :
if (state.devices is Success) {
val devices = state.devices()
val currentDeviceId = state.currentSessionCrossSigningInfo.deviceId
val currentDeviceInfo = devices?.firstOrNull {
it.deviceInfo.deviceId == currentDeviceId
}
val currentDeviceInfo = devices?.firstOrNull { it.deviceInfo.deviceId == currentDeviceId }
val isCurrentSessionVerified = currentDeviceInfo?.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted
val otherDevices = devices?.filter { it.deviceInfo.deviceId != currentDeviceId }

renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount)
renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified)
renderCurrentDevice(currentDeviceInfo)
renderOtherSessionsView(otherDevices)
} else {
Expand All @@ -181,14 +181,21 @@ class VectorSettingsDevicesFragment :
handleLoadingStatus(state.isLoading)
}

private fun renderSecurityRecommendations(inactiveSessionsCount: Int, unverifiedSessionsCount: Int) {
if (unverifiedSessionsCount == 0 && inactiveSessionsCount == 0) {
private fun renderSecurityRecommendations(
inactiveSessionsCount: Int,
unverifiedSessionsCount: Int,
isCurrentSessionVerified: Boolean,
) {
val isUnverifiedSectionVisible = unverifiedSessionsCount > 0 && isCurrentSessionVerified
val isInactiveSectionVisible = inactiveSessionsCount > 0
if (isUnverifiedSectionVisible.not() && isInactiveSectionVisible.not()) {
hideSecurityRecommendations()
} else {
views.deviceListHeaderSectionSecurityRecommendations.isVisible = true
views.deviceListSecurityRecommendationsDivider.isVisible = true
views.deviceListUnverifiedSessionsRecommendation.isVisible = unverifiedSessionsCount > 0
views.deviceListInactiveSessionsRecommendation.isVisible = inactiveSessionsCount > 0

views.deviceListUnverifiedSessionsRecommendation.isVisible = isUnverifiedSectionVisible
views.deviceListInactiveSessionsRecommendation.isVisible = isInactiveSectionVisible
val unverifiedSessionsViewState = SecurityRecommendationViewState(
description = getString(R.string.device_manager_unverified_sessions_description),
sessionsCount = unverifiedSessionsCount,
Expand All @@ -206,11 +213,19 @@ class VectorSettingsDevicesFragment :
}
}

private fun hideSecurityRecommendations() {
views.deviceListHeaderSectionSecurityRecommendations.isVisible = false
private fun hideUnverifiedSessionsRecommendation() {
views.deviceListUnverifiedSessionsRecommendation.isVisible = false
}

private fun hideInactiveSessionsRecommendation() {
views.deviceListInactiveSessionsRecommendation.isVisible = false
}

private fun hideSecurityRecommendations() {
views.deviceListHeaderSectionSecurityRecommendations.isVisible = false
views.deviceListSecurityRecommendationsDivider.isVisible = false
hideUnverifiedSessionsRecommendation()
hideInactiveSessionsRecommendation()
}

private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@
package im.vector.app.features.settings.devices.v2.filter

import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject

class FilterDevicesUseCase @Inject constructor() {

fun execute(
currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo,
devices: List<DeviceFullInfo>,
filterType: DeviceManagerFilterType,
excludedDeviceIds: List<String> = emptyList(),
): List<DeviceFullInfo> {
val isCurrentSessionVerified = currentSessionCrossSigningInfo.isCrossSigningVerified.orFalse()
return devices
.filter {
when (filterType) {
DeviceManagerFilterType.ALL_SESSIONS -> true
DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
// when current session is not verified, other session status cannot be trusted
DeviceManagerFilterType.VERIFIED -> isCurrentSessionVerified && it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
// when current session is not verified, other session status cannot be trusted
DeviceManagerFilterType.UNVERIFIED -> isCurrentSessionVerified && !it.cryptoDeviceInfo?.trustLevel?.isCrossSigningVerified().orFalse()
DeviceManagerFilterType.INACTIVE -> it.isInactive
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class OtherSessionsController @Inject constructor(
data.forEach { device ->
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 = calculateDescription(device, formattedLastActivityDate)
val description = buildDescription(device, formattedLastActivityDate)
val descriptionColor = if (device.isCurrentDevice) {
host.colorProvider.getColorFromAttribute(R.attr.colorError)
} else {
Expand All @@ -77,7 +77,7 @@ class OtherSessionsController @Inject constructor(
}
}

private fun calculateDescription(device: DeviceFullInfo, formattedLastActivityDate: String): String {
private fun buildDescription(device: DeviceFullInfo, formattedLastActivityDate: String): String {
return when {
device.isInactive -> {
stringProvider.getQuantityString(
Expand All @@ -93,6 +93,9 @@ class OtherSessionsController @Inject constructor(
device.isCurrentDevice -> {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified_current_session)
}
device.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Default -> {
stringProvider.getString(R.string.device_manager_session_last_activity, formattedLastActivityDate)
}
else -> {
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,10 @@ class SessionInfoView @JvmOverloads constructor(
isVerifyButtonVisible: Boolean,
) {
views.sessionInfoVerificationStatusImageView.render(encryptionTrustLevel)
if (encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) {
renderCrossSigningVerified(isCurrentSession)
} else {
renderCrossSigningUnverified(isCurrentSession, isVerifyButtonVisible)
when {
encryptionTrustLevel == RoomEncryptionTrustLevel.Trusted -> renderCrossSigningVerified(isCurrentSession)
encryptionTrustLevel == RoomEncryptionTrustLevel.Default && !isCurrentSession -> renderCrossSigningUnknown()
else -> renderCrossSigningUnverified(isCurrentSession, isVerifyButtonVisible)
}
if (hasLearnMoreLink) {
appendLearnMoreToVerificationStatus()
Expand Down Expand Up @@ -142,6 +142,12 @@ class SessionInfoView @JvmOverloads constructor(
views.sessionInfoVerifySessionButton.isVisible = isVerifyButtonVisible
}

private fun renderCrossSigningUnknown() {
views.sessionInfoVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unknown)
views.sessionInfoVerificationStatusDetailTextView.text = context.getString(R.string.device_manager_verification_status_detail_other_session_unknown)
views.sessionInfoVerifySessionButton.isVisible = false
}

private fun renderDeviceInfo(sessionName: String, deviceType: DeviceType, stringProvider: StringProvider) {
setDeviceTypeIconUseCase.execute(deviceType, views.sessionInfoDeviceTypeImageView, stringProvider)
views.sessionInfoNameTextView.text = sessionName
Expand All @@ -155,34 +161,31 @@ class SessionInfoView @JvmOverloads constructor(
drawableProvider: DrawableProvider,
colorProvider: ColorProvider,
) {
deviceInfo.lastSeenTs
?.takeIf { isLastSeenDetailsVisible }
?.let { timestamp ->
views.sessionInfoLastActivityTextView.isVisible = true
views.sessionInfoLastActivityTextView.text = if (isInactive) {
val formattedTs = dateFormatter.format(timestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)
context.resources.getQuantityString(
R.plurals.device_manager_other_sessions_description_inactive,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedTs
)
} else {
val formattedTs = dateFormatter.format(timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
context.getString(R.string.device_manager_session_last_activity, formattedTs)
}
val drawable = if (isInactive) {
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor)
} else {
null
}
views.sessionInfoLastActivityTextView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
}
?: run {
views.sessionInfoLastActivityTextView.isGone = true
}

if (deviceInfo.lastSeenTs != null && isLastSeenDetailsVisible) {
val timestamp = deviceInfo.lastSeenTs
views.sessionInfoLastActivityTextView.isVisible = true
views.sessionInfoLastActivityTextView.text = if (isInactive) {
val formattedTs = dateFormatter.format(timestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)
context.resources.getQuantityString(
R.plurals.device_manager_other_sessions_description_inactive,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
formattedTs
)
} else {
val formattedTs = dateFormatter.format(timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
context.getString(R.string.device_manager_session_last_activity, formattedTs)
}
val drawable = if (isInactive) {
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
drawableProvider.getDrawable(R.drawable.ic_inactive_sessions, drawableColor)
} else {
null
}
views.sessionInfoLastActivityTextView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
} else {
views.sessionInfoLastActivityTextView.isGone = true
}
views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible })
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
Expand All @@ -42,7 +41,6 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
import im.vector.app.features.workers.signout.SignOutUiWorker
Expand Down Expand Up @@ -181,11 +179,6 @@ class SessionOverviewFragment :
updateSessionInfo(state)
updateLoading(state.isLoading)
updatePushNotificationToggle(state.deviceId, state.pushers.invoke().orEmpty())
if (state.deviceInfo is Success) {
renderSessionInfo(state.isCurrentSessionTrusted, state.deviceInfo.invoke())
} else {
hideSessionInfo()
}
}

private fun updateToolbar(viewState: SessionOverviewViewState) {
Expand Down Expand Up @@ -214,7 +207,7 @@ class SessionOverviewFragment :
deviceFullInfo = deviceInfo,
isVerifyButtonVisible = isCurrentSession || viewState.isCurrentSessionTrusted,
isDetailsButtonVisible = false,
isLearnMoreLinkVisible = true,
isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default,
isLastSeenDetailsVisible = !isCurrentSession,
)
views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
Expand Down Expand Up @@ -243,18 +236,6 @@ class SessionOverviewFragment :
}
}

private fun renderSessionInfo(isCurrentSession: Boolean, deviceFullInfo: DeviceFullInfo) {
views.sessionOverviewInfo.isVisible = true
val viewState = SessionInfoViewState(
isCurrentSession = isCurrentSession,
deviceFullInfo = deviceFullInfo,
isDetailsButtonVisible = false,
isLearnMoreLinkVisible = true,
isLastSeenDetailsVisible = true,
)
views.sessionOverviewInfo.render(viewState, dateFormatter, drawableProvider, colorProvider, stringProvider)
}

private fun updateLoading(isLoading: Boolean) {
if (isLoading) {
showLoading(null)
Expand Down Expand Up @@ -313,8 +294,4 @@ class SessionOverviewFragment :
)
SessionLearnMoreBottomSheet.show(childFragmentManager, args)
}

private fun hideSessionInfo() {
views.sessionOverviewInfo.isGone = true
}
}
Loading