Merge pull request #7247 from vector-im/feature/ons/parse_user_agent

[Device Manager] Parse user agents (PSG-762)
This commit is contained in:
Onuray Sahin 2022-09-30 18:36:33 +03:00 committed by GitHub
commit d0dd446af8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 16 deletions

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

@ -0,0 +1 @@
[Device Manager] Parse user agents

View File

@ -52,9 +52,17 @@ data class DeviceInfo(
* The last ip address. * The last ip address.
*/ */
@Json(name = "last_seen_ip") @Json(name = "last_seen_ip")
val lastSeenIp: String? = null val lastSeenIp: String? = null,
@Json(name = "org.matrix.msc3852.last_seen_user_agent")
val unstableLastSeenUserAgent: String? = null,
@Json(name = "last_seen_user_agent")
val lastSeenUserAgent: String? = null,
) : DatedObject { ) : DatedObject {
override val date: Long override val date: Long
get() = lastSeenTs ?: 0 get() = lastSeenTs ?: 0
fun getBestLastSeenUserAgent() = lastSeenUserAgent ?: unstableLastSeenUserAgent
} }

View File

@ -0,0 +1,42 @@
/*
* 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
import im.vector.app.features.settings.devices.v2.list.DeviceType
data class DeviceExtendedInfo(
/**
* One of MOBILE, WEB, DESKTOP or UNKNOWN.
*/
val deviceType: DeviceType,
/**
* i.e. Google Pixel 6.
*/
val deviceModel: String? = null,
/**
* i.e. Android 11.
*/
val deviceOperatingSystem: String? = null,
/**
* i.e. Element Nightly.
*/
val clientName: String? = null,
/**
* i.e. 1.5.0.
*/
val clientVersion: String? = null,
)

View File

@ -26,4 +26,5 @@ data class DeviceFullInfo(
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel, val roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
val isInactive: Boolean, val isInactive: Boolean,
val isCurrentDevice: Boolean, val isCurrentDevice: Boolean,
val deviceExtendedInfo: DeviceExtendedInfo,
) )

View File

@ -38,6 +38,7 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val filterDevicesUseCase: FilterDevicesUseCase, private val filterDevicesUseCase: FilterDevicesUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
) { ) {
fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> { fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
@ -72,7 +73,8 @@ 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
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice) val deviceUserAgent = parseDeviceUserAgentUseCase.execute(deviceInfo.getBestLastSeenUserAgent())
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent)
} }
} }
} }

View File

@ -0,0 +1,192 @@
/*
* 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
import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
class ParseDeviceUserAgentUseCase @Inject constructor() {
fun execute(userAgent: String?): DeviceExtendedInfo {
if (userAgent == null) return createUnknownUserAgent()
return when {
userAgent.contains(ANDROID_KEYWORD) -> parseAndroidUserAgent(userAgent)
userAgent.contains(IOS_KEYWORD) -> parseIosUserAgent(userAgent)
userAgent.contains(DESKTOP_KEYWORD) -> parseDesktopUserAgent(userAgent)
userAgent.contains(WEB_KEYWORD) -> parseWebUserAgent(userAgent)
else -> createUnknownUserAgent()
}
}
private fun parseAndroidUserAgent(userAgent: String): DeviceExtendedInfo {
val appName = userAgent.substringBefore("/")
val appVersion = userAgent.substringAfter("/").substringBefore(" (")
val deviceInfoSegments = userAgent.substringAfter("(").substringBeforeLast(")").split("; ")
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 DeviceExtendedInfo(
deviceType = DeviceType.MOBILE,
deviceModel = deviceModel,
deviceOperatingSystem = deviceOperatingSystem,
clientName = appName,
clientVersion = appVersion
)
}
private fun parseIosUserAgent(userAgent: String): DeviceExtendedInfo {
val appName = userAgent.substringBefore("/")
val appVersion = userAgent.substringAfter("/").substringBefore(" (")
val deviceInfoSegments = userAgent.substringAfter("(").substringBeforeLast(")").split("; ")
val deviceModel = deviceInfoSegments.getOrNull(0)
val deviceOperatingSystem = deviceInfoSegments.getOrNull(1)
return DeviceExtendedInfo(
deviceType = DeviceType.MOBILE,
deviceModel = deviceModel,
deviceOperatingSystem = deviceOperatingSystem,
clientName = appName,
clientVersion = appVersion
)
}
private fun parseDesktopUserAgent(userAgent: String): DeviceExtendedInfo {
val browserSegments = userAgent.split(" ")
val (browserName, browserVersion) = when {
isFirefox(browserSegments) -> {
Pair("Firefox", getBrowserVersion(browserSegments, "Firefox"))
}
isEdge(browserSegments) -> {
Pair("Edge", getBrowserVersion(browserSegments, "Edge"))
}
isMobile(browserSegments) -> {
when (val name = getMobileBrowserName(browserSegments)) {
null -> {
Pair(null, null)
}
"Safari" -> {
Pair(name, getBrowserVersion(browserSegments, "Version"))
}
else -> {
Pair(name, getBrowserVersion(browserSegments, name))
}
}
}
isSafari(browserSegments) -> {
Pair("Safari", getBrowserVersion(browserSegments, "Version"))
}
else -> {
when (val name = getRegularBrowserName(browserSegments)) {
null -> {
Pair(null, null)
}
else -> {
Pair(name, getBrowserVersion(browserSegments, name))
}
}
}
}
val deviceOperatingSystemSegments = userAgent.substringAfter("(").substringBefore(")").split("; ")
val deviceOperatingSystem = if (deviceOperatingSystemSegments.getOrNull(1)?.startsWith("Android").orFalse()) {
deviceOperatingSystemSegments.getOrNull(1)
} else {
deviceOperatingSystemSegments.getOrNull(0)
}
return DeviceExtendedInfo(
deviceType = DeviceType.DESKTOP,
deviceModel = null,
deviceOperatingSystem = deviceOperatingSystem,
clientName = browserName,
clientVersion = browserVersion,
)
}
private fun parseWebUserAgent(userAgent: String): DeviceExtendedInfo {
return parseDesktopUserAgent(userAgent).copy(
deviceType = DeviceType.WEB
)
}
private fun createUnknownUserAgent(): DeviceExtendedInfo {
return DeviceExtendedInfo(DeviceType.UNKNOWN)
}
private fun isFirefox(browserSegments: List<String>): Boolean {
return browserSegments.lastOrNull()?.startsWith("Firefox").orFalse()
}
private fun getBrowserVersion(browserSegments: List<String>, browserName: String): String? {
// Chrome/104.0.3497.100 -> 104
return browserSegments
.find { it.startsWith(browserName) }
?.split("/")
?.getOrNull(1)
?.split(".")
?.firstOrNull()
}
private fun isEdge(browserSegments: List<String>): Boolean {
return browserSegments.lastOrNull()?.startsWith("Edge").orFalse()
}
private fun isSafari(browserSegments: List<String>): Boolean {
return browserSegments.lastOrNull()?.startsWith("Safari").orFalse() &&
browserSegments.getOrNull(browserSegments.size - 2)?.startsWith("Version").orFalse()
}
private fun isMobile(browserSegments: List<String>): Boolean {
return browserSegments.getOrNull(browserSegments.size - 2)?.startsWith("Mobile").orFalse()
}
private fun getMobileBrowserName(browserSegments: List<String>): String? {
val possibleBrowserName = browserSegments.getOrNull(browserSegments.size - 3)?.split("/")?.firstOrNull()
return if (possibleBrowserName == "Version") {
"Safari"
} else {
possibleBrowserName
}
}
private fun getRegularBrowserName(browserSegments: List<String>): String? {
return browserSegments.getOrNull(browserSegments.size - 2)?.split("/")?.firstOrNull()
}
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 const val ANDROID_KEYWORD = "; MatrixAndroidSdk2"
// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)
private const val IOS_KEYWORD = "; iOS "
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301
// Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36
private const val DESKTOP_KEYWORD = " Electron/"
// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
private const val WEB_KEYWORD = "Mozilla/"
}
}

View File

@ -19,6 +19,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.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.list.CheckIfSessionIsInactiveUseCase 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.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase
@ -34,6 +35,7 @@ class GetDeviceFullInfoUseCase @Inject constructor(
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
) { ) {
fun execute(deviceId: String): Flow<DeviceFullInfo> { fun execute(deviceId: String): Flow<DeviceFullInfo> {
@ -49,12 +51,14 @@ class GetDeviceFullInfoUseCase @Inject constructor(
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo) val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0) val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId
val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.getBestLastSeenUserAgent())
DeviceFullInfo( DeviceFullInfo(
deviceInfo = info, deviceInfo = info,
cryptoDeviceInfo = cryptoInfo, cryptoDeviceInfo = cryptoInfo,
roomEncryptionTrustLevel = roomEncryptionTrustLevel, roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive, isInactive = isInactive,
isCurrentDevice = isCurrentDevice, isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = deviceUserAgent,
) )
} else { } else {
null null

View File

@ -19,6 +19,7 @@ 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.MvRxTestRule 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.CheckIfCurrentSessionCanBeVerifiedUseCase
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo 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.GetCurrentSessionCrossSigningInfoUseCase
@ -243,14 +244,16 @@ class DevicesViewModelTest {
cryptoDeviceInfo = verifiedCryptoDeviceInfo, cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = true isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
val deviceFullInfo2 = DeviceFullInfo( val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(), deviceInfo = mockk(),
cryptoDeviceInfo = unverifiedCryptoDeviceInfo, cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true, isInactive = true,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2) val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList) val deviceFullInfoListFlow = flowOf(deviceFullInfoList)

View File

@ -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.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
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
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase
@ -53,6 +54,7 @@ class GetDeviceFullInfoListUseCaseTest {
private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>() private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>()
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 getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase( private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
@ -60,6 +62,7 @@ class GetDeviceFullInfoListUseCaseTest {
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
filterDevicesUseCase = filterDevicesUseCase, filterDevicesUseCase = filterDevicesUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
) )
@Before @Before
@ -87,21 +90,21 @@ class GetDeviceFullInfoListUseCaseTest {
lastSeenTs = A_TIMESTAMP_1, lastSeenTs = A_TIMESTAMP_1,
isInactive = true, isInactive = true,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo1 cryptoDeviceInfo = cryptoDeviceInfo1,
) )
val deviceInfo2 = givenADevicesInfo( val deviceInfo2 = givenADevicesInfo(
deviceId = A_DEVICE_ID_2, deviceId = A_DEVICE_ID_2,
lastSeenTs = A_TIMESTAMP_2, lastSeenTs = A_TIMESTAMP_2,
isInactive = false, isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo2 cryptoDeviceInfo = cryptoDeviceInfo2,
) )
val deviceInfo3 = givenADevicesInfo( val deviceInfo3 = givenADevicesInfo(
deviceId = A_DEVICE_ID_3, deviceId = A_DEVICE_ID_3,
lastSeenTs = A_TIMESTAMP_3, lastSeenTs = A_TIMESTAMP_3,
isInactive = false, isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
cryptoDeviceInfo = cryptoDeviceInfo3 cryptoDeviceInfo = cryptoDeviceInfo3,
) )
val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3) val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3)
every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList) every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList)
@ -110,21 +113,24 @@ class GetDeviceFullInfoListUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo1, cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true, isInactive = true,
isCurrentDevice = true isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
val expectedResult2 = DeviceFullInfo( val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2, deviceInfo = deviceInfo2,
cryptoDeviceInfo = cryptoDeviceInfo2, cryptoDeviceInfo = cryptoDeviceInfo2,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
val expectedResult3 = DeviceFullInfo( val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3, deviceInfo = deviceInfo3,
cryptoDeviceInfo = cryptoDeviceInfo3, cryptoDeviceInfo = cryptoDeviceInfo3,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false, isInactive = false,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
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
@ -186,8 +192,12 @@ class GetDeviceFullInfoListUseCaseTest {
val deviceInfo = mockk<DeviceInfo>() val deviceInfo = mockk<DeviceInfo>()
every { deviceInfo.deviceId } returns deviceId every { deviceInfo.deviceId } returns deviceId
every { deviceInfo.lastSeenTs } returns lastSeenTs every { deviceInfo.lastSeenTs } returns lastSeenTs
every { deviceInfo.getBestLastSeenUserAgent() } returns ""
every { getEncryptionTrustLevelForDeviceUseCase.execute(any(), cryptoDeviceInfo) } returns roomEncryptionTrustLevel every { getEncryptionTrustLevelForDeviceUseCase.execute(any(), cryptoDeviceInfo) } returns roomEncryptionTrustLevel
every { checkIfSessionIsInactiveUseCase.execute(lastSeenTs) } returns isInactive every { checkIfSessionIsInactiveUseCase.execute(lastSeenTs) } returns isInactive
every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(
DeviceType.MOBILE,
)
return deviceInfo return deviceInfo
} }

View File

@ -0,0 +1,138 @@
/*
* 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
import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
private val A_USER_AGENT_LIST_FOR_ANDROID = listOf(
// New User Agent Implementation
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
// Legacy User Agent Implementation
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
)
private val AN_EXPECTED_RESULT_LIST_FOR_ANDROID = listOf(
DeviceExtendedInfo(DeviceType.MOBILE, "Xiaomi Mi 9T", "Android 11", "Element dbg", "1.5.0-dev"),
DeviceExtendedInfo(DeviceType.MOBILE, "Samsung SM-G960F", "Android 6.0.1", "Element", "1.5.0"),
DeviceExtendedInfo(DeviceType.MOBILE, "Google Nexus 5", "Android 7.0", "Element", "1.5.0"),
DeviceExtendedInfo(DeviceType.MOBILE, "Google (Nexus) 5", "Android 7.0", "Element", "1.5.0"),
DeviceExtendedInfo(DeviceType.MOBILE, "Google (Nexus) (5)", "Android 7.0", "Element", "1.5.0"),
DeviceExtendedInfo(DeviceType.MOBILE, "SM-A510F Build/MMB29", "Android 6.0.1", "Element", "1.0.0"),
DeviceExtendedInfo(DeviceType.MOBILE, "SM-G610M Build/NRD90M", "Android 7.0", "Element", "1.0.0"),
)
private val A_USER_AGENT_LIST_FOR_IOS = listOf(
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
)
private val AN_EXPECTED_RESULT_LIST_FOR_IOS = listOf(
DeviceExtendedInfo(DeviceType.MOBILE, "iPhone", "iOS 15.2", "Element", "1.8.21"),
DeviceExtendedInfo(DeviceType.MOBILE, "iPhone XS Max", "iOS 15.2", "Element", "1.8.21"),
DeviceExtendedInfo(DeviceType.MOBILE, "iPad Pro (11-inch)", "iOS 15.2", "Element", "1.8.21"),
DeviceExtendedInfo(DeviceType.MOBILE, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2",
"Element", "1.8.21"),
)
private val A_USER_AGENT_LIST_FOR_DESKTOP = listOf(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 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(
DeviceExtendedInfo(DeviceType.DESKTOP, null, "Macintosh", "Electron", "20"),
DeviceExtendedInfo(DeviceType.DESKTOP, null, "Windows NT 10.0", "Electron", "20"),
)
private val A_USER_AGENT_LIST_FOR_WEB = listOf(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
"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(
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Chrome", "104"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Chrome", "104"),
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Firefox", "39"),
DeviceExtendedInfo(DeviceType.WEB, null, "Macintosh", "Safari", "8"),
DeviceExtendedInfo(DeviceType.WEB, null, "Android 9", "Chrome", "69"),
DeviceExtendedInfo(DeviceType.WEB, null, "iPad", "Safari", "8"),
DeviceExtendedInfo(DeviceType.WEB, null, "iPhone", "Safari", "8"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 6.0", "Firefox", "40"),
DeviceExtendedInfo(DeviceType.WEB, null, "Windows NT 10.0", "Edge", "12"),
)
private val AN_UNKNOWN_USER_AGENT_LIST = listOf(
"AppleTV11,1/11.1",
"Curl Client/1.0",
)
private val AN_UNKNOWN_USER_AGENT_EXPECTED_RESULT_LIST = listOf(
DeviceExtendedInfo(DeviceType.UNKNOWN, null, null, null, null),
DeviceExtendedInfo(DeviceType.UNKNOWN, null, null, null, null),
)
class ParseDeviceUserAgentUseCaseTest {
private val parseDeviceUserAgentUseCase = ParseDeviceUserAgentUseCase()
@Test
fun `given an Android user agent then it should be parsed as expected`() {
A_USER_AGENT_LIST_FOR_ANDROID.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_EXPECTED_RESULT_LIST_FOR_ANDROID[index]
}
}
@Test
fun `given an iOS user agent then it should be parsed as expected`() {
A_USER_AGENT_LIST_FOR_IOS.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_EXPECTED_RESULT_LIST_FOR_IOS[index]
}
}
@Test
fun `given a Desktop user agent then it should be parsed as expected`() {
A_USER_AGENT_LIST_FOR_DESKTOP.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_EXPECTED_RESULT_LIST_FOR_DESKTOP[index]
}
}
@Test
fun `given a Web user agent then it should be parsed as expected`() {
A_USER_AGENT_LIST_FOR_WEB.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_EXPECTED_RESULT_LIST_FOR_WEB[index]
}
}
@Test
fun `given an unknown user agent then it should be parsed as expected`() {
AN_UNKNOWN_USER_AGENT_LIST.forEachIndexed { index, userAgent ->
parseDeviceUserAgentUseCase.execute(userAgent) shouldBeEqualTo AN_UNKNOWN_USER_AGENT_EXPECTED_RESULT_LIST[index]
}
}
}

View File

@ -16,7 +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.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.DeviceFullInfo
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
import org.junit.Test import org.junit.Test
@ -34,7 +36,8 @@ private val activeVerifiedDevice = DeviceFullInfo(
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false, isInactive = false,
isCurrentDevice = true isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
private val inactiveVerifiedDevice = DeviceFullInfo( private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
@ -45,7 +48,8 @@ private val inactiveVerifiedDevice = DeviceFullInfo(
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true, isInactive = true,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
private val activeUnverifiedDevice = DeviceFullInfo( private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
@ -56,7 +60,8 @@ private val activeUnverifiedDevice = DeviceFullInfo(
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false, isInactive = false,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
private val inactiveUnverifiedDevice = DeviceFullInfo( private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"), deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
@ -67,7 +72,8 @@ private val inactiveUnverifiedDevice = DeviceFullInfo(
), ),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning, roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true, isInactive = true,
isCurrentDevice = false isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
private val devices = listOf( private val devices = listOf(

View File

@ -18,8 +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.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.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.verification.CurrentSessionCrossSigningInfo 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.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase
@ -53,12 +56,14 @@ class GetDeviceFullInfoUseCaseTest {
private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>() private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>()
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 getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase( private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance, activeSessionHolder = fakeActiveSessionHolder.instance,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase, getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase, getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase, checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
) )
@Before @Before
@ -76,7 +81,7 @@ class GetDeviceFullInfoUseCaseTest {
// Given // Given
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo() val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
val deviceInfo = DeviceInfo( val deviceInfo = DeviceInfo(
lastSeenTs = A_TIMESTAMP lastSeenTs = A_TIMESTAMP,
) )
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo)) fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow() fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
@ -87,6 +92,7 @@ class GetDeviceFullInfoUseCaseTest {
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)
// When // When
val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull() val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
@ -97,7 +103,8 @@ class GetDeviceFullInfoUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo, cryptoDeviceInfo = cryptoDeviceInfo,
roomEncryptionTrustLevel = trustLevel, roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive, isInactive = isInactive,
isCurrentDevice = isCurrentDevice isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
) )
verify { fakeActiveSessionHolder.instance.getSafeActiveSession() } verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
verify { getCurrentSessionCrossSigningInfoUseCase.execute() } verify { getCurrentSessionCrossSigningInfoUseCase.execute() }