diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceUserAgent.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceUserAgent.kt index f307025d0b..cf201fafa4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceUserAgent.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceUserAgent.kt @@ -24,21 +24,13 @@ data class DeviceUserAgent( */ val deviceType: DeviceType, /** - * i.e. Google - */ - val deviceManufacturer: String? = null, - /** - * i.e. Pixel 6 + * i.e. Google Pixel 6 */ val deviceModel: String? = null, - /** - * i.e. Android - */ - val deviceOperatingSystem: String? = null, /** * i.e. Android 11 */ - val deviceOperatingSystemVersion: String? = null, + val deviceOperatingSystem: String? = null, /** * i.e. Element Nightly */ diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt index b2341e23f7..e4cdbb0a87 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt @@ -38,6 +38,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor( private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val filterDevicesUseCase: FilterDevicesUseCase, + private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, ) { fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow> { @@ -72,7 +73,8 @@ class GetDeviceFullInfoListUseCase @Inject constructor( val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoDeviceInfo) val isInactive = checkIfSessionIsInactiveUseCase.execute(deviceInfo.lastSeenTs ?: 0) val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoDeviceInfo?.deviceId - DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice) + val deviceUserAgent = parseDeviceUserAgentUseCase.execute(deviceInfo.lastSeenUserAgent) + DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent) } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt index 1ea7dabbe4..fe02aa6400 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ParseDeviceUserAgentUseCase.kt @@ -21,7 +21,9 @@ import javax.inject.Inject class ParseDeviceUserAgentUseCase @Inject constructor() { - fun execute(userAgent: String): DeviceUserAgent { + fun execute(userAgent: String?): DeviceUserAgent { + if (userAgent == null) return createUnknownUserAgent() + return when { userAgent.contains(ANDROID_KEYWORD) -> parseAndroidUserAgent(userAgent) userAgent.contains(IOS_KEYWORD) -> parseIosUserAgent(userAgent) @@ -35,23 +37,26 @@ class ParseDeviceUserAgentUseCase @Inject constructor() { val appName = userAgent.substringBefore("/") val appVersion = userAgent.substringAfter("/").substringBefore(" (") val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ") - val deviceManufacturer = deviceInfoSegments.getOrNull(0) - val deviceModel = deviceInfoSegments.getOrNull(1) - val deviceOsInfo = deviceInfoSegments.getOrNull(2)?.takeIf { it.startsWith("Android") } - val deviceOs = deviceOsInfo?.substringBefore(" ") - val deviceOsVersion = deviceOsInfo?.substringAfter(" ") - return DeviceUserAgent(DeviceType.MOBILE, deviceManufacturer, deviceModel, deviceOs, deviceOsVersion, appName, appVersion) + val deviceModel: String? + val deviceOperatingSystem: String? + if (deviceInfoSegments.firstOrNull() == "Linux") { + val deviceOperatingSystemIndex = deviceInfoSegments.indexOfFirst { it.startsWith("Android") } + deviceOperatingSystem = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex) + deviceModel = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex + 1) + } else { + deviceModel = deviceInfoSegments.getOrNull(0) + deviceOperatingSystem = deviceInfoSegments.getOrNull(1) + } + return DeviceUserAgent(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion) } private fun parseIosUserAgent(userAgent: String): DeviceUserAgent { val appName = userAgent.substringBefore("/") val appVersion = userAgent.substringAfter("/").substringBefore(" (") val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ") - val deviceManufacturer = "Apple" val deviceModel = deviceInfoSegments.getOrNull(0) - val deviceOs = deviceInfoSegments.getOrNull(1)?.substringBefore(" ") - val deviceOsVersion = deviceInfoSegments.getOrNull(1)?.substringAfter(" ") - return DeviceUserAgent(DeviceType.MOBILE, deviceManufacturer, deviceModel, deviceOs, deviceOsVersion, appName, appVersion) + val deviceOperatingSystem = deviceInfoSegments.getOrNull(1) + return DeviceUserAgent(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion) } private fun parseDesktopUserAgent(userAgent: String): DeviceUserAgent { @@ -59,9 +64,8 @@ class ParseDeviceUserAgentUseCase @Inject constructor() { val appName = appInfoSegments.getOrNull(0) val appVersion = appInfoSegments.getOrNull(1) val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ") - val deviceOs = deviceInfoSegments.getOrNull(1)?.substringBeforeLast(" ") - val deviceOsVersion = deviceInfoSegments.getOrNull(1)?.substringAfterLast(" ") - return DeviceUserAgent(DeviceType.DESKTOP, null, null, deviceOs, deviceOsVersion, appName, appVersion) + val deviceOperatingSystem = deviceInfoSegments.getOrNull(1) + return DeviceUserAgent(DeviceType.DESKTOP, null, deviceOperatingSystem, appName, appVersion) } private fun parseWebUserAgent(userAgent: String): DeviceUserAgent { @@ -76,6 +80,7 @@ class ParseDeviceUserAgentUseCase @Inject constructor() { companion object { // Element dbg/1.5.0-dev (Xiaomi; Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0) + // Legacy : Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0) private val ANDROID_KEYWORD = "; MatrixAndroidSdk2" // Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt index 121973a134..cfaa6aed85 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCase.kt @@ -19,12 +19,14 @@ package im.vector.app.features.settings.devices.v2.overview import androidx.lifecycle.asFlow import im.vector.app.core.di.ActiveSessionHolder 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.list.CheckIfSessionIsInactiveUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow +import okhttp3.internal.userAgent import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.flow.unwrap import javax.inject.Inject @@ -34,6 +36,7 @@ class GetDeviceFullInfoUseCase @Inject constructor( private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, + private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase, ) { fun execute(deviceId: String): Flow { @@ -49,12 +52,14 @@ class GetDeviceFullInfoUseCase @Inject constructor( val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo) val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId + val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.lastSeenUserAgent) DeviceFullInfo( deviceInfo = info, cryptoDeviceInfo = cryptoInfo, roomEncryptionTrustLevel = roomEncryptionTrustLevel, isInactive = isInactive, isCurrentDevice = isCurrentDevice, + deviceUserAgent = deviceUserAgent, ) } else { null diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt index 44940dd2c3..e5a3962c6c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/DevicesViewModelTest.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2 import android.os.SystemClock import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MvRxTestRule +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.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase @@ -36,6 +37,7 @@ import io.mockk.runs import io.mockk.unmockkAll import io.mockk.verify import kotlinx.coroutines.flow.flowOf +import okhttp3.internal.userAgent import org.junit.After import org.junit.Before import org.junit.Rule @@ -242,14 +244,16 @@ class DevicesViewModelTest { cryptoDeviceInfo = verifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, - isCurrentDevice = true + isCurrentDevice = true, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) val deviceFullInfo2 = DeviceFullInfo( deviceInfo = mockk(), cryptoDeviceInfo = unverifiedCryptoDeviceInfo, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = true, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) val deviceFullInfoListFlow = flowOf(deviceFullInfoList) diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCaseTest.kt index 767819fd24..0537e4f652 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCaseTest.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2 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 +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.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase @@ -53,6 +54,7 @@ class GetDeviceFullInfoListUseCaseTest { private val getEncryptionTrustLevelForDeviceUseCase = mockk() private val getCurrentSessionCrossSigningInfoUseCase = mockk() private val filterDevicesUseCase = mockk() + private val parseDeviceUserAgentUseCase = mockk() private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, @@ -60,6 +62,7 @@ class GetDeviceFullInfoListUseCaseTest { getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, filterDevicesUseCase = filterDevicesUseCase, + parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase, ) @Before @@ -110,21 +113,24 @@ class GetDeviceFullInfoListUseCaseTest { cryptoDeviceInfo = cryptoDeviceInfo1, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = true, - isCurrentDevice = true + isCurrentDevice = true, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) val expectedResult2 = DeviceFullInfo( deviceInfo = deviceInfo2, cryptoDeviceInfo = cryptoDeviceInfo2, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) val expectedResult3 = DeviceFullInfo( deviceInfo = deviceInfo3, cryptoDeviceInfo = cryptoDeviceInfo3, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = false, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1) every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt index 2bb5168190..88828419f4 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCaseTest.kt @@ -17,6 +17,8 @@ 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.DeviceUserAgent +import im.vector.app.features.settings.devices.v2.list.DeviceType import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldContainAll import org.junit.Test @@ -34,7 +36,8 @@ private val activeVerifiedDevice = DeviceFullInfo( ), roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = false, - isCurrentDevice = true + isCurrentDevice = true, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) private val inactiveVerifiedDevice = DeviceFullInfo( deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"), @@ -45,7 +48,8 @@ private val inactiveVerifiedDevice = DeviceFullInfo( ), roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, isInactive = true, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) private val activeUnverifiedDevice = DeviceFullInfo( deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"), @@ -56,7 +60,8 @@ private val activeUnverifiedDevice = DeviceFullInfo( ), roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = false, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) private val inactiveUnverifiedDevice = DeviceFullInfo( deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"), @@ -67,7 +72,8 @@ private val inactiveUnverifiedDevice = DeviceFullInfo( ), roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, isInactive = true, - isCurrentDevice = false + isCurrentDevice = false, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) private val devices = listOf( diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt index 04cd5fc492..392c737152 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/GetDeviceFullInfoUseCaseTest.kt @@ -19,7 +19,10 @@ package im.vector.app.features.settings.devices.v2.overview import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asFlow import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.DeviceUserAgent +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.DeviceType import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase @@ -53,12 +56,14 @@ class GetDeviceFullInfoUseCaseTest { private val getEncryptionTrustLevelForDeviceUseCase = mockk() private val checkIfSessionIsInactiveUseCase = mockk() private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + private val parseDeviceUserAgentUseCase = mockk() private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase( activeSessionHolder = fakeActiveSessionHolder.instance, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase, + parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase, ) @Before @@ -97,7 +102,8 @@ class GetDeviceFullInfoUseCaseTest { cryptoDeviceInfo = cryptoDeviceInfo, roomEncryptionTrustLevel = trustLevel, isInactive = isInactive, - isCurrentDevice = isCurrentDevice + isCurrentDevice = isCurrentDevice, + deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE) ) verify { fakeActiveSessionHolder.instance.getSafeActiveSession() } verify { getCurrentSessionCrossSigningInfoUseCase.execute() }