Merge pull request #7306 from vector-im/feature/mna/device-manager-extended-details

[Device Management] Render extended device info (PSG-773)
This commit is contained in:
Maxime NATUREL 2022-10-12 18:09:33 +02:00 committed by GitHub
commit f8f416e979
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 574 additions and 104 deletions

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

@ -0,0 +1 @@
[Device Management] Render extended device info

View File

@ -3314,6 +3314,13 @@
<string name="device_manager_session_details_session_name">Session name</string> <string name="device_manager_session_details_session_name">Session name</string>
<string name="device_manager_session_details_session_id">Session ID</string> <string name="device_manager_session_details_session_id">Session ID</string>
<string name="device_manager_session_details_session_last_activity">Last activity</string> <string name="device_manager_session_details_session_last_activity">Last activity</string>
<string name="device_manager_session_details_application">Application</string>
<string name="device_manager_session_details_application_name">Name</string>
<string name="device_manager_session_details_application_version">Version</string>
<string name="device_manager_session_details_application_url">URL</string>
<string name="device_manager_session_details_device_browser">Browser</string>
<string name="device_manager_session_details_device_model">Model</string>
<string name="device_manager_session_details_device_operating_system">Operating system</string>
<string name="device_manager_session_details_device_ip_address">IP address</string> <string name="device_manager_session_details_device_ip_address">IP address</string>
<string name="device_manager_session_rename">Rename session</string> <string name="device_manager_session_rename">Rename session</string>
<string name="device_manager_session_rename_edit_hint">Session name</string> <string name="device_manager_session_rename_edit_hint">Session name</string>

View File

@ -19,4 +19,4 @@ package im.vector.app.core.session.clientinfo
/** /**
* Prefix for the key account data event which holds client info. * Prefix for the key account data event which holds client info.
*/ */
const val MATRIX_CLIENT_INFO_KEY_PREFIX = "io.element.matrix_client_information." internal const val MATRIX_CLIENT_INFO_KEY_PREFIX = "io.element.matrix_client_information."

View File

@ -16,6 +16,8 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@ -27,4 +29,5 @@ data class DeviceFullInfo(
val isInactive: Boolean, val isInactive: Boolean,
val isCurrentDevice: Boolean, val isCurrentDevice: Boolean,
val deviceExtendedInfo: DeviceExtendedInfo, val deviceExtendedInfo: DeviceExtendedInfo,
val matrixClientInfo: MatrixClientInfoContent?,
) )

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.session.clientinfo.GetMatrixClientInfoUseCase
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
@ -39,6 +41,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val filterDevicesUseCase: FilterDevicesUseCase, private val filterDevicesUseCase: FilterDevicesUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
private val getMatrixClientInfoUseCase: GetMatrixClientInfoUseCase,
) { ) {
fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> { fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
@ -48,7 +51,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo() session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList -> ) { currentSessionCrossSigningInfo, cryptoList, infoList ->
val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) val deviceFullInfoList = convertToDeviceFullInfoList(session, currentSessionCrossSigningInfo, cryptoList, infoList)
val excludedDeviceIds = if (excludeCurrentDevice) { val excludedDeviceIds = if (excludeCurrentDevice) {
listOf(currentSessionCrossSigningInfo.deviceId) listOf(currentSessionCrossSigningInfo.deviceId)
} else { } else {
@ -62,6 +65,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
} }
private fun convertToDeviceFullInfoList( private fun convertToDeviceFullInfoList(
session: Session,
currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo, currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo,
cryptoList: List<CryptoDeviceInfo>, cryptoList: List<CryptoDeviceInfo>,
infoList: List<DeviceInfo>, infoList: List<DeviceInfo>,
@ -73,8 +77,20 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId
val deviceUserAgent = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent()) val deviceExtendedInfo = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent())
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent) val matrixClientInfo = deviceInfo.deviceId
?.takeIf { it.isNotEmpty() }
?.let { getMatrixClientInfoUseCase.execute(session, it) }
DeviceFullInfo(
deviceInfo = deviceInfo,
cryptoDeviceInfo = cryptoDeviceInfo,
roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive,
isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = deviceExtendedInfo,
matrixClientInfo = matrixClientInfo
)
} }
} }
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject import javax.inject.Inject
@ -139,13 +140,11 @@ class ParseDeviceUserAgentUseCase @Inject constructor() {
} }
private fun getBrowserVersion(browserSegments: List<String>, browserName: String): String? { private fun getBrowserVersion(browserSegments: List<String>, browserName: String): String? {
// Chrome/104.0.3497.100 -> 104 // e.g Chrome/104.0.3497.100 -> 104.0.3497.100
return browserSegments return browserSegments
.find { it.startsWith(browserName) } .find { it.startsWith(browserName) }
?.split("/") ?.split("/")
?.getOrNull(1) ?.getOrNull(1)
?.split(".")
?.firstOrNull()
} }
private fun isEdge(browserSegments: List<String>): Boolean { private fun isEdge(browserSegments: List<String>): Boolean {

View File

@ -0,0 +1,30 @@
/*
* 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.details
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
class CheckIfSectionApplicationIsVisibleUseCase @Inject constructor() {
fun execute(matrixClientInfoContent: MatrixClientInfoContent?): Boolean {
return matrixClientInfoContent?.name?.isNotEmpty().orFalse() ||
matrixClientInfoContent?.version?.isNotEmpty().orFalse() ||
matrixClientInfoContent?.url?.isNotEmpty().orFalse()
}
}

View File

@ -16,13 +16,36 @@
package im.vector.app.features.settings.devices.v2.details package im.vector.app.features.settings.devices.v2.details
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import javax.inject.Inject import javax.inject.Inject
class CheckIfSectionDeviceIsVisibleUseCase @Inject constructor() { class CheckIfSectionDeviceIsVisibleUseCase @Inject constructor() {
fun execute(deviceInfo: DeviceInfo): Boolean { fun execute(deviceFullInfo: DeviceFullInfo): Boolean {
return deviceInfo.lastSeenIp?.isNotEmpty().orFalse() val hasExtendedInfo = when (deviceFullInfo.deviceExtendedInfo.deviceType) {
DeviceType.MOBILE -> hasAnyDeviceExtendedInfoMobile(deviceFullInfo.deviceExtendedInfo)
DeviceType.WEB -> hasAnyDeviceExtendedInfoWeb(deviceFullInfo.deviceExtendedInfo)
DeviceType.DESKTOP -> hasAnyDeviceExtendedInfoDesktop(deviceFullInfo.deviceExtendedInfo)
DeviceType.UNKNOWN -> false
}
return hasExtendedInfo || deviceFullInfo.deviceInfo.lastSeenIp?.isNotEmpty().orFalse()
}
private fun hasAnyDeviceExtendedInfoMobile(deviceExtendedInfo: DeviceExtendedInfo): Boolean {
return deviceExtendedInfo.deviceModel?.isNotEmpty().orFalse() ||
deviceExtendedInfo.deviceOperatingSystem?.isNotEmpty().orFalse()
}
private fun hasAnyDeviceExtendedInfoWeb(deviceExtendedInfo: DeviceExtendedInfo): Boolean {
return deviceExtendedInfo.clientName?.isNotEmpty().orFalse() ||
deviceExtendedInfo.deviceOperatingSystem?.isNotEmpty().orFalse()
}
private fun hasAnyDeviceExtendedInfoDesktop(deviceExtendedInfo: DeviceExtendedInfo): Boolean {
return deviceExtendedInfo.deviceOperatingSystem?.isNotEmpty().orFalse()
} }
} }

View File

@ -23,17 +23,21 @@ import im.vector.app.R
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import javax.inject.Inject import javax.inject.Inject
class SessionDetailsController @Inject constructor( class SessionDetailsController @Inject constructor(
private val checkIfSectionSessionIsVisibleUseCase: CheckIfSectionSessionIsVisibleUseCase, private val checkIfSectionSessionIsVisibleUseCase: CheckIfSectionSessionIsVisibleUseCase,
private val checkIfSectionDeviceIsVisibleUseCase: CheckIfSectionDeviceIsVisibleUseCase, private val checkIfSectionDeviceIsVisibleUseCase: CheckIfSectionDeviceIsVisibleUseCase,
private val checkIfSectionApplicationIsVisibleUseCase: CheckIfSectionApplicationIsVisibleUseCase,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val dimensionConverter: DimensionConverter, private val dimensionConverter: DimensionConverter,
) : TypedEpoxyController<DeviceInfo>() { ) : TypedEpoxyController<DeviceFullInfo>() {
var callback: Callback? = null var callback: Callback? = null
@ -41,15 +45,22 @@ class SessionDetailsController @Inject constructor(
fun onItemLongClicked(content: String) fun onItemLongClicked(content: String)
} }
override fun buildModels(data: DeviceInfo?) { override fun buildModels(data: DeviceFullInfo?) {
data?.let { info -> data?.let { fullInfo ->
val hasSectionSession = hasSectionSession(data) val deviceInfo = fullInfo.deviceInfo
val matrixClientInfo = fullInfo.matrixClientInfo
val hasSectionSession = hasSectionSession(deviceInfo)
if (hasSectionSession) { if (hasSectionSession) {
buildSectionSession(info) buildSectionSession(deviceInfo)
} }
if (hasSectionDevice(data)) { val hasApplicationSection = hasSectionApplication(matrixClientInfo)
buildSectionDevice(info, addExtraTopMargin = hasSectionSession) if (hasApplicationSection && matrixClientInfo != null) {
buildSectionApplication(matrixClientInfo, addExtraTopMargin = hasSectionSession)
}
if (hasSectionDevice(fullInfo)) {
buildSectionDevice(fullInfo, addExtraTopMargin = hasSectionSession || hasApplicationSection)
} }
} }
} }
@ -83,39 +94,126 @@ class SessionDetailsController @Inject constructor(
} }
private fun buildSectionSession(data: DeviceInfo) { private fun buildSectionSession(data: DeviceInfo) {
val sessionName = data.displayName val sessionName = data.displayName.orEmpty()
val sessionId = data.deviceId val sessionId = data.deviceId.orEmpty()
val sessionLastSeenTs = data.lastSeenTs val sessionLastSeenTs = data.lastSeenTs ?: -1
buildHeaderItem(R.string.device_manager_session_title) buildHeaderItem(R.string.device_manager_session_title)
sessionName?.let { if (sessionName.isNotEmpty()) {
val hasDivider = sessionId != null || sessionLastSeenTs != null val hasDivider = sessionId.isNotEmpty() || sessionLastSeenTs > 0
buildContentItem(R.string.device_manager_session_details_session_name, it, hasDivider) buildContentItem(R.string.device_manager_session_details_session_name, sessionName, hasDivider)
} }
sessionId?.let { if (sessionId.isNotEmpty()) {
val hasDivider = sessionLastSeenTs != null val hasDivider = sessionLastSeenTs > 0
buildContentItem(R.string.device_manager_session_details_session_id, it, hasDivider) buildContentItem(R.string.device_manager_session_details_session_id, sessionId, hasDivider)
} }
sessionLastSeenTs?.let { if (sessionLastSeenTs > 0) {
val formattedDate = dateFormatter.format(it, DateFormatKind.MESSAGE_DETAIL) val formattedDate = dateFormatter.format(sessionLastSeenTs, DateFormatKind.MESSAGE_DETAIL)
val hasDivider = false val hasDivider = false
buildContentItem(R.string.device_manager_session_details_session_last_activity, formattedDate, hasDivider) buildContentItem(R.string.device_manager_session_details_session_last_activity, formattedDate, hasDivider)
} }
} }
private fun hasSectionDevice(data: DeviceInfo): Boolean { private fun hasSectionApplication(matrixClientInfoContent: MatrixClientInfoContent?): Boolean {
return checkIfSectionApplicationIsVisibleUseCase.execute(matrixClientInfoContent)
}
private fun buildSectionApplication(matrixClientInfoContent: MatrixClientInfoContent, addExtraTopMargin: Boolean) {
val name = matrixClientInfoContent.name.orEmpty()
val version = matrixClientInfoContent.version.orEmpty()
val url = matrixClientInfoContent.url.orEmpty()
buildHeaderItem(R.string.device_manager_session_details_application, addExtraTopMargin)
if (name.isNotEmpty()) {
val hasDivider = version.isNotEmpty() || url.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_application_name, name, hasDivider)
}
if (version.isNotEmpty()) {
val hasDivider = url.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_application_version, version, hasDivider)
}
if (url.isNotEmpty()) {
val hasDivider = false
buildContentItem(R.string.device_manager_session_details_application_url, url, hasDivider)
}
}
private fun hasSectionDevice(data: DeviceFullInfo): Boolean {
return checkIfSectionDeviceIsVisibleUseCase.execute(data) return checkIfSectionDeviceIsVisibleUseCase.execute(data)
} }
private fun buildSectionDevice(data: DeviceInfo, addExtraTopMargin: Boolean) { private fun buildSectionDevice(data: DeviceFullInfo, addExtraTopMargin: Boolean) {
val lastSeenIp = data.lastSeenIp
buildHeaderItem(R.string.device_manager_device_title, addExtraTopMargin) buildHeaderItem(R.string.device_manager_device_title, addExtraTopMargin)
lastSeenIp?.let { when (data.deviceExtendedInfo.deviceType) {
DeviceType.MOBILE -> buildSectionDeviceMobile(data)
DeviceType.WEB -> buildSectionDeviceWeb(data)
DeviceType.DESKTOP -> buildSectionDeviceDesktop(data)
DeviceType.UNKNOWN -> buildSectionDeviceUnknown(data)
}
}
private fun buildSectionDeviceWeb(data: DeviceFullInfo) {
val browserName = data.deviceExtendedInfo.clientName.orEmpty()
val browserVersion = data.deviceExtendedInfo.clientVersion.orEmpty()
val browser = "$browserName $browserVersion"
val operatingSystem = data.deviceExtendedInfo.deviceOperatingSystem.orEmpty()
val lastSeenIp = data.deviceInfo.lastSeenIp.orEmpty()
if (browser.isNotEmpty()) {
val hasDivider = operatingSystem.isNotEmpty() || lastSeenIp.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_device_browser, browser, hasDivider)
}
if (operatingSystem.isNotEmpty()) {
val hasDivider = lastSeenIp.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_device_operating_system, operatingSystem, hasDivider)
}
buildIpAddressContentItem(lastSeenIp)
}
private fun buildSectionDeviceDesktop(data: DeviceFullInfo) {
val operatingSystem = data.deviceExtendedInfo.deviceOperatingSystem.orEmpty()
val lastSeenIp = data.deviceInfo.lastSeenIp.orEmpty()
if (operatingSystem.isNotEmpty()) {
val hasDivider = lastSeenIp.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_device_operating_system, operatingSystem, hasDivider)
}
buildIpAddressContentItem(lastSeenIp)
}
private fun buildSectionDeviceMobile(data: DeviceFullInfo) {
val model = data.deviceExtendedInfo.deviceModel.orEmpty()
val operatingSystem = data.deviceExtendedInfo.deviceOperatingSystem.orEmpty()
val lastSeenIp = data.deviceInfo.lastSeenIp.orEmpty()
if (model.isNotEmpty()) {
val hasDivider = operatingSystem.isNotEmpty() || lastSeenIp.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_device_model, model, hasDivider)
}
if (operatingSystem.isNotEmpty()) {
val hasDivider = lastSeenIp.isNotEmpty()
buildContentItem(R.string.device_manager_session_details_device_operating_system, operatingSystem, hasDivider)
}
buildIpAddressContentItem(lastSeenIp)
}
private fun buildSectionDeviceUnknown(data: DeviceFullInfo) {
val lastSeenIp = data.deviceInfo.lastSeenIp.orEmpty()
buildIpAddressContentItem(lastSeenIp)
}
private fun buildIpAddressContentItem(lastSeenIp: String) {
if (lastSeenIp.isNotEmpty()) {
val hasDivider = false val hasDivider = false
buildContentItem(R.string.device_manager_session_details_device_ip_address, it, hasDivider) buildContentItem(R.string.device_manager_session_details_device_ip_address, lastSeenIp, hasDivider)
} }
} }
} }

View File

@ -33,7 +33,7 @@ import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.databinding.FragmentSessionDetailsBinding import im.vector.app.databinding.FragmentSessionDetailsBinding
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -92,16 +92,16 @@ class SessionDetailsFragment :
} }
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
if (state.deviceInfo is Success) { if (state.deviceFullInfo is Success) {
renderSessionDetails(state.deviceInfo.invoke()) renderSessionDetails(state.deviceFullInfo.invoke())
} else { } else {
hideSessionDetails() hideSessionDetails()
} }
} }
private fun renderSessionDetails(deviceInfo: DeviceInfo) { private fun renderSessionDetails(deviceFullInfo: DeviceFullInfo) {
views.sessionDetails.isVisible = true views.sessionDetails.isVisible = true
sessionDetailsController.setData(deviceInfo) sessionDetailsController.setData(deviceFullInfo)
} }
private fun hideSessionDetails() { private fun hideSessionDetails() {

View File

@ -48,7 +48,7 @@ class SessionDetailsViewModel @AssistedInject constructor(
private fun observeSessionInfo(deviceId: String) { private fun observeSessionInfo(deviceId: String) {
getDeviceFullInfoUseCase.execute(deviceId) getDeviceFullInfoUseCase.execute(deviceId)
.onEach { setState { copy(deviceInfo = Success(it.deviceInfo)) } } .onEach { setState { copy(deviceFullInfo = Success(it)) } }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }

View File

@ -19,11 +19,11 @@ package im.vector.app.features.settings.devices.v2.details
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
data class SessionDetailsViewState( data class SessionDetailsViewState(
val deviceId: String, val deviceId: String,
val deviceInfo: Async<DeviceInfo> = Uninitialized, val deviceFullInfo: Async<DeviceFullInfo> = Uninitialized,
) : MavericksState { ) : MavericksState {
constructor(args: SessionDetailsArgs) : this( constructor(args: SessionDetailsArgs) : this(
deviceId = args.deviceId deviceId = args.deviceId

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2.details.extended
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType

View File

@ -18,6 +18,7 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.session.clientinfo.GetMatrixClientInfoUseCase
import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
@ -36,6 +37,7 @@ class GetDeviceFullInfoUseCase @Inject constructor(
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
private val getMatrixClientInfoUseCase: GetMatrixClientInfoUseCase,
) { ) {
fun execute(deviceId: String): Flow<DeviceFullInfo> { fun execute(deviceId: String): Flow<DeviceFullInfo> {
@ -52,6 +54,10 @@ class GetDeviceFullInfoUseCase @Inject constructor(
val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId
val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.getBestLastSeenUserAgent()) val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.getBestLastSeenUserAgent())
val matrixClientInfo = info.deviceId
?.takeIf { it.isNotEmpty() }
?.let { getMatrixClientInfoUseCase.execute(session, it) }
DeviceFullInfo( DeviceFullInfo(
deviceInfo = info, deviceInfo = info,
cryptoDeviceInfo = cryptoInfo, cryptoDeviceInfo = cryptoInfo,
@ -59,6 +65,7 @@ class GetDeviceFullInfoUseCase @Inject constructor(
isInactive = isInactive, isInactive = isInactive,
isCurrentDevice = isCurrentDevice, isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = deviceUserAgent, deviceExtendedInfo = deviceUserAgent,
matrixClientInfo = matrixClientInfo
) )
} else { } else {
null null

View File

@ -19,6 +19,8 @@ package im.vector.app.features.settings.devices.v2
import android.os.SystemClock import android.os.SystemClock
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.test.MavericksTestRule import com.airbnb.mvrx.test.MavericksTestRule
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType
import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
@ -53,7 +55,7 @@ class DevicesViewModelTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>() private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>() private val getDeviceFullInfoListUseCase = mockk<GetDeviceFullInfoListUseCase>()
private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>() private val refreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>() private val refreshDevicesOnCryptoDevicesChangeUseCase = mockk<RefreshDevicesOnCryptoDevicesChangeUseCase>()
private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>() private val checkIfCurrentSessionCanBeVerifiedUseCase = mockk<CheckIfCurrentSessionCanBeVerifiedUseCase>()
@ -245,7 +247,8 @@ class DevicesViewModelTest {
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = true, isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
val deviceFullInfo2 = DeviceFullInfo( val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(), deviceInfo = mockk(),
@ -253,7 +256,8 @@ class DevicesViewModelTest {
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true, isInactive = true,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList) val deviceFullInfoListFlow = flowOf(deviceFullInfoList)

View File

@ -16,6 +16,9 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.session.clientinfo.GetMatrixClientInfoUseCase
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
@ -55,6 +58,7 @@ class GetDeviceFullInfoListUseCaseTest {
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>() private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val filterDevicesUseCase = mockk<FilterDevicesUseCase>() private val filterDevicesUseCase = mockk<FilterDevicesUseCase>()
private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>() private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>()
private val getMatrixClientInfoUseCase = mockk<GetMatrixClientInfoUseCase>()
private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase( private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
@ -63,6 +67,7 @@ class GetDeviceFullInfoListUseCaseTest {
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
filterDevicesUseCase = filterDevicesUseCase, filterDevicesUseCase = filterDevicesUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase, parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
getMatrixClientInfoUseCase = getMatrixClientInfoUseCase,
) )
@Before @Before
@ -108,13 +113,17 @@ class GetDeviceFullInfoListUseCaseTest {
) )
val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3) val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3)
every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList) every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList)
val matrixClientInfo1 = givenAMatrixClientInfo(A_DEVICE_ID_1)
val matrixClientInfo2 = givenAMatrixClientInfo(A_DEVICE_ID_2)
val matrixClientInfo3 = givenAMatrixClientInfo(A_DEVICE_ID_3)
val expectedResult1 = DeviceFullInfo( val expectedResult1 = DeviceFullInfo(
deviceInfo = deviceInfo1, deviceInfo = deviceInfo1,
cryptoDeviceInfo = cryptoDeviceInfo1, cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true, isInactive = true,
isCurrentDevice = true, isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = matrixClientInfo1,
) )
val expectedResult2 = DeviceFullInfo( val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2, deviceInfo = deviceInfo2,
@ -122,7 +131,8 @@ class GetDeviceFullInfoListUseCaseTest {
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = matrixClientInfo2,
) )
val expectedResult3 = DeviceFullInfo( val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3, deviceInfo = deviceInfo3,
@ -130,7 +140,8 @@ class GetDeviceFullInfoListUseCaseTest {
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false, isInactive = false,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = matrixClientInfo3,
) )
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1) val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult
@ -152,6 +163,9 @@ class GetDeviceFullInfoListUseCaseTest {
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_1) checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_1)
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_2) checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_2)
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_3) checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP_3)
getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID_1)
getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID_2)
getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID_3)
} }
} }
@ -201,4 +215,10 @@ class GetDeviceFullInfoListUseCaseTest {
return deviceInfo return deviceInfo
} }
private fun givenAMatrixClientInfo(deviceId: String): MatrixClientInfoContent {
val matrixClientInfo = mockk<MatrixClientInfoContent>()
every { getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, deviceId) } returns matrixClientInfo
return matrixClientInfo
}
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
@ -61,8 +62,8 @@ private val A_USER_AGENT_LIST_FOR_DESKTOP = listOf(
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
) )
private val AN_EXPECTED_RESULT_LIST_FOR_DESKTOP = listOf( private val AN_EXPECTED_RESULT_LIST_FOR_DESKTOP = listOf(
DeviceExtendedInfo(DeviceType.DESKTOP, null, "Macintosh", "Electron", "20"), DeviceExtendedInfo(DeviceType.DESKTOP, null, "Macintosh", "Electron", "20.1.1"),
DeviceExtendedInfo(DeviceType.DESKTOP, null, "Windows NT 10.0", "Electron", "20"), DeviceExtendedInfo(DeviceType.DESKTOP, null, "Windows NT 10.0", "Electron", "20.1.1"),
) )
private val A_USER_AGENT_LIST_FOR_WEB = listOf( private val A_USER_AGENT_LIST_FOR_WEB = listOf(
@ -77,15 +78,15 @@ private val A_USER_AGENT_LIST_FOR_WEB = listOf(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
) )
private val AN_EXPECTED_RESULT_LIST_FOR_WEB = listOf( private val AN_EXPECTED_RESULT_LIST_FOR_WEB = listOf(
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Chrome", "104"), DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Chrome", "104.0.5112.102"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Chrome", "104"), DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Chrome", "104.0.5112.102"),
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Firefox", "39"), DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Firefox", "39.0"),
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Safari", "8"), DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Safari", "8.0.3"),
DeviceExtendedInfo(DeviceType.WEB, null, "Android 9", "Chrome", "69"), DeviceExtendedInfo(DeviceType.WEB, null, "Android 9", "Chrome", "69.0.3497.100"),
DeviceExtendedInfo(DeviceType.WEB, null, "iPad", "Safari", "8"), DeviceExtendedInfo(DeviceType.WEB, null, "iPad", "Safari", "8.0"),
DeviceExtendedInfo(DeviceType.WEB, null, "iPhone", "Safari", "8"), DeviceExtendedInfo(DeviceType.WEB, null, "iPhone", "Safari", "8.0"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 6.0", "Firefox", "40"), DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 6.0", "Firefox", "40.0"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Edge", "12"), DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Edge", "12.246"),
) )
private val AN_UNKNOWN_USER_AGENT_LIST = listOf( private val AN_UNKNOWN_USER_AGENT_LIST = listOf(

View File

@ -0,0 +1,145 @@
/*
* 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.details
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private const val AN_APP_NAME = "app-name"
private const val AN_APP_VERSION = "app-version"
private const val AN_APP_URL = "app-url"
class CheckIfSectionApplicationIsVisibleUseCaseTest {
private val checkIfSectionApplicationIsVisibleUseCase = CheckIfSectionApplicationIsVisibleUseCase()
@Test
fun `given client info with name, version or url when checking is application section is visible then it returns true`() {
// Given
val clientInfoList = listOf(
givenAClientInfo(
name = AN_APP_NAME,
version = null,
url = null,
),
givenAClientInfo(
name = null,
version = AN_APP_VERSION,
url = null,
),
givenAClientInfo(
name = null,
version = null,
url = AN_APP_URL,
),
givenAClientInfo(
name = AN_APP_NAME,
version = AN_APP_VERSION,
url = null,
),
givenAClientInfo(
name = AN_APP_NAME,
version = null,
url = AN_APP_URL,
),
givenAClientInfo(
name = null,
version = AN_APP_VERSION,
url = AN_APP_URL,
),
givenAClientInfo(
name = AN_APP_NAME,
version = AN_APP_VERSION,
url = AN_APP_URL,
),
)
clientInfoList.forEach { clientInfo ->
// When
val result = checkIfSectionApplicationIsVisibleUseCase.execute(clientInfo)
// Then
result shouldBeEqualTo true
}
}
@Test
fun `given client info with missing application info when checking is application section is visible then it returns false`() {
// Given
val clientInfoList = listOf(
givenAClientInfo(
name = null,
version = null,
url = null,
),
givenAClientInfo(
name = "",
version = null,
url = null,
),
givenAClientInfo(
name = null,
version = "",
url = null,
),
givenAClientInfo(
name = null,
version = null,
url = "",
),
givenAClientInfo(
name = "",
version = "",
url = null,
),
givenAClientInfo(
name = "",
version = null,
url = "",
),
givenAClientInfo(
name = null,
version = "",
url = "",
),
givenAClientInfo(
name = "",
version = "",
url = "",
),
)
clientInfoList.forEach { clientInfo ->
// When
val result = checkIfSectionApplicationIsVisibleUseCase.execute(clientInfo)
// Then
result shouldBeEqualTo false
}
}
private fun givenAClientInfo(
name: String?,
version: String?,
url: String?,
) = MatrixClientInfoContent(
name = name,
version = version,
url = url,
)
}

View File

@ -16,44 +16,116 @@
package im.vector.app.features.settings.devices.v2.details package im.vector.app.features.settings.devices.v2.details
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
private const val AN_IP_ADDRESS = "ip-address" private const val AN_IP_ADDRESS = "ip-address"
private const val A_DEVICE_MODEL = "device-model"
private const val A_DEVICE_OPERATING_SYSTEM = "device-operating-system"
private const val A_CLIENT_NAME = "client-name"
class CheckIfSectionDeviceIsVisibleUseCaseTest { class CheckIfSectionDeviceIsVisibleUseCaseTest {
private val checkIfSectionDeviceIsVisibleUseCase = CheckIfSectionDeviceIsVisibleUseCase() private val checkIfSectionDeviceIsVisibleUseCase = CheckIfSectionDeviceIsVisibleUseCase()
@Test @Test
fun `given device info with Ip address when checking is device section is visible then it returns true`() = runTest { fun `given device of any type with Ip address when checking if device section is visible then it returns true`() {
// Given DeviceType.values().forEach { deviceType ->
val deviceInfo = givenADeviceInfo(AN_IP_ADDRESS) // Given
val deviceExtendedInfo = givenAnExtendedDeviceInfo(deviceType)
val deviceFullInfo = givenADeviceFullInfo(deviceExtendedInfo)
val deviceInfo = givenADeviceInfo(ipAddress = AN_IP_ADDRESS)
every { deviceFullInfo.deviceInfo } returns deviceInfo
// When // When
val result = checkIfSectionDeviceIsVisibleUseCase.execute(deviceInfo) val result = checkIfSectionDeviceIsVisibleUseCase.execute(deviceFullInfo)
// Then // Then
result shouldBeEqualTo true result shouldBeEqualTo true
}
} }
@Test @Test
fun `given device info with empty or null Ip address when checking is device section is visible then it returns false`() = runTest { fun `given device of any type with empty or null Ip address and no extended info when checking if device section is visible then it returns false`() {
DeviceType.values().forEach { deviceType ->
// Given
val deviceExtendedInfo = givenAnExtendedDeviceInfo(deviceType)
val deviceFullInfo1 = givenADeviceFullInfo(deviceExtendedInfo)
val deviceFullInfo2 = givenADeviceFullInfo(deviceExtendedInfo)
val deviceInfo1 = givenADeviceInfo(ipAddress = "")
val deviceInfo2 = givenADeviceInfo(ipAddress = null)
every { deviceFullInfo1.deviceInfo } returns deviceInfo1
every { deviceFullInfo2.deviceInfo } returns deviceInfo2
// When
val result1 = checkIfSectionDeviceIsVisibleUseCase.execute(deviceFullInfo1)
val result2 = checkIfSectionDeviceIsVisibleUseCase.execute(deviceFullInfo2)
// Then
result1 shouldBeEqualTo false
result2 shouldBeEqualTo false
}
}
@Test
fun `given device of any type with extended info when checking if device section is visible then it returns true`() {
// Given // Given
val deviceInfo1 = givenADeviceInfo("") val deviceExtendedInfoList = listOf(
val deviceInfo2 = givenADeviceInfo(null) givenAnExtendedDeviceInfo(
DeviceType.MOBILE,
deviceModel = A_DEVICE_MODEL,
),
givenAnExtendedDeviceInfo(
DeviceType.MOBILE,
deviceOperatingSystem = A_DEVICE_OPERATING_SYSTEM,
),
givenAnExtendedDeviceInfo(
DeviceType.MOBILE,
deviceModel = A_DEVICE_MODEL,
deviceOperatingSystem = A_DEVICE_OPERATING_SYSTEM,
),
givenAnExtendedDeviceInfo(
DeviceType.DESKTOP,
deviceOperatingSystem = A_DEVICE_OPERATING_SYSTEM,
),
givenAnExtendedDeviceInfo(
DeviceType.WEB,
clientName = A_CLIENT_NAME,
),
givenAnExtendedDeviceInfo(
DeviceType.WEB,
deviceOperatingSystem = A_DEVICE_OPERATING_SYSTEM,
),
givenAnExtendedDeviceInfo(
DeviceType.WEB,
clientName = A_CLIENT_NAME,
deviceOperatingSystem = A_DEVICE_OPERATING_SYSTEM,
),
)
// When deviceExtendedInfoList.forEach { deviceExtendedInfo ->
val result1 = checkIfSectionDeviceIsVisibleUseCase.execute(deviceInfo1) val deviceFullInfo = givenADeviceFullInfo(deviceExtendedInfo)
val result2 = checkIfSectionDeviceIsVisibleUseCase.execute(deviceInfo2) val deviceInfo = givenADeviceInfo(ipAddress = null)
every { deviceFullInfo.deviceInfo } returns deviceInfo
// Then // When
result1 shouldBeEqualTo false val result = checkIfSectionDeviceIsVisibleUseCase.execute(deviceFullInfo)
result2 shouldBeEqualTo false
// Then
result shouldBeEqualTo true
}
}
private fun givenADeviceFullInfo(deviceExtendedInfo: DeviceExtendedInfo): DeviceFullInfo {
val deviceFullInfo = mockk<DeviceFullInfo>()
every { deviceFullInfo.deviceExtendedInfo } returns deviceExtendedInfo
return deviceFullInfo
} }
private fun givenADeviceInfo(ipAddress: String?): DeviceInfo { private fun givenADeviceInfo(ipAddress: String?): DeviceInfo {
@ -61,4 +133,20 @@ class CheckIfSectionDeviceIsVisibleUseCaseTest {
every { info.lastSeenIp } returns ipAddress every { info.lastSeenIp } returns ipAddress
return info return info
} }
private fun givenAnExtendedDeviceInfo(
deviceType: DeviceType,
clientName: String? = null,
clientVersion: String? = null,
deviceOperatingSystem: String? = null,
deviceModel: String? = null,
): DeviceExtendedInfo {
return DeviceExtendedInfo(
deviceType = deviceType,
clientName = clientName,
clientVersion = clientVersion,
deviceOperatingSystem = deviceOperatingSystem,
deviceModel = deviceModel,
)
}
} }

View File

@ -18,7 +18,6 @@ package im.vector.app.features.settings.devices.v2.details
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
@ -32,7 +31,7 @@ class CheckIfSectionSessionIsVisibleUseCaseTest {
private val checkIfSectionSessionIsVisibleUseCase = CheckIfSectionSessionIsVisibleUseCase() private val checkIfSectionSessionIsVisibleUseCase = CheckIfSectionSessionIsVisibleUseCase()
@Test @Test
fun `given device info with name, id or lastSeenTs when checking is session section is visible then it returns true`() = runTest { fun `given device info with name, id or lastSeenTs when checking is session section is visible then it returns true`() {
// Given // Given
val deviceInfoList = listOf( val deviceInfoList = listOf(
givenADeviceInfo( givenADeviceInfo(
@ -82,7 +81,7 @@ class CheckIfSectionSessionIsVisibleUseCaseTest {
} }
@Test @Test
fun `given device info with missing session info when checking is session section is visible then it returns true`() = runTest { fun `given device info with missing session info when checking is session section is visible then it returns false`() {
// Given // Given
val deviceInfoList = listOf( val deviceInfoList = listOf(
givenADeviceInfo( givenADeviceInfo(

View File

@ -57,12 +57,10 @@ class SessionDetailsViewModelTest {
fun `given the viewModel has been initialized then viewState is updated with session info`() { fun `given the viewModel has been initialized then viewState is updated with session info`() {
// Given // Given
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
val deviceInfo = mockk<DeviceInfo>()
every { deviceFullInfo.deviceInfo } returns deviceInfo
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(deviceFullInfo)
val expectedState = SessionDetailsViewState( val expectedState = SessionDetailsViewState(
deviceId = A_SESSION_ID, deviceId = A_SESSION_ID,
deviceInfo = Success(deviceInfo) deviceFullInfo = Success(deviceFullInfo)
) )
// When // When

View File

@ -16,8 +16,9 @@
package im.vector.app.features.settings.devices.v2.filter package im.vector.app.features.settings.devices.v2.filter
import im.vector.app.features.settings.devices.v2.DeviceExtendedInfo import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldContainAll import org.amshove.kluent.shouldContainAll
@ -37,7 +38,8 @@ private val activeVerifiedDevice = DeviceFullInfo(
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = true, isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
private val inactiveVerifiedDevice = DeviceFullInfo( private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
@ -49,7 +51,8 @@ private val inactiveVerifiedDevice = DeviceFullInfo(
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true, isInactive = true,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
private val activeUnverifiedDevice = DeviceFullInfo( private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
@ -61,7 +64,8 @@ private val activeUnverifiedDevice = DeviceFullInfo(
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false, isInactive = false,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
private val inactiveUnverifiedDevice = DeviceFullInfo( private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
@ -73,7 +77,8 @@ private val inactiveUnverifiedDevice = DeviceFullInfo(
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true, isInactive = true,
isCurrentDevice = false, isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = MatrixClientInfoContent(),
) )
private val devices = listOf( private val devices = listOf(

View File

@ -18,9 +18,11 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import im.vector.app.features.settings.devices.v2.DeviceExtendedInfo import im.vector.app.core.session.clientinfo.GetMatrixClientInfoUseCase
import im.vector.app.core.session.clientinfo.MatrixClientInfoContent
import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase
import im.vector.app.features.settings.devices.v2.details.extended.DeviceExtendedInfo
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.features.settings.devices.v2.list.DeviceType import im.vector.app.features.settings.devices.v2.list.DeviceType
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
@ -57,6 +59,7 @@ class GetDeviceFullInfoUseCaseTest {
private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>() private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>() private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>()
private val getMatrixClientInfoUseCase = mockk<GetMatrixClientInfoUseCase>()
private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase( private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
@ -64,6 +67,7 @@ class GetDeviceFullInfoUseCaseTest {
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase, checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase, parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
getMatrixClientInfoUseCase = getMatrixClientInfoUseCase,
) )
@Before @Before
@ -80,19 +84,14 @@ class GetDeviceFullInfoUseCaseTest {
fun `given current session and info for device when getting device info then the result is correct`() = runTest { fun `given current session and info for device when getting device info then the result is correct`() = runTest {
// Given // Given
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
val deviceInfo = DeviceInfo( val deviceInfo = givenADeviceInfo()
lastSeenTs = A_TIMESTAMP, val cryptoDeviceInfo = givenACryptoDeviceInfo()
)
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
val cryptoDeviceInfo = CryptoDeviceInfo(deviceId = A_DEVICE_ID, userId = "")
fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData = MutableLiveData(Optional(cryptoDeviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow()
val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo) val trustLevel = givenTrustLevel(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = false val isInactive = false
val isCurrentDevice = true val isCurrentDevice = true
every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive
every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(DeviceType.MOBILE) every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(DeviceType.MOBILE)
val matrixClientInfo = givenAMatrixClientInfo()
// When // When
val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull() val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
@ -104,14 +103,18 @@ class GetDeviceFullInfoUseCaseTest {
roomEncryptionTrustLevel = trustLevel, roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive, isInactive = isInactive,
isCurrentDevice = isCurrentDevice, isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE) deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE),
matrixClientInfo = matrixClientInfo,
) )
verify { fakeActiveSessionHolder.instance.getSafeActiveSession() } verify {
verify { getCurrentSessionCrossSigningInfoUseCase.execute() } fakeActiveSessionHolder.instance.getSafeActiveSession()
verify { getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) } getCurrentSessionCrossSigningInfoUseCase.execute()
verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow() } getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
verify { fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow() } fakeActiveSessionHolder.fakeSession.fakeCryptoService.getMyDevicesInfoLive(A_DEVICE_ID).asFlow()
verify { checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP) } fakeActiveSessionHolder.fakeSession.fakeCryptoService.getLiveCryptoDeviceInfoWithId(A_DEVICE_ID).asFlow()
checkIfSessionIsInactiveUseCase.execute(A_TIMESTAMP)
getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID)
}
} }
@Test @Test
@ -161,4 +164,27 @@ class GetDeviceFullInfoUseCaseTest {
every { getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) } returns trustLevel every { getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) } returns trustLevel
return trustLevel return trustLevel
} }
private fun givenADeviceInfo(): DeviceInfo {
val deviceInfo = DeviceInfo(
deviceId = A_DEVICE_ID,
lastSeenTs = A_TIMESTAMP,
)
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
return deviceInfo
}
private fun givenACryptoDeviceInfo(): CryptoDeviceInfo {
val cryptoDeviceInfo = CryptoDeviceInfo(deviceId = A_DEVICE_ID, userId = "")
fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData = MutableLiveData(Optional(cryptoDeviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.cryptoDeviceInfoWithIdLiveData.givenAsFlow()
return cryptoDeviceInfo
}
private fun givenAMatrixClientInfo(): MatrixClientInfoContent {
val matrixClientInfo = mockk<MatrixClientInfoContent>()
every { getMatrixClientInfoUseCase.execute(fakeActiveSessionHolder.fakeSession, A_DEVICE_ID) } returns matrixClientInfo
return matrixClientInfo
}
} }