Skip to content
1 change: 1 addition & 0 deletions changelog.d/7294.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Device Management] Render extended device info
7 changes: 7 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3309,6 +3309,13 @@
<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_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_rename">Rename session</string>
<string name="device_manager_session_rename_edit_hint">Session name</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ package im.vector.app.core.session.clientinfo
/**
* 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."
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

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.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
Expand All @@ -27,4 +29,5 @@ data class DeviceFullInfo(
val isInactive: Boolean,
val isCurrentDevice: Boolean,
val deviceExtendedInfo: DeviceExtendedInfo,
val matrixClientInfo: MatrixClientInfoContent?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2

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.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
Expand All @@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
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.DeviceInfo
import org.matrix.android.sdk.flow.flow
Expand All @@ -39,6 +41,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val filterDevicesUseCase: FilterDevicesUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
private val getMatrixClientInfoUseCase: GetMatrixClientInfoUseCase,
) {

fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
Expand All @@ -48,7 +51,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList ->
val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList)
val deviceFullInfoList = convertToDeviceFullInfoList(session, currentSessionCrossSigningInfo, cryptoList, infoList)
val excludedDeviceIds = if (excludeCurrentDevice) {
listOf(currentSessionCrossSigningInfo.deviceId)
} else {
Expand All @@ -62,6 +65,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
}

private fun convertToDeviceFullInfoList(
session: Session,
currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo,
cryptoList: List<CryptoDeviceInfo>,
infoList: List<DeviceInfo>,
Expand All @@ -73,8 +77,20 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId
val deviceUserAgent = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent())
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent)
val deviceExtendedInfo = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent())
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
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

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 org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
Expand Down Expand Up @@ -139,13 +140,11 @@ class ParseDeviceUserAgentUseCase @Inject constructor() {
}

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
.find { it.startsWith(browserName) }
?.split("/")
?.getOrNull(1)
?.split(".")
?.firstOrNull()
}

private fun isEdge(browserSegments: List<String>): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,36 @@

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.session.crypto.model.DeviceInfo
import javax.inject.Inject

class CheckIfSectionDeviceIsVisibleUseCase @Inject constructor() {

fun execute(deviceInfo: DeviceInfo): Boolean {
return deviceInfo.lastSeenIp?.isNotEmpty().orFalse()
fun execute(deviceFullInfo: DeviceFullInfo): Boolean {
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,44 @@ import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
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.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 javax.inject.Inject

class SessionDetailsController @Inject constructor(
private val checkIfSectionSessionIsVisibleUseCase: CheckIfSectionSessionIsVisibleUseCase,
private val checkIfSectionDeviceIsVisibleUseCase: CheckIfSectionDeviceIsVisibleUseCase,
private val checkIfSectionApplicationIsVisibleUseCase: CheckIfSectionApplicationIsVisibleUseCase,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter,
private val dimensionConverter: DimensionConverter,
) : TypedEpoxyController<DeviceInfo>() {
) : TypedEpoxyController<DeviceFullInfo>() {

var callback: Callback? = null

interface Callback {
fun onItemLongClicked(content: String)
}

override fun buildModels(data: DeviceInfo?) {
data?.let { info ->
val hasSectionSession = hasSectionSession(data)
override fun buildModels(data: DeviceFullInfo?) {
data?.let { fullInfo ->
val deviceInfo = fullInfo.deviceInfo
val matrixClientInfo = fullInfo.matrixClientInfo
val hasSectionSession = hasSectionSession(deviceInfo)
if (hasSectionSession) {
buildSectionSession(info)
buildSectionSession(deviceInfo)
}

if (hasSectionDevice(data)) {
buildSectionDevice(info, addExtraTopMargin = hasSectionSession)
val hasApplicationSection = hasSectionApplication(matrixClientInfo)
if (hasApplicationSection && matrixClientInfo != null) {
buildSectionApplication(matrixClientInfo, addExtraTopMargin = hasSectionSession)
}

if (hasSectionDevice(fullInfo)) {
buildSectionDevice(fullInfo, addExtraTopMargin = hasSectionSession || hasApplicationSection)
}
}
}
Expand Down Expand Up @@ -83,39 +94,126 @@ class SessionDetailsController @Inject constructor(
}

private fun buildSectionSession(data: DeviceInfo) {
val sessionName = data.displayName
val sessionId = data.deviceId
val sessionLastSeenTs = data.lastSeenTs
val sessionName = data.displayName.orEmpty()
val sessionId = data.deviceId.orEmpty()
val sessionLastSeenTs = data.lastSeenTs ?: -1

buildHeaderItem(R.string.device_manager_session_title)

sessionName?.let {
val hasDivider = sessionId != null || sessionLastSeenTs != null
buildContentItem(R.string.device_manager_session_details_session_name, it, hasDivider)
if (sessionName.isNotEmpty()) {
val hasDivider = sessionId.isNotEmpty() || sessionLastSeenTs > 0
buildContentItem(R.string.device_manager_session_details_session_name, sessionName, hasDivider)
}
sessionId?.let {
val hasDivider = sessionLastSeenTs != null
buildContentItem(R.string.device_manager_session_details_session_id, it, hasDivider)
if (sessionId.isNotEmpty()) {
val hasDivider = sessionLastSeenTs > 0
buildContentItem(R.string.device_manager_session_details_session_id, sessionId, hasDivider)
}
sessionLastSeenTs?.let {
val formattedDate = dateFormatter.format(it, DateFormatKind.MESSAGE_DETAIL)
if (sessionLastSeenTs > 0) {
val formattedDate = dateFormatter.format(sessionLastSeenTs, DateFormatKind.MESSAGE_DETAIL)
val hasDivider = false
buildContentItem(R.string.device_manager_session_details_session_last_activity, formattedDate, hasDivider)
}
}

private fun hasSectionDevice(data: DeviceInfo): Boolean {
return checkIfSectionDeviceIsVisibleUseCase.execute(data)
private fun hasSectionApplication(matrixClientInfoContent: MatrixClientInfoContent?): Boolean {
return checkIfSectionApplicationIsVisibleUseCase.execute(matrixClientInfoContent)
}

private fun buildSectionDevice(data: DeviceInfo, addExtraTopMargin: Boolean) {
val lastSeenIp = data.lastSeenIp
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)
}

private fun buildSectionDevice(data: DeviceFullInfo, addExtraTopMargin: Boolean) {
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
buildContentItem(R.string.device_manager_session_details_device_ip_address, it, hasDivider)
buildContentItem(R.string.device_manager_session_details_device_ip_address, lastSeenIp, hasDivider)
}
}
}
Loading