mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 03:48:12 +03:00
Merge pull request #7247 from vector-im/feature/ons/parse_user_agent
[Device Manager] Parse user agents (PSG-762)
This commit is contained in:
commit
d0dd446af8
12 changed files with 430 additions and 16 deletions
1
changelog.d/7247.wip
Normal file
1
changelog.d/7247.wip
Normal file
|
@ -0,0 +1 @@
|
|||
[Device Manager] Parse user agents
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -26,4 +26,5 @@ data class DeviceFullInfo(
|
|||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel,
|
||||
val isInactive: Boolean,
|
||||
val isCurrentDevice: Boolean,
|
||||
val deviceExtendedInfo: DeviceExtendedInfo,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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() }
|
||||
|
|
Loading…
Reference in a new issue