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.
*/
@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 {
override val date: Long
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 isInactive: Boolean,
val isCurrentDevice: Boolean,
val deviceExtendedInfo: DeviceExtendedInfo,
)

View file

@ -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<List<DeviceFullInfo>> {
@ -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.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 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
@ -34,6 +35,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<DeviceFullInfo> {
@ -49,12 +51,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.getBestLastSeenUserAgent())
DeviceFullInfo(
deviceInfo = info,
cryptoDeviceInfo = cryptoInfo,
roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive,
isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = deviceUserAgent,
)
} else {
null

View file

@ -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
@ -243,14 +244,16 @@ class DevicesViewModelTest {
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
isCurrentDevice = true
isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(),
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true,
isCurrentDevice = false
isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
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.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<GetEncryptionTrustLevelForDeviceUseCase>()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val filterDevicesUseCase = mockk<FilterDevicesUseCase>()
private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>()
private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
@ -60,6 +62,7 @@ class GetDeviceFullInfoListUseCaseTest {
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
filterDevicesUseCase = filterDevicesUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
)
@Before
@ -87,21 +90,21 @@ class GetDeviceFullInfoListUseCaseTest {
lastSeenTs = A_TIMESTAMP_1,
isInactive = true,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo1
cryptoDeviceInfo = cryptoDeviceInfo1,
)
val deviceInfo2 = givenADevicesInfo(
deviceId = A_DEVICE_ID_2,
lastSeenTs = A_TIMESTAMP_2,
isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
cryptoDeviceInfo = cryptoDeviceInfo2
cryptoDeviceInfo = cryptoDeviceInfo2,
)
val deviceInfo3 = givenADevicesInfo(
deviceId = A_DEVICE_ID_3,
lastSeenTs = A_TIMESTAMP_3,
isInactive = false,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
cryptoDeviceInfo = cryptoDeviceInfo3
cryptoDeviceInfo = cryptoDeviceInfo3,
)
val deviceInfoList = listOf(deviceInfo1, deviceInfo2, deviceInfo3)
every { fakeFlowSession.liveMyDevicesInfo() } returns flowOf(deviceInfoList)
@ -110,21 +113,24 @@ class GetDeviceFullInfoListUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true,
isCurrentDevice = true
isCurrentDevice = true,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2,
cryptoDeviceInfo = cryptoDeviceInfo2,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
isCurrentDevice = false
isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3,
cryptoDeviceInfo = cryptoDeviceInfo3,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false,
isCurrentDevice = false
isCurrentDevice = false,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult
@ -186,8 +192,12 @@ class GetDeviceFullInfoListUseCaseTest {
val deviceInfo = mockk<DeviceInfo>()
every { deviceInfo.deviceId } returns deviceId
every { deviceInfo.lastSeenTs } returns lastSeenTs
every { deviceInfo.getBestLastSeenUserAgent() } returns ""
every { getEncryptionTrustLevelForDeviceUseCase.execute(any(), cryptoDeviceInfo) } returns roomEncryptionTrustLevel
every { checkIfSessionIsInactiveUseCase.execute(lastSeenTs) } returns isInactive
every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(
DeviceType.MOBILE,
)
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
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.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,
deviceExtendedInfo = DeviceExtendedInfo(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,
deviceExtendedInfo = DeviceExtendedInfo(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,
deviceExtendedInfo = DeviceExtendedInfo(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,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
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.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.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<GetEncryptionTrustLevelForDeviceUseCase>()
private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val parseDeviceUserAgentUseCase = mockk<ParseDeviceUserAgentUseCase>()
private val getDeviceFullInfoUseCase = GetDeviceFullInfoUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
parseDeviceUserAgentUseCase = parseDeviceUserAgentUseCase,
)
@Before
@ -76,7 +81,7 @@ class GetDeviceFullInfoUseCaseTest {
// Given
val currentSessionCrossSigningInfo = givenCurrentSessionCrossSigningInfo()
val deviceInfo = DeviceInfo(
lastSeenTs = A_TIMESTAMP
lastSeenTs = A_TIMESTAMP,
)
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData = MutableLiveData(Optional(deviceInfo))
fakeActiveSessionHolder.fakeSession.fakeCryptoService.myDevicesInfoWithIdLiveData.givenAsFlow()
@ -87,6 +92,7 @@ class GetDeviceFullInfoUseCaseTest {
val isInactive = false
val isCurrentDevice = true
every { checkIfSessionIsInactiveUseCase.execute(any()) } returns isInactive
every { parseDeviceUserAgentUseCase.execute(any()) } returns DeviceExtendedInfo(DeviceType.MOBILE)
// When
val deviceFullInfo = getDeviceFullInfoUseCase.execute(A_DEVICE_ID).firstOrNull()
@ -97,7 +103,8 @@ class GetDeviceFullInfoUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo,
roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive,
isCurrentDevice = isCurrentDevice
isCurrentDevice = isCurrentDevice,
deviceExtendedInfo = DeviceExtendedInfo(DeviceType.MOBILE)
)
verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
verify { getCurrentSessionCrossSigningInfoUseCase.execute() }