Refactor user agent parsing.

This commit is contained in:
Onuray Sahin 2022-09-28 14:32:20 +03:00
parent 3e66a6538e
commit 04a305b403
8 changed files with 61 additions and 35 deletions

View File

@ -24,21 +24,13 @@ data class DeviceUserAgent(
*/
val deviceType: DeviceType,
/**
* i.e. Google
*/
val deviceManufacturer: String? = null,
/**
* i.e. Pixel 6
* i.e. Google Pixel 6
*/
val deviceModel: String? = null,
/**
* i.e. Android
*/
val deviceOperatingSystem: String? = null,
/**
* i.e. Android 11
*/
val deviceOperatingSystemVersion: String? = null,
val deviceOperatingSystem: String? = null,
/**
* i.e. Element Nightly
*/

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.lastSeenUserAgent)
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, roomEncryptionTrustLevel, isInactive, isCurrentDevice, deviceUserAgent)
}
}
}

View File

@ -21,7 +21,9 @@ import javax.inject.Inject
class ParseDeviceUserAgentUseCase @Inject constructor() {
fun execute(userAgent: String): DeviceUserAgent {
fun execute(userAgent: String?): DeviceUserAgent {
if (userAgent == null) return createUnknownUserAgent()
return when {
userAgent.contains(ANDROID_KEYWORD) -> parseAndroidUserAgent(userAgent)
userAgent.contains(IOS_KEYWORD) -> parseIosUserAgent(userAgent)
@ -35,23 +37,26 @@ class ParseDeviceUserAgentUseCase @Inject constructor() {
val appName = userAgent.substringBefore("/")
val appVersion = userAgent.substringAfter("/").substringBefore(" (")
val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ")
val deviceManufacturer = deviceInfoSegments.getOrNull(0)
val deviceModel = deviceInfoSegments.getOrNull(1)
val deviceOsInfo = deviceInfoSegments.getOrNull(2)?.takeIf { it.startsWith("Android") }
val deviceOs = deviceOsInfo?.substringBefore(" ")
val deviceOsVersion = deviceOsInfo?.substringAfter(" ")
return DeviceUserAgent(DeviceType.MOBILE, deviceManufacturer, deviceModel, deviceOs, deviceOsVersion, appName, appVersion)
val deviceModel: String?
val deviceOperatingSystem: String?
if (deviceInfoSegments.firstOrNull() == "Linux") {
val deviceOperatingSystemIndex = deviceInfoSegments.indexOfFirst { it.startsWith("Android") }
deviceOperatingSystem = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex)
deviceModel = deviceInfoSegments.getOrNull(deviceOperatingSystemIndex + 1)
} else {
deviceModel = deviceInfoSegments.getOrNull(0)
deviceOperatingSystem = deviceInfoSegments.getOrNull(1)
}
return DeviceUserAgent(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion)
}
private fun parseIosUserAgent(userAgent: String): DeviceUserAgent {
val appName = userAgent.substringBefore("/")
val appVersion = userAgent.substringAfter("/").substringBefore(" (")
val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ")
val deviceManufacturer = "Apple"
val deviceModel = deviceInfoSegments.getOrNull(0)
val deviceOs = deviceInfoSegments.getOrNull(1)?.substringBefore(" ")
val deviceOsVersion = deviceInfoSegments.getOrNull(1)?.substringAfter(" ")
return DeviceUserAgent(DeviceType.MOBILE, deviceManufacturer, deviceModel, deviceOs, deviceOsVersion, appName, appVersion)
val deviceOperatingSystem = deviceInfoSegments.getOrNull(1)
return DeviceUserAgent(DeviceType.MOBILE, deviceModel, deviceOperatingSystem, appName, appVersion)
}
private fun parseDesktopUserAgent(userAgent: String): DeviceUserAgent {
@ -59,9 +64,8 @@ class ParseDeviceUserAgentUseCase @Inject constructor() {
val appName = appInfoSegments.getOrNull(0)
val appVersion = appInfoSegments.getOrNull(1)
val deviceInfoSegments = userAgent.substringAfter("(").substringBefore(")").split("; ")
val deviceOs = deviceInfoSegments.getOrNull(1)?.substringBeforeLast(" ")
val deviceOsVersion = deviceInfoSegments.getOrNull(1)?.substringAfterLast(" ")
return DeviceUserAgent(DeviceType.DESKTOP, null, null, deviceOs, deviceOsVersion, appName, appVersion)
val deviceOperatingSystem = deviceInfoSegments.getOrNull(1)
return DeviceUserAgent(DeviceType.DESKTOP, null, deviceOperatingSystem, appName, appVersion)
}
private fun parseWebUserAgent(userAgent: String): DeviceUserAgent {
@ -76,6 +80,7 @@ class ParseDeviceUserAgentUseCase @Inject constructor() {
companion object {
// Element dbg/1.5.0-dev (Xiaomi; Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
// Legacy : Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
private val ANDROID_KEYWORD = "; MatrixAndroidSdk2"
// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)

View File

@ -19,12 +19,14 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import okhttp3.internal.userAgent
import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.flow.unwrap
import javax.inject.Inject
@ -34,6 +36,7 @@ class GetDeviceFullInfoUseCase @Inject constructor(
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val parseDeviceUserAgentUseCase: ParseDeviceUserAgentUseCase,
) {
fun execute(deviceId: String): Flow<DeviceFullInfo> {
@ -49,12 +52,14 @@ class GetDeviceFullInfoUseCase @Inject constructor(
val roomEncryptionTrustLevel = getEncryptionTrustLevelForDeviceUseCase.execute(currentSessionCrossSigningInfo, cryptoInfo)
val isInactive = checkIfSessionIsInactiveUseCase.execute(info.lastSeenTs ?: 0)
val isCurrentDevice = currentSessionCrossSigningInfo.deviceId == cryptoInfo.deviceId
val deviceUserAgent = parseDeviceUserAgentUseCase.execute(info.lastSeenUserAgent)
DeviceFullInfo(
deviceInfo = info,
cryptoDeviceInfo = cryptoInfo,
roomEncryptionTrustLevel = roomEncryptionTrustLevel,
isInactive = isInactive,
isCurrentDevice = isCurrentDevice,
deviceUserAgent = deviceUserAgent,
)
} else {
null

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
@ -36,6 +37,7 @@ import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import okhttp3.internal.userAgent
import org.junit.After
import org.junit.Before
import org.junit.Rule
@ -242,14 +244,16 @@ class DevicesViewModelTest {
cryptoDeviceInfo = verifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
isCurrentDevice = true
isCurrentDevice = true,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
val deviceFullInfo2 = DeviceFullInfo(
deviceInfo = mockk(),
cryptoDeviceInfo = unverifiedCryptoDeviceInfo,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList)

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
@ -110,21 +113,24 @@ class GetDeviceFullInfoListUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo1,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true,
isCurrentDevice = true
isCurrentDevice = true,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
val expectedResult2 = DeviceFullInfo(
deviceInfo = deviceInfo2,
cryptoDeviceInfo = cryptoDeviceInfo2,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
val expectedResult3 = DeviceFullInfo(
deviceInfo = deviceInfo3,
cryptoDeviceInfo = cryptoDeviceInfo3,
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult

View File

@ -17,6 +17,8 @@
package im.vector.app.features.settings.devices.v2.filter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.DeviceUserAgent
import im.vector.app.features.settings.devices.v2.list.DeviceType
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldContainAll
import org.junit.Test
@ -34,7 +36,8 @@ private val activeVerifiedDevice = DeviceFullInfo(
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false,
isCurrentDevice = true
isCurrentDevice = true,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
@ -45,7 +48,8 @@ private val inactiveVerifiedDevice = DeviceFullInfo(
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
@ -56,7 +60,8 @@ private val activeUnverifiedDevice = DeviceFullInfo(
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
@ -67,7 +72,8 @@ private val inactiveUnverifiedDevice = DeviceFullInfo(
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true,
isCurrentDevice = false
isCurrentDevice = false,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
private val devices = listOf(

View File

@ -19,7 +19,10 @@ package im.vector.app.features.settings.devices.v2.overview
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.DeviceUserAgent
import im.vector.app.features.settings.devices.v2.ParseDeviceUserAgentUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.features.settings.devices.v2.list.DeviceType
import im.vector.app.features.settings.devices.v2.verification.CurrentSessionCrossSigningInfo
import im.vector.app.features.settings.devices.v2.verification.GetCurrentSessionCrossSigningInfoUseCase
import im.vector.app.features.settings.devices.v2.verification.GetEncryptionTrustLevelForDeviceUseCase
@ -53,12 +56,14 @@ class GetDeviceFullInfoUseCaseTest {
private val getEncryptionTrustLevelForDeviceUseCase = mockk<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
@ -97,7 +102,8 @@ class GetDeviceFullInfoUseCaseTest {
cryptoDeviceInfo = cryptoDeviceInfo,
roomEncryptionTrustLevel = trustLevel,
isInactive = isInactive,
isCurrentDevice = isCurrentDevice
isCurrentDevice = isCurrentDevice,
deviceUserAgent = DeviceUserAgent(DeviceType.MOBILE)
)
verify { fakeActiveSessionHolder.instance.getSafeActiveSession() }
verify { getCurrentSessionCrossSigningInfoUseCase.execute() }